From b90b286f4a329906ee42154e49240301adf36bc1 Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Sun, 18 Feb 2024 11:26:01 +0000 Subject: [PATCH] v3.5 auto-build from v4 --- includes/cli-server/router.php | 16 + .../sqlite/class-wp-sqlite-translator.php | 4 + .../Symfony/Component/Process/Process.php | 19 + src/Extension/IsolationSupport.php | 414 +++++++++++++++ src/Lib/Generator/AbstractGenerator.php | 78 +++ src/Lib/Generator/WPAjax.php | 67 +++ src/Lib/Generator/WPCanonical.php | 57 ++ src/Lib/Generator/WPRestApi.php | 80 +++ src/Lib/Generator/WPRestController.php | 464 +++++++++++++++++ .../Generator/WPRestPostTypeController.php | 490 ++++++++++++++++++ src/Lib/Generator/WPUnit.php | 3 + src/Lib/Generator/WPXML.php | 70 +++ src/Lib/Generator/WPXMLRPC.php | 78 +++ src/Module/WPLoader/FiltersGroup.php | 75 +++ src/Project/PluginProject.php | 8 + src/Project/ThemeProject.php | 8 + src/Template/Wpbrowser.php | 4 + src/TestCase/WPTestCase.php | 199 ++++++- src/Utils/ChromedriverInstaller.php | 228 ++++++++ src/Utils/Random.php | 8 + .../_generated/AcceptanceTesterActions.php | 4 + .../Lib/Generator/GenerationCommandsTest.php | 102 ++++ .../WPBrowser/Project/ThemeProjectTest.php | 54 ++ .../Utils/ChromedriverInstallerTest.php | 60 +++ .../RunInSeparateProcessAnnotationTest.php | 74 +++ .../RunInSeparateProcessAttributeTest.php | 76 +++ ...nTestsInSeparateProcessesAttributeTest.php | 27 + 27 files changed, 2761 insertions(+), 6 deletions(-) create mode 100644 src/Extension/IsolationSupport.php create mode 100644 src/Lib/Generator/AbstractGenerator.php create mode 100644 src/Lib/Generator/WPAjax.php create mode 100644 src/Lib/Generator/WPCanonical.php create mode 100644 src/Lib/Generator/WPRestApi.php create mode 100644 src/Lib/Generator/WPRestController.php create mode 100644 src/Lib/Generator/WPRestPostTypeController.php create mode 100644 src/Lib/Generator/WPXML.php create mode 100644 src/Lib/Generator/WPXMLRPC.php create mode 100644 src/Module/WPLoader/FiltersGroup.php create mode 100644 tests/unit/lucatume/WPBrowser/Lib/Generator/GenerationCommandsTest.php create mode 100644 tests/wploadersuite/RunInSeparateProcessAnnotationTest.php create mode 100644 tests/wploadersuite/RunInSeparateProcessAttributeTest.php create mode 100644 tests/wploadersuite/RunTestsInSeparateProcessesAttributeTest.php diff --git a/includes/cli-server/router.php b/includes/cli-server/router.php index d7e85a3fe..12208c97d 100644 --- a/includes/cli-server/router.php +++ b/includes/cli-server/router.php @@ -22,6 +22,7 @@ } ]; +<<<<<<< Updated upstream if (file_exists($root . $path)) { // Enforces trailing slash, keeping links tidy in the admin if (is_dir($root . $path) && substr_compare($path, '/', -strlen('/')) !== 0) { @@ -36,6 +37,21 @@ } else { return false; } +======= + // Enforces trailing slash, keeping links tidy in the admin + if ( is_dir( $root.$path ) && substr_compare($path, '/', -strlen('/')) !== 0 ) { + header( "Location: $path/" ); + exit; + } + + // Runs PHP file if it exists + if ( strpos($path, '.php') !== false ) { + chdir( dirname( $root.$path ) ); + require_once $root.$path; + } else { + return false; + } +>>>>>>> Stashed changes } else { // Otherwise, run `index.php` chdir($root); diff --git a/includes/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-translator.php b/includes/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-translator.php index 0f1d4a302..05e6e4c48 100644 --- a/includes/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-translator.php +++ b/includes/sqlite-database-integration/wp-includes/sqlite/class-wp-sqlite-translator.php @@ -1694,7 +1694,11 @@ private function preprocess_like_expr( &$token ) { /* Remove the quotes around the name. */ $unescaped_value = mb_substr( $token->token, 1, -1, 'UTF-8' ); if ( strpos($unescaped_value, '\_') !== false || strpos($unescaped_value, '\%') !== false ) { +<<<<<<< Updated upstream $this->like_escape_count ++; +======= + ++$this->like_escape_count; +>>>>>>> Stashed changes return str_replace( array( '\_', '\%' ), array( self::LIKE_ESCAPE_CHAR . '_', self::LIKE_ESCAPE_CHAR . '%' ), diff --git a/src/Adapters/Symfony/Component/Process/Process.php b/src/Adapters/Symfony/Component/Process/Process.php index c9bc6e399..3e57252a8 100644 --- a/src/Adapters/Symfony/Component/Process/Process.php +++ b/src/Adapters/Symfony/Component/Process/Process.php @@ -9,6 +9,13 @@ class Process extends SymfonyProcess { /** +<<<<<<< Updated upstream +======= + * @var bool|null + */ + private static $inheritEnvironmentVariables; + /** +>>>>>>> Stashed changes * @var bool */ private $createNewConsole = false; @@ -27,8 +34,20 @@ public function __construct( ?float $timeout = 60, array $options = null ) { +<<<<<<< Updated upstream if (method_exists($this, 'inheritEnvironmentVariables')) { parent::__construct($command, $cwd, $env, $input, $timeout, $options); //@phpstan-ignore-line +======= + parent::__construct($command, $cwd, $env, $input, $timeout, $options); //@phpstan-ignore-line + + if (self::$inheritEnvironmentVariables === null) { + self::$inheritEnvironmentVariables = method_exists($this, 'inheritEnvironmentVariables') + && strpos((string)(new ReflectionMethod($this, 'inheritEnvironmentVariables'))->getDocComment(), '@deprecated') === false; + } + + if (self::$inheritEnvironmentVariables) { + // @phpstan-ignore-next-line +>>>>>>> Stashed changes $this->inheritEnvironmentVariables(true); } diff --git a/src/Extension/IsolationSupport.php b/src/Extension/IsolationSupport.php new file mode 100644 index 000000000..dab9bb41e --- /dev/null +++ b/src/Extension/IsolationSupport.php @@ -0,0 +1,414 @@ + + */ + public static $events = [ + Events::SUITE_INIT => 'onSuiteInit', + Events::TEST_START => 'onTestStart', + Events::TEST_FAIL => 'onTestFail', + ]; + + /** + * @var string + */ + private $processCode = <<< PHP +\$dataName = \$this->getName(); +\$args = func_get_args(); +foreach(\$args as &\$arg){ + if(\$arg instanceof \Closure){ + \$arg = new \lucatume\WPBrowser\Opis\Closure\SerializableClosure(\$arg); + } +} +\$encodedDataSet = base64_encode(serialize(\$args)); +\$modules = \$this->getMetadata()->getCurrent('modules'); +\$wploderModuleNameInSuite = isset(\$modules['WPLoader']) ? 'WPLoader' : \lucatume\WPBrowser\Module\WPLoader::class; +\$command = [ + \lucatume\WPBrowser\Utils\Composer::binDir('codecept'), + \lucatume\WPBrowser\Command\RunOriginal::getCommandName(), + sprintf('%s:%s', codecept_relative_path('{{file}}'), '{{name}}'), + '--override', + "modules: config: {\$wploderModuleNameInSuite}: skipInstall: true", + '--ext', + 'IsolationSupport' +]; +\$process = new \lucatume\WPBrowser\Adapters\Symfony\Component\Process\Process( + \$command, + null, + [ + 'WPBROWSER_ISOLATED_RUN' => '1', + 'WPBROWSER_DATA_NAME' => \$dataName, + 'WPBROWSER_DATA_SET' => \$encodedDataSet, + 'WPBROWSER_TEST_FILE' => '{{file}}' + ], +); +\$exitCode = \$process->run(); + +if (\$exitCode !== 0) { + \$output = \$process->getOutput(); + preg_match( + '/WPBROWSER_ISOLATION_RESULT_START(.*)WPBROWSER_ISOLATION_RESULT_END/us', + \$output, + \$matches + ); + \$failure = \$matches[1] ?? null; + + if (\$failure === null) { + \$this->fail("Test failed: {\$process->getErrorOutput()}"); + } + \$serializableThrowable = unserialize(base64_decode(\$failure), ['allowed_classes' => true]); + throw(\$serializableThrowable->getThrowable()); +} +return; +PHP; + + /** + * @throws ExtensionException + */ + public function onSuiteInit(SuiteEvent $event): void + { + if ($this->isMainProcess()) { + $this->monkeyPatchTestCasesToRunInSeparateProcess($event); + return; + } + + $this->monkeyPatchTestMethodsToReplaceAnnotations(); + } + + private function isMainProcess(): bool + { + return empty($_SERVER['WPBROWSER_ISOLATED_RUN']); + } + + /** + * @throws ExtensionException + */ + protected function monkeyPatchTestCasesToRunInSeparateProcess(SuiteEvent $event): void + { + /** @var array{path: string} $settings */ + $settings = $event->getSettings(); + + /** @var Iterator $testFiles */ + $testFiles = new RegexIterator( + new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($settings['path'], FilesystemIterator::CURRENT_AS_PATHNAME) + ), + '/Test\.php$/' + ); + + foreach ($testFiles as $testFile) { + $patchedFile = $this->getPatchedFile($testFile); + + if ($patchedFile === false) { + continue; + } + + MonkeyPatch::redirectFileToFile( + $testFile, + $patchedFile, + false, + self::MAIN_PROCESS_PATCH_CONTEXT + ); + } + } + + /** + * @throws ExtensionException + * @return string|false + */ + private function getPatchedFile(string $testFile) + { + $patchedFile = MonkeyPatch::getReplacementFileName( + $testFile, + self::MAIN_PROCESS_PATCH_CONTEXT + ); + + if (is_file($patchedFile)) { + return $patchedFile; + } + + $fileContents = file_get_contents($testFile); + + if ($fileContents === false) { + throw new ExtensionException($this, "Failed to to open {$testFile} for reading."); + } + + if (strpos($fileContents, '@runTestsInSeparateProcesses') !== false + || strpos($fileContents, '#[RunTestsInSeparateProcesses') !== false + ) { + return $this->patchFileContentsToRunTestsInSeparateProcesses($testFile, $fileContents); + } + + if (strpos($fileContents, '@runInSeparateProcess') !== false + || strpos($fileContents, '#[RunInSeparateProcess') !== false + ) { + return $this->patchFileContentsToRunTestInSeparateProcess($testFile, $fileContents); + } + + return false; + } + + /** + * @throws ExtensionException + * @return string|false + */ + public function patchFileContentsToRunTestsInSeparateProcesses( + string $testFile, + string $fileContents + ) { + return $this->patchFileContentsToInjectSeparateProcessExecution( + '/\\s*?public\\s+function\\s+(?[^(]+)[^{]*?{/um', + $testFile, + $fileContents + ); + } + + /** + * @throws ExtensionException + * @return string|false + */ + private function patchFileContentsToInjectSeparateProcessExecution( + string $pattern, + string $testFile, + string $fileContents + ) { + // Starts with `test` OR contains the `@test` annotation OR contains the `#[Test]` attribute. + $patchedFileContents = preg_replace_callback( + $pattern, + function ($matches) use ($testFile, $fileContents) { + return $this->injectProcessCode($matches, $testFile, $fileContents); + }, + $fileContents + ); + + if ($patchedFileContents === $fileContents) { + return false; + } + + if ($patchedFileContents === null) { + throw new ExtensionException($this, "File contents patching failed for file {$testFile}."); + } + + $patchedFileContents = preg_replace( + [ + '/(^\\s*\\*\\s*)@runInSeparateProcess/um', + '/(^\\s+#\\[)RunInSeparateProcess([^]]*?])/um', + '/(^\\s*\\*\\s*)@runTestsInSeparateProcesses/um', + '/(^\\s+#\\[)RunTestsInSeparateProcesses([^]]*?])/um' + ], + [ + '$1@willRunInSeparateProcess', + '$1WillRunInSeparateProcess$2', + '$1@willRunTestsInSeparateProcesses', + '$1willRunTestsInSeparateProcesses$2' + ], + $patchedFileContents + ); + + $patchedFile = MonkeyPatch::getReplacementFileName( + $testFile, + self::MAIN_PROCESS_PATCH_CONTEXT + ); + + if (!file_put_contents($patchedFile, $patchedFileContents, LOCK_EX)) { + throw new ExtensionException($this, "Failed writing patch file {$patchedFile} for {$testFile}."); + } + + return $patchedFile; + } + + /** + * @param array $matches + */ + private function injectProcessCode(array $matches, string $testFile, string $fileContents): string + { + if (!$this->isTestMethod($matches['fname'], $fileContents)) { + return $matches[0]; + } + $compiledProcessCode = str_replace( + ['{{file}}', '{{name}}'], + [$testFile, $matches['fname']], + $this->processCode + ); + $processCode = preg_replace(["/[\r\n]*/", '~\\s{2,}~'], '', $compiledProcessCode); + return sprintf("%s %s", $matches[0], $processCode); + } + + private function isTestMethod(string $name, string $fileContents): bool + { + if (strncmp($name, 'test', strlen('test')) === 0) { + return true; + } + + $methodDocBlockLines = []; + $methodPos = strpos($fileContents, $name); + + if ($methodPos === false) { + return false; + } + + $input = substr($fileContents, 0, $methodPos); + // Drop the first line as it will be the `public function ...` one. + $lines = explode("\n", $input, -1); + $pattern = '/^(\\s*$|\\s*\\/\\*\\*|\\s*\\*|\\s*\\*\\s*\\/|\\s*#\\[[^]]+])/um'; + for ($i = count($lines) - 1; $i !== 0; $i--) { + $line = $lines [$i]; + if (!preg_match($pattern, $line)) { + break; + } + array_unshift($methodDocBlockLines, $line); + } + $methodDocBlock = implode("\n", $methodDocBlockLines); + return strpos($methodDocBlock, '#[Test]') !== false + || strpos($methodDocBlock, '@test') !== false; + } + + /** + * @throws ExtensionException + * @return string|false + */ + protected function patchFileContentsToRunTestInSeparateProcess( + string $testFile, + string $fileContents + ) { + $pattern = '/' + . '^\\s*' # Start of line and arbitrary number of spaces. + . '(' # Start OR group. + . '\\*\\s*@runInSeparateProcess' # @runInSeparateProcess annotation. + . '|' # OR ... + . '#\\[RunInSeparateProcess]' # [RunInSeparateProcess] attribute. + . ')' # Close OR group. + . '.*?public\\s+function\\s+(?[^(\\s]*?)\\([^{]*?{' # The function declaration until the `{`. + . '/usm'; # Multi-line pattern. + return $this->patchFileContentsToInjectSeparateProcessExecution( + $pattern, + $testFile, + $fileContents + ); + } + + /** + * @throws ExtensionException + */ + protected function monkeyPatchTestMethodsToReplaceAnnotations(): void + { + $testFile = $_SERVER['WPBROWSER_TEST_FILE']; + $fileContents = file_get_contents($testFile); + + if ($fileContents === false) { + throw new ExtensionException($this, "Failed to to open {$testFile} for reading."); + } + + $patchedTestFile = preg_replace( + [ + '/(^\\s*\\*\\s*)@dataProvider/um', + '/(^\\s+#\\[)DataProvider([^]]*?])/um', + '/(^\\s*\\*\\s*)@runInSeparateProcess/um', + '/(^\\s+#\\[)RunInSeparateProcess([^]]*?])/um', + '/(^\\s*\\*\\s*)@runTestsInSeparateProcesses/um', + '/(^\\s+#\\[)RunTestsInSeparateProcesses([^]]*?])/um' + ], + [ + '$1@dataProvidedBy', + '$1DataProvidedBy$2', + '$1@runningInSeparateProcess', + '$1RunningInSeparateProcess$2', + '$1@runningTestsInSeparateProcesses', + '$1RunningTestsInSeparateProcesses$2' + ], + $fileContents + ); + + if ($patchedTestFile === null) { + throw new ExtensionException($this, "File contents patching failed for file {$testFile}."); + } + + MonkeyPatch::redirectFileContents($testFile, $patchedTestFile, false, self::ISOLATED_PROCESS_PATCH_CONTEXT); + } + + public function onTestFail(FailEvent $failEvent): void + { + if ($this->isMainProcess()) { + return; + } + + $this->printSerializedFailure($failEvent); + } + + private function printSerializedFailure(FailEvent $failEvent): void + { + printf("\r\nWPBROWSER_ISOLATION_RESULT_START\n"); + $fail = $failEvent->getFail(); + $serializableThrowable = new SerializableThrowable($fail); + printf(base64_encode(serialize($serializableThrowable))); + printf("WPBROWSER_ISOLATION_RESULT_END\r\n"); + } + + /** + * @throws ReflectionException + */ + public function onTestStart(TestEvent $e): void + { + if ($this->isMainProcess()) { + return; + } + + $this->injectProvidedDataSet($e); + } + + /** + * @throws ReflectionException + */ + private function injectProvidedDataSet(TestEvent $e): void + { + $data = unserialize(base64_decode($_SERVER['WPBROWSER_DATA_SET']), ['allowed_classes' => true]); + + if (!is_array($data)) { + throw new \RuntimeException('Test method data must be an array, but it is ' . gettype($data)); + } + + foreach ($data as &$dataElement) { + if ($dataElement instanceof SerializableClosure) { + $dataElement = $dataElement->getClosure(); + } + } + unset($dataElement); + + $dataName = $_SERVER['WPBROWSER_DATA_NAME']; + + /** @var TestCaseWrapper|TestCase $testCaseWrapper */ + $testCaseWrapper = $e->getTest(); + $testCase = $testCaseWrapper instanceof TestCase ? $testCaseWrapper : $testCaseWrapper->getTestCase(); + Property::setPrivateProperties($testCase, [ + 'data' => $data, + 'dataName' => $dataName + ]); + } +} diff --git a/src/Lib/Generator/AbstractGenerator.php b/src/Lib/Generator/AbstractGenerator.php new file mode 100644 index 000000000..45c758d8a --- /dev/null +++ b/src/Lib/Generator/AbstractGenerator.php @@ -0,0 +1,78 @@ +settings = $settings; + $this->name = $this->removeSuffix($name, 'Test'); + } + + public function produce(): string + { + $ns = $this->getNamespaceHeader($this->settings['namespace'] . '\\' . $this->name); + + return (new Template($this->template))->place('namespace', $ns) + ->place('name', $this->getShortClassName($this->name)) + ->place('tester', $this->getTester()) + ->produce(); + } + + protected function getTester(): string + { + if (isset($this->settings['actor'])) { + $actor = $this->settings['actor']; + } + + try { + /** @var array{actor_suffix: string} $config */ + $config = Configuration::config(); + $propertyName = isset($config['actor_suffix']) ? + lcfirst($config['actor_suffix']) + : ''; + } catch (Exception $exception) { + $propertyName = ''; + } + + if (!isset($actor)) { + return ''; + } + + $testerFrag = <<post->create(); + + add_post_meta( \$post, 'testkey', 'initial_value' ); + + // Become an administrator. + \$this->_setRole( 'administrator' ); + + \$_POST = [ + '_ajax_nonce-add-meta' => wp_create_nonce('add-meta'), + 'post_id' => \$post, + 'key' => 'testkey', + 'value' => 'updated_value', + ]; + + // Make the request. + try { + \$this->_handleAjax( 'add-meta' ); + } catch ( WPAjaxDieContinueException \$e ) { + unset( \$e ); + } + + \$this->assertSame( 'updated_value', get_post_meta( \$post, 'testkey', true ) ); + } +} + +EOF; +} diff --git a/src/Lib/Generator/WPCanonical.php b/src/Lib/Generator/WPCanonical.php new file mode 100644 index 000000000..22f29ed14 --- /dev/null +++ b/src/Lib/Generator/WPCanonical.php @@ -0,0 +1,57 @@ +add_rule( + 'ccr/(.+?)/sort/(asc|desc)', + 'index.php?category_name=\$matches[1]&order=\$matches[2]', + 'top' + ); + \$wp_rewrite->flush_rules(); + + \$this->assertCanonical( + '/ccr/test-category/sort/asc/', + [ + 'url' => '/ccr/test-category/sort/asc/', + 'qv' => [ + 'category_name' => 'test-category', + 'order' => 'asc', + ], + ] + ); + } +} + +EOF; +} diff --git a/src/Lib/Generator/WPRestApi.php b/src/Lib/Generator/WPRestApi.php new file mode 100644 index 000000000..d829ef4ce --- /dev/null +++ b/src/Lib/Generator/WPRestApi.php @@ -0,0 +1,80 @@ +user->create( [ 'role' => 'author' ] ); + + // Create and become editor. + \$editor_id = static::factory()->user->create( [ 'role' => 'editor' ] ); + wp_set_current_user( \$editor_id ); + + // Create 2 posts, one from the editor and one from the author. + \$post_1_id = static::factory()->post->create( [ 'post_author' => \$editor_id ] ); + \$post_2_id = static::factory()->post->create( [ 'post_author' => \$author_id ] ); + + // Get all posts in the database. + \$request = new \WP_REST_Request( 'GET', '/wp/v2/posts' ); + \$request->set_param( 'per_page', 10 ); + \$response = rest_get_server()->dispatch( \$request ); + \$this->assertSame( 200, \$response->get_status() ); + \$this->assertCount( 2, \$response->get_data() ); + + // Exclude editor and author. + \$request = new \WP_REST_Request( 'GET', '/wp/v2/posts' ); + \$request->set_param( 'per_page', 10 ); + \$request->set_param( 'author_exclude', [ \$editor_id, \$author_id ] ); + \$response = rest_get_server()->dispatch( \$request ); + \$this->assertSame( 200, \$response->get_status() ); + \$data = \$response->get_data(); + \$this->assertCount( 0, \$data ); + + // Exclude editor. + \$request = new \WP_REST_Request( 'GET', '/wp/v2/posts' ); + \$request->set_param( 'per_page', 10 ); + \$request->set_param( 'author_exclude', \$editor_id ); + \$response = rest_get_server()->dispatch( \$request ); + \$this->assertSame( 200, \$response->get_status() ); + \$data = \$response->get_data(); + \$this->assertCount( 1, \$data ); + \$this->assertNotEquals( \$editor_id, \$data[0]['author'] ); + + // Invalid 'author_exclude' should error. + \$request = new \WP_REST_Request( 'GET', '/wp/v2/posts' ); + \$request->set_param( 'author_exclude', 'invalid' ); + \$response = rest_get_server()->dispatch( \$request ); + \$this->assertErrorResponse( 'rest_invalid_param', \$response, 400 ); + } +} + +EOF; +} diff --git a/src/Lib/Generator/WPRestController.php b/src/Lib/Generator/WPRestController.php new file mode 100644 index 000000000..012ce9e6d --- /dev/null +++ b/src/Lib/Generator/WPRestController.php @@ -0,0 +1,464 @@ +controller !== null) { + // Do not register the controller more than once. + return; + } + + \$controller = new class extends \WP_REST_Controller { + private array \$data = [ + 'minion' => [ + 'label' => 'Minion', + 'count' => 89, + ], + 'villain' => [ + 'label' => 'Villain', + 'count' => 23, + ], + 'bbeg' => [ + 'label' => 'Big Bad Evil Guy', + 'count' => 1, + ], + ]; + + public function anyone(): bool + { + return true; + } + + public function adminsOnly(): bool + { + return current_user_can('manage_options'); + } + + public function greet(\WP_REST_Request \$request): \WP_REST_Response + { + \$response = new \WP_REST_Response(); + \$name = \$request->get_param('name'); + \$greet = sprintf('Hello %s!', \$name); + \$response->set_data(\$greet); + + return \$response; + } + + public function get_item_schema(): array + { + return [ + '\$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'adversary', + 'type' => 'object', + 'properties' => [ + 'label' => [ + 'description' => 'The adversary display name', + 'type' => 'string', + 'context' => ['view', 'edit'], + ], + 'count' => [ + 'description' => 'The adversary current count', + 'type' => 'integer', + 'context' => ['edit'], + ] + ] + ]; + } + + public function prepare_item_for_response(\$item, \$request): array + { + \$context = \$request->get_param('context'); + \$item['count'] = (int)\$item['count']; + + if (\$context === 'edit') { + return \$item; + } + + return array_diff_key(\$item, ['count' => true]); + } + + public function getAdversary(\WP_REST_Request \$request): \WP_REST_Response + { + \$name = \$request->get_param('name'); + \$item = \$this->data[\$name] ?? null; + + if (\$item === null) { + return new \WP_REST_Response([], 404); + } + + \$item = \$this->prepare_item_for_response(\$item, \$request); + + return new \WP_REST_Response(\$item); + } + + public function getAdversaries(\WP_REST_Request \$request): \WP_REST_Response + { + \$data = \$this->data; + + foreach (\$data as &\$item) { + \$item = \$this->prepare_item_for_response(\$item, \$request); + } + + return new \WP_REST_Response(\$data); + } + + public function upsertAdversary(\WP_REST_Request \$request): \WP_REST_Response + { + \$name = (string)\$request->get_param('name'); + \$update = isset(\$this->data[\$name]); + \$label = (string)\$request->get_param('label') ?: \$this->data[\$name]['label']; + \$count = (string)\$request->get_param('count') ?: \$this->data[\$name]['count']; + + \$item = compact('label', 'count'); + \$this->data[\$name] = \$item; + + return new \WP_REST_Response(\$item, \$update ? 200 : 201); + } + + public function deleteAdversary(\WP_REST_Request \$request): \WP_REST_Response + { + \$name = \$request->get_param('name'); + + \$item = \$this->data[\$name] ?? null; + + if (\$item === null) { + return new \WP_REST_Response('NOT FOUND', 404); + } + + \$this->data = array_diff_key(\$this->data, [\$name => true]); + + return new \WP_REST_Response(\$item, 200); + } + }; + + \$this->controller = \$controller; + + register_rest_route( + 'example', + 'greet/(?P[\w-]+)', + [ + 'methods' => 'GET', + 'callback' => [\$controller, 'greet'], + 'permission_callback' => [\$controller, 'anyone'], + ] + ); + + register_rest_route( + 'example', + 'adversaries', + [ + 'methods' => 'GET', + 'callback' => [\$controller, 'getAdversaries'], + 'permission_callback' => [\$controller, 'anyone'], + 'schema' => [\$controller, 'get_item_schema'], + ], + ); + + register_rest_route( + 'example', + 'adversaries/(?[\w-]*)', + [ + 'methods' => 'GET', + 'callback' => [\$controller, 'getAdversary'], + 'permission_callback' => [\$controller, 'anyone'], + 'schema' => [\$controller, 'get_item_schema'], + ], + ); + + register_rest_route( + 'example', + 'adversary', + [ + 'methods' => ['POST', 'PUT'], + 'callback' => [\$controller, 'upsertAdversary'], + 'permission_callback' => [\$controller, 'adminsOnly'], + 'schema' => [\$controller, 'get_item_schema'], + ], + ); + + register_rest_route( + 'example', + 'adversary', + [ + 'methods' => 'DELETE', + 'callback' => [\$controller, 'deleteAdversary'], + 'permission_callback' => [\$controller, 'adminsOnly'], + 'schema' => [\$controller, 'get_item_schema'], + ], + ); + } + + public function tearDown(): void + { + // Your tear down methods here. + + // Then... + parent::tearDown(); + } + + public function test_register_routes() + { + \$routes = rest_get_server()->get_routes(); + \$this->assertArrayHasKey('/example', \$routes); + \$this->assertCount(1, \$routes['/example']); + \$this->assertArrayHasKey('/example/greet/(?P[\w-]+)', \$routes); + \$this->assertCount(1, \$routes['/example/greet/(?P[\w-]+)']); + \$this->assertArrayHasKey('/example/adversaries', \$routes); + \$this->assertCount(1, \$routes['/example/adversaries']); + } + + public function test_context_param() + { + \$request = new \WP_REST_Request('GET', '/example/adversaries'); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals( + [ + 'minion' => ['label' => 'Minion'], + 'villain' => ['label' => 'Villain'], + 'bbeg' => ['label' => 'Big Bad Evil Guy'], + ], + \$response->data + ); + + \$request = new \WP_REST_Request('GET', '/example/adversaries'); + \$request->set_param('context', 'view'); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals( + [ + 'minion' => ['label' => 'Minion'], + 'villain' => ['label' => 'Villain'], + 'bbeg' => ['label' => 'Big Bad Evil Guy'], + ], + \$response->data + ); + + \$request = new \WP_REST_Request('GET', '/example/adversaries'); + \$request->set_param('context', 'edit'); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals( + [ + 'minion' => [ + 'label' => 'Minion', + 'count' => 89, + ], + 'villain' => [ + 'label' => 'Villain', + 'count' => 23, + ], + 'bbeg' => [ + 'label' => 'Big Bad Evil Guy', + 'count' => 1, + ], + ], + \$response->data + ); + } + + public function test_get_items() + { + \$request = new \WP_REST_Request('GET', '/example/adversaries'); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals( + [ + 'minion' => ['label' => 'Minion'], + 'villain' => ['label' => 'Villain'], + 'bbeg' => ['label' => 'Big Bad Evil Guy'], + ], + \$response->data + ); + } + + public function test_get_item() + { + \$request = new \WP_REST_Request('GET', '/example/adversaries/bbeg'); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(['label' => 'Big Bad Evil Guy'], \$response->data); + + \$request = new \WP_REST_Request('GET', '/example/adversaries/bbeg'); + \$request->set_param('context', 'edit'); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(['label' => 'Big Bad Evil Guy', 'count' => 1], \$response->data); + + \$request = new \WP_REST_Request('GET', '/example/adversaries/troll'); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals([], \$response->data); + \$this->assertEquals(404, \$response->status); + } + + public function test_create_item() + { + \$request = new \WP_REST_Request('POST', '/example/adversary'); + \$request->set_param('name', 'goblin'); + \$request->set_param('label', 'Goblin'); + \$request->set_param('count', 1000); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(401, \$response->status); + + // Become admin. + wp_set_current_user(static::factory()->user->create(['role' => 'administrator'])); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(201, \$response->status); + \$this->assertEquals([ + 'label' => 'Goblin', + 'count' => 1000, + ], \$response->data); + } + + public function test_update_item() + { + // Become admin. + wp_set_current_user(static::factory()->user->create(['role' => 'administrator'])); + + // Create a new item to operate on. + \$request = new \WP_REST_Request('POST', '/example/adversary'); + \$request->set_param('name', 'troll'); + \$request->set_param('label', 'Troll'); + \$request->set_param('count', 3); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(201, \$response->status); + \$this->assertEquals([ + 'label' => 'Troll', + 'count' => 3, + ], \$response->data); + + // Update the item. + \$request = new \WP_REST_Request('PUT', '/example/adversary'); + \$request->set_param('name', 'troll'); + \$request->set_param('count', 2); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(200, \$response->status); + \$this->assertEquals([ + 'label' => 'Troll', + 'count' => 2, + ], \$response->data); + } + + public function test_delete_item() + { + // Become admin. + wp_set_current_user(static::factory()->user->create(['role' => 'administrator'])); + + // Create a new item to operate on. + \$request = new \WP_REST_Request('POST', '/example/adversary'); + \$request->set_param('name', 'ghast'); + \$request->set_param('label', 'Ghast'); + \$request->set_param('count', 5); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(201, \$response->status); + + // Delete a non-existing adversary. + \$request = new \WP_REST_Request('DELETE', '/example/adversary'); + \$request->set_param('name', 'beholder'); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(404, \$response->status); + + // Delete the item. + \$request = new \WP_REST_Request('DELETE', '/example/adversary'); + \$request->set_param('name', 'ghast'); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(200, \$response->status); + \$this->assertEquals([ + 'label' => 'Ghast', + 'count' => 5, + ], \$response->data); + } + + public function test_prepare_item() + { + \$item = [ + 'label' => 'Ghoul', + 'count' => '123', + ]; + + \$request = new \WP_REST_Request('GET', '/example/adversaries/ghoul'); + + \$this->assertEquals( + [ + 'label' => 'Ghoul', + ], + \$this->controller->prepare_item_for_response(\$item, \$request) + ); + + \$request->set_param('context', 'edit'); + + \$this->assertEquals( + [ + 'label' => 'Ghoul', + 'count' => 123 + ], + \$this->controller->prepare_item_for_response(\$item, \$request) + ); + } + + public function test_get_item_schema() + { + \$controller = \$this->controller; + + \$this->assertEquals([ + '\$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'adversary', + 'type' => 'object', + 'properties' => [ + 'label' => [ + 'description' => 'The adversary display name', + 'type' => 'string', + 'context' => ['view', 'edit'], + ], + 'count' => [ + 'description' => 'The adversary current count', + 'type' => 'integer', + 'context' => ['edit'], + ] + ] + ], \$controller->get_item_schema()); + } +} + +EOF; +} diff --git a/src/Lib/Generator/WPRestPostTypeController.php b/src/Lib/Generator/WPRestPostTypeController.php new file mode 100644 index 000000000..7fe0dd977 --- /dev/null +++ b/src/Lib/Generator/WPRestPostTypeController.php @@ -0,0 +1,490 @@ +controller !== null) { + // Do not register the controller more than once. + return; + } + + register_post_type('book'); + + \$controller = new class('book') extends WP_REST_Posts_Controller { + public function anyone(): bool + { + return true; + } + + public function adminsOnly(): bool + { + return current_user_can('manage_options'); + } + + public function get_item_schema() + { + return [ + '\$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'book', + 'type' => 'object', + 'properties' => [ + 'title' => [ + 'description' => 'The book title', + 'type' => 'string', + 'context' => ['view', 'edit'], + ], + 'year' => [ + 'description' => 'The year the book was published', + 'type' => 'string', + 'context' => ['view', 'edit'], + ], + 'copies' => [ + 'description' => 'The number of book copies available', + 'type' => 'integer', + 'context' => ['edit'], + ] + ] + ]; + } + + public function prepare_item_for_response(\$item, \$request) + { + \$post = get_post(\$item); + \$data = [ + 'title' => \$post->post_title, + 'year' => (int)get_post_meta(\$post->ID, 'year', true), + ]; + + if (\$request->get_param('context') === 'edit') { + \$data['copies'] = get_post_meta(\$post->ID, 'copies', true); + } + + return \$data; + } + + + public function getBook(\WP_REST_Request \$request): \WP_REST_Response + { + \$book = get_post(\$request->get_param('id')); + + if (!\$book instanceof \WP_Post || get_post_type(\$book->ID) !== 'book') { + return new \WP_REST_Response(null, 404); + } + + return new \WP_REST_Response(\$this->prepare_item_for_response(\$book, \$request), 200); + } + + public function createBook(\WP_REST_Request \$request): \WP_REST_Response + { + \$prepareRequest = clone \$request; + \$prepareRequest->set_param('context', 'edit'); + \$postarr = [ + 'post_type' => 'book', + 'post_title' => \$request->get_param('title'), + 'meta_input' => [ + 'year' => \$request->get_param('year'), + 'copies' => \$request->get_param('copies') + ] + ]; + + \$inserted = wp_insert_post(\$postarr); + + if (\$inserted instanceof \WP_Error) { + return new \WP_REST_Response(\$inserted->get_error_message(), 500); + } + + return new \WP_REST_Response(\$this->prepare_item_for_response(\$inserted, \$prepareRequest), 201); + } + + public function updateBook(\WP_REST_Request \$request): \WP_REST_Response + { + \$book = get_post(\$request->get_param('id')); + \$prepareRequest = clone \$request; + \$prepareRequest->set_param('context', 'edit'); + + if (!\$book instanceof \WP_Post || get_post_type(\$book->ID) !== 'book') { + return new \WP_REST_Response(null, 404); + } + + \$postarr = [ + 'ID' => \$book->ID, + 'post_title' => \$request->get_param('title') ?: \$book->post_title, + 'meta_input' => [ + 'year' => \$request->get_param('year') ?: get_post_meta(\$book->ID, 'year', true), + 'copies' => \$request->get_param('copies') ?: get_post_meta(\$book->ID, 'copies', true) + ] + ]; + + if ((\$update = wp_update_post(\$postarr)) instanceof \WP_Error) { + return new \WP_REST_Response(\$update->get_error_message(), 500); + } + + return new \WP_REST_Response(\$this->prepare_item_for_response(\$book, \$prepareRequest), 200); + } + + public function deleteBook(\WP_REST_Request \$request): \WP_REST_Response + { + \$book = get_post(\$request->get_param('id')); + \$prepareRequest = clone \$request; + \$prepareRequest->set_param('context', 'edit'); + + if (!\$book instanceof \WP_Post || get_post_type(\$book->ID) !== 'book') { + return new \WP_REST_Response(null, 404); + } + + \$prepared = \$this->prepare_item_for_response(\$book, \$prepareRequest); + + if (!wp_delete_post(\$book->ID)) { + return new \WP_REST_Response(null, 500); + } + + return new \WP_REST_Response(\$prepared, 200); + } + + public function getBooks(\WP_REST_Request \$request): \WP_REST_Response + { + \$posts = get_posts(['post_type' => 'book']); + + \$books = array_map( + fn(\$post) => \$this->prepare_item_for_response(\$post, \$request), + \$posts + ); + + return new \WP_REST_Response(\$books, 200); + } + }; + + register_rest_route('example', '/books', [ + 'methods' => 'GET', + 'callback' => [\$controller, 'getBooks'], + 'permission_callback' => [\$controller, 'anyone'], + 'schema' => \$controller->get_item_schema() + ]); + + register_rest_route('example', '/book/(?P\d+)', [ + 'methods' => 'GET', + 'callback' => [\$controller, 'getBook'], + 'permission_callback' => [\$controller, 'anyone'], + 'schema' => \$controller->get_item_schema() + ]); + + register_rest_route('example', '/book', [ + 'methods' => 'POST', + 'callback' => [\$controller, 'createBook'], + 'permission_callback' => [\$controller, 'adminsOnly'], + 'schema' => \$controller->get_item_schema() + ]); + + register_rest_route('example', '/book/(?P\d+)', [ + 'methods' => 'PUT', + 'callback' => [\$controller, 'updateBook'], + 'permission_callback' => [\$controller, 'adminsOnly'], + 'schema' => \$controller->get_item_schema() + ]); + + register_rest_route('example', '/book/(?P\d+)', [ + 'methods' => 'DELETE', + 'callback' => [\$controller, 'deleteBook'], + 'permission_callback' => [\$controller, 'adminsOnly'], + 'schema' => \$controller->get_item_schema() + ]); + + \$this->controller = \$controller; + } + + public function tearDown(): void + { + // Your tear down methods here. + + // Then... + parent::tearDown(); + } + + public function test_register_routes() + { + \$routes = rest_get_server()->get_routes(); + \$this->assertArrayHasKey('/example', \$routes); + \$this->assertCount(1, \$routes['/example']); + \$this->assertArrayHasKey('/example/books', \$routes); + \$this->assertCount(1, \$routes['/example/books']); + \$this->assertArrayHasKey('/example/book', \$routes); + \$this->assertCount(1, \$routes['/example/book']); + \$this->assertArrayHasKey('/example/book/(?P\d+)', \$routes); + \$this->assertCount(3, \$routes['/example/book/(?P\d+)']); + } + + public function test_context_param() + { + \$book = static::factory()->post->create_and_get([ + 'post_type' => 'book', + 'post_title' => 'Alice in Wonderland', + 'meta_input' => [ + 'year' => 1865, + 'copies' => 13 + ] + ]); + + \$request = new \WP_REST_Request('GET', "/example/book/{\$book->ID}"); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(200, \$response->status); + \$this->assertEquals([ + 'title' => 'Alice in Wonderland', + 'year' => 1865 + ], \$response->data); + + \$request->set_param('context', 'edit'); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(200, \$response->status); + \$this->assertEquals([ + 'title' => 'Alice in Wonderland', + 'year' => 1865, + 'copies' => 13 + ], \$response->data); + } + + public function test_get_items() + { + \$request = new \WP_REST_Request('GET', '/example/books'); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(200, \$response->status); + \$this->assertEquals([], \$response->data); + + \$book1 = static::factory()->post->create_and_get([ + 'post_type' => 'book', + 'post_title' => 'Alice in Wonderland', + 'meta_input' => [ + 'year' => 1865, + 'copies' => 13 + ] + ]); + \$book2 = static::factory()->post->create_and_get([ + 'post_type' => 'book', + 'post_title' => 'Through the Looking-Glass', + 'meta_input' => [ + 'year' => 1871, + 'copies' => 10 + ] + ]); + + \$request = new \WP_REST_Request('GET', "/example/books"); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(200, \$response->status); + \$this->assertEqualSets([ + [ + 'title' => 'Alice in Wonderland', + 'year' => 1865 + ], + [ + 'title' => 'Through the Looking-Glass', + 'year' => 1871 + ] + ], \$response->data); + } + + public function test_get_item() + { + \$book = static::factory()->post->create_and_get([ + 'post_type' => 'book', + 'post_title' => 'Alice in Wonderland', + 'meta_input' => [ + 'year' => 1865, + 'copies' => 13 + ] + ]); + + \$request = new \WP_REST_Request('GET', "/example/book/2389"); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(404, \$response->status); + + \$request = new \WP_REST_Request('GET', "/example/book/{\$book->ID}"); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(200, \$response->status); + \$this->assertEquals([ + 'title' => 'Alice in Wonderland', + 'year' => 1865 + ], \$response->data); + } + + public function test_create_item() + { + \$request = new \WP_REST_Request('POST', "/example/book"); + \$request->set_body_params([ + 'title' => 'Alice in Wonderland', + 'year' => 1865, + 'copies' => 13 + ]); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(401, \$response->status); + + // Become administrator. + wp_set_current_user(static::factory()->user->create(['role' => 'administrator'])); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(201, \$response->status); + \$this->assertEquals([ + 'title' => 'Alice in Wonderland', + 'year' => 1865, + 'copies' => 13 + ], \$response->data); + } + + public function test_update_item() + { + \$book = static::factory()->post->create_and_get([ + 'post_type' => 'book', + 'post_title' => 'Alice in Wonderland', + 'meta_input' => [ + 'year' => 1865, + 'copies' => 13 + ] + ]); + + \$request = new \WP_REST_Request('PUT', "/example/book/{\$book->ID}"); + \$request->set_param('copies', 10); + \$request->set_param('year', 1867); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(401, \$response->status); + + // Become administrator. + wp_set_current_user(static::factory()->user->create(['role' => 'administrator'])); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(200, \$response->status); + \$this->assertEquals([ + 'title' => 'Alice in Wonderland', + 'year' => 1867, + 'copies' => 10 + ], \$response->data); + } + + public function test_delete_item() + { + \$book = static::factory()->post->create_and_get([ + 'post_type' => 'book', + 'post_title' => 'Alice in Wonderland', + 'meta_input' => [ + 'year' => 1865, + 'copies' => 13 + ] + ]); + + \$request = new \WP_REST_Request('DELETE', "/example/book/{\$book->ID}"); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(401, \$response->status); + + // Become administrator. + wp_set_current_user(static::factory()->user->create(['role' => 'administrator'])); + + \$response = rest_get_server()->dispatch(\$request); + + \$this->assertEquals(200, \$response->status); + \$this->assertEquals([ + 'title' => 'Alice in Wonderland', + 'year' => 1865, + 'copies' => 13 + ], \$response->data); + } + + public function test_prepare_item() + { + \$book = static::factory()->post->create_and_get([ + 'post_type' => 'book', + 'post_title' => 'Alice in Wonderland', + 'meta_input' => [ + 'year' => 1865, + 'copies' => 13 + ] + ]); + + \$request = new \WP_REST_Request('GET', "/example/book/{\$book->ID}"); + + \$this->assertEquals([ + 'title' => 'Alice in Wonderland', + 'year' => 1865 + ], \$this->controller->prepare_item_for_response(\$book, \$request)); + + \$request->set_param('context', 'edit'); + + \$this->assertEquals([ + 'title' => 'Alice in Wonderland', + 'year' => 1865, + 'copies' => 13 + ], \$this->controller->prepare_item_for_response(\$book, \$request)); + + // Become administrator. + wp_set_current_user(static::factory()->user->create(['role' => 'administrator'])); + + \$this->assertEquals([ + 'title' => 'Alice in Wonderland', + 'year' => 1865, + 'copies' => 13 + ], \$this->controller->prepare_item_for_response(\$book, \$request)); + } + + public function test_get_item_schema() + { + \$this->assertEquals([ + '\$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'book', + 'type' => 'object', + 'properties' => [ + 'title' => [ + 'description' => 'The book title', + 'type' => 'string', + 'context' => ['view', 'edit'], + ], + 'year' => [ + 'description' => 'The year the book was published', + 'type' => 'string', + 'context' => ['view', 'edit'], + ], + 'copies' => [ + 'description' => 'The number of book copies available', + 'type' => 'integer', + 'context' => ['edit'], + ] + ] + ], \$this->controller->get_item_schema()); + } +} + +EOF; +} diff --git a/src/Lib/Generator/WPUnit.php b/src/Lib/Generator/WPUnit.php index d1a930212..164bd83c1 100644 --- a/src/Lib/Generator/WPUnit.php +++ b/src/Lib/Generator/WPUnit.php @@ -24,6 +24,7 @@ class WPUnit /** * @var string */ +<<<<<<< Updated upstream protected $baseClass; use Classname; use Namespaces; @@ -40,6 +41,8 @@ class WPUnit /** * @var string */ +======= +>>>>>>> Stashed changes protected $template = << 'http://' . \WP_TESTS_DOMAIN . '/wp-sitemap-posts-post-1.xml', + ], + [ + 'loc' => 'http://' . \WP_TESTS_DOMAIN . '/wp-sitemap-posts-page-1.xml', + ], + [ + 'loc' => 'http://' . \WP_TESTS_DOMAIN . '/wp-sitemap-taxonomies-category-1.xml', + ], + [ + 'loc' => 'http://' . \WP_TESTS_DOMAIN . '/wp-sitemap-taxonomies-post_tag-1.xml', + ], + [ + 'loc' => 'http://' . \WP_TESTS_DOMAIN . '/wp-sitemap-users-1.xml', + ], + ]; + + \$renderer = new \WP_Sitemaps_Renderer(); + + \$actual = \$renderer->get_sitemap_index_xml( \$entries ); + \$expected = '' . + '' . + '' . + 'http://' . \WP_TESTS_DOMAIN . '/wp-sitemap-posts-post-1.xml' . + 'http://' . \WP_TESTS_DOMAIN . '/wp-sitemap-posts-page-1.xml' . + 'http://' . \WP_TESTS_DOMAIN . '/wp-sitemap-taxonomies-category-1.xml' . + 'http://' . \WP_TESTS_DOMAIN . '/wp-sitemap-taxonomies-post_tag-1.xml' . + 'http://' . \WP_TESTS_DOMAIN . '/wp-sitemap-users-1.xml' . + ''; + + \$this->assertXMLEquals( \$expected, \$actual, 'Sitemap index markup incorrect.' ); + } +} + +EOF; +} diff --git a/src/Lib/Generator/WPXMLRPC.php b/src/Lib/Generator/WPXMLRPC.php new file mode 100644 index 000000000..b6cb9a592 --- /dev/null +++ b/src/Lib/Generator/WPXMLRPC.php @@ -0,0 +1,78 @@ +modify( '-1 hour' ); + \$datetimeutc = \$datetime->setTimezone( new DateTimeZone( 'UTC' ) ); + + \$this->make_user_by_role( 'administrator' ); + \$post_id = static::factory()->post->create(); + + \$comment_data = [ + 'comment_post_ID' => \$post_id, + 'comment_author' => 'Test commenter', + 'comment_author_url' => 'http://example.com/', + 'comment_author_email' => 'example@example.com', + 'comment_content' => 'Hello, world!', + 'comment_approved' => '1', + ]; + \$comment_id = wp_insert_comment( \$comment_data ); + + \$result = \$this->myxmlrpcserver->wp_editComment( + [ + 1, + 'administrator', + 'administrator', + \$comment_id, + [ + 'date_created_gmt' => new IXR_Date( \$datetimeutc->format( 'Ymd\TH:i:s' ) ), + ], + ] + ); + + \$fetched_comment = get_comment( \$comment_id ); + + \$this->assertTrue( \$result ); + \$this->assertSame( + \$datetime->format( 'Y-m-d H:i:s' ), + \$fetched_comment->comment_date, + 'UTC time into wp_editComment' + ); + } +} + +EOF; +} diff --git a/src/Module/WPLoader/FiltersGroup.php b/src/Module/WPLoader/FiltersGroup.php new file mode 100644 index 000000000..f77bfdf9b --- /dev/null +++ b/src/Module/WPLoader/FiltersGroup.php @@ -0,0 +1,75 @@ +> + */ + protected $filters = []; + /** + * The callback that will be used to remove filters. + * + * @var callable + */ + protected $removeCallback; + + /** + * The callback that will be used to add filters. + * + * @var callable + */ + protected $addCallback; + + /** + * FiltersGroup constructor. + * + * @param array> $filters The list of filters to manage. + * @param callable|null $removeWith The callable that should be used to remove the filters or `null` to use + * the default one. + * @param callable|null $addWith The callable that should be used to add the filters, or `null` to use the + */ + public function __construct(array $filters = [], + callable $removeWith = null, + callable $addWith = null + ) { + /** + * An array detailing each filter callback, priority and arguments. + */ + $this->filters = $filters; + $this->removeCallback = $removeWith ?? 'remove_filter'; + $this->addCallback = $addWith ?? 'add_filter'; + } + + /** + * Removes the filters of the group. + */ + public function remove(): void + { + foreach ($this->filters as $filter) { + $filterWithoutAcceptedArguments = array_slice($filter, 0, 3); + call_user_func_array($this->removeCallback, $filterWithoutAcceptedArguments); + } + } + + /** + * Adds the filters of the group. + */ + public function add(): void + { + foreach ($this->filters as $filter) { + call_user_func_array($this->addCallback, $filter); + } + } +} diff --git a/src/Project/PluginProject.php b/src/Project/PluginProject.php index 0fa33cca4..f7d18440c 100644 --- a/src/Project/PluginProject.php +++ b/src/Project/PluginProject.php @@ -168,7 +168,11 @@ public function test_it_deactivates_activates_correctly(EndToEndTester \$I): voi } EOT +<<<<<<< Updated upstream , +======= +, +>>>>>>> Stashed changes [ 'slug' => Strings::slug($this->getName()) ] @@ -221,7 +225,11 @@ public function test_plugin_active(): void } } EOT +<<<<<<< Updated upstream , +======= +, +>>>>>>> Stashed changes [ 'pluginString' => $this->getActivationString() ] diff --git a/src/Project/ThemeProject.php b/src/Project/ThemeProject.php index 589324530..335a07025 100644 --- a/src/Project/ThemeProject.php +++ b/src/Project/ThemeProject.php @@ -149,7 +149,11 @@ public function test_it_activates_correctly(EndToEndTester \$I): void } EOT +<<<<<<< Updated upstream , +======= +, +>>>>>>> Stashed changes [ 'basename' => Strings::slug($this->basename) ] @@ -202,7 +206,11 @@ public function test_theme_active(): void } } EOT +<<<<<<< Updated upstream , +======= +, +>>>>>>> Stashed changes [ 'stylesheet' => $this->getActivationString() ] diff --git a/src/Template/Wpbrowser.php b/src/Template/Wpbrowser.php index 2f86ee80c..1dfc800b9 100644 --- a/src/Template/Wpbrowser.php +++ b/src/Template/Wpbrowser.php @@ -101,7 +101,11 @@ public function createGlobalConfig(): void 'extensions' => [ 'enabled' => array_merge([RunFailed::class], array_keys($testEnv->extensionsEnabled)), 'config' => $testEnv->extensionsEnabled, +<<<<<<< Updated upstream 'commands' => array_merge([RunOriginal::class, RunAll::class, GenerateWPUnit::class, DbExport::class, DbImport::class], $testEnv->customCommands) +======= + 'commands' => array_merge([RunOriginal::class, RunAll::class, GenerateWPUnit::class, DbExport::class, DbImport::class, MonkeyCachePath::class, MonkeyCacheClear::class], $testEnv->customCommands) +>>>>>>> Stashed changes ] ]; diff --git a/src/TestCase/WPTestCase.php b/src/TestCase/WPTestCase.php index fa4240831..db370497b 100644 --- a/src/TestCase/WPTestCase.php +++ b/src/TestCase/WPTestCase.php @@ -9,11 +9,89 @@ use ReflectionMethod; use WP_UnitTestCase; +<<<<<<< Updated upstream class WPTestCase extends Unit { // Backup, and reset, globals between tests. protected $backupGlobals = true; +======= +/** + * @method static commit_transaction() + * @method static delete_user($user_id) + * @method static factory() + * @method static flush_cache() + * @method static forceTicket($ticket) + * @method static get_called_class() + * @method static set_up_before_class() + * @method static tear_down_after_class() + * @method static text_array_to_dataprovider($input) + * @method static touch($file) + * @method _create_temporary_tables($query) + * @method _drop_temporary_tables($query) + * @method _make_attachment($upload, $parent_post_id = 0) + * @method assertDiscardWhitespace($expected, $actual, $message = '') + * @method assertEqualFields($actual, $fields, $message = '') + * @method assertEqualSets($expected, $actual, $message = '') + * @method assertEqualSetsWithIndex($expected, $actual, $message = '') + * @method assertEqualsIgnoreEOL($expected, $actual, $message = '') + * @method assertIXRError($actual, $message = '') + * @method assertNonEmptyMultidimensionalArray($actual, $message = '') + * @method assertNotIXRError($actual, $message = '') + * @method assertNotWPError($actual, $message = '') + * @method assertQueryTrue($prop) + * @method assertSameIgnoreEOL($expected, $actual, $message = '') + * @method assertSameSets($expected, $actual, $message = '') + * @method assertSameSetsWithIndex($expected, $actual, $message = '') + * @method assertWPError($actual, $message = '') + * @method assert_post_conditions() + * @method clean_up_global_scope() + * @method delete_folders($path) + * @method deprecated_function_run($function_name, $replacement, $version, $message = '') + * @method doing_it_wrong_run($function_name, $message, $version) + * @method expectDeprecated() + * @method expectedDeprecated() + * @method files_in_dir($dir) + * @method get_wp_die_handler($handler) + * @method go_to($url) + * @method knownPluginBug($ticket_id) + * @method knownUTBug($ticket_id) + * @method knownWPBug($ticket_id) + * @method remove_added_uploads() + * @method rmdir($path) + * @method scan_user_uploads() + * @method scandir($dir) + * @method setExpectedDeprecated($deprecated) + * @method setExpectedException($exception, $message = '', $code = NULL) + * @method setExpectedIncorrectUsage($doing_it_wrong) + * @method set_permalink_structure($structure = '') + * @method set_up() + * @method skipOnAutomatedBranches() + * @method skipTestOnTimeout($response) + * @method skipWithMultisite() + * @method skipWithoutMultisite() + * @method start_transaction() + * @method tear_down() + * @method temp_filename() + * @method unlink($file) + * @method unregister_all_meta_keys() + * @method void setCalledClass(string $class) + * @method wp_die_handler($message, $title, $args) + */ +class WPTestCase extends Unit +{ + use WPTestCasePHPUnitMethodsTrait; + /** + * @var string[]|null + */ + private $coreTestCaseProperties = null; + /** + * @var Actor + */ + protected $tester; + // Backup, and reset, globals between tests. + protected $backupGlobals = false; +>>>>>>> Stashed changes // A list of globals that should not be backed up: they are handled by the Core test case. protected $backupGlobalsBlacklist = [ 'wpdb', @@ -48,10 +126,13 @@ class WPTestCase extends Unit '_wpTestsBackupStaticAttributes', '_wpTestsBackupStaticAttributesExcludeList' ]; - // Backup, and reset, static class attributes between tests. +<<<<<<< Updated upstream protected $backupStaticAttributes = true; +======= + protected $backupStaticAttributes = false; +>>>>>>> Stashed changes // A list of static attributes that should not be backed up as they are wired to explode when doing so. protected $backupStaticAttributesBlacklist = [ // WordPress @@ -66,7 +147,6 @@ class WPTestCase extends Unit 'Automattic\WooCommerce\Internal\Admin\FeaturePlugin' => ['instance'], 'Automattic\WooCommerce\RestApi\Server' => ['instance'] ]; - /** * @param array $data */ @@ -85,11 +165,19 @@ public function __construct(?string $name = null, array $data = [], $dataName = } if (property_exists($this, 'backupGlobalsExcludeList')) { +<<<<<<< Updated upstream $backupGlobalsExcludeListReflectionProperty = new \ReflectionProperty($this, 'backupGlobalsExcludeList'); $backupGlobalsExcludeListReflectionProperty->setAccessible(true); } else { // Older versions of PHPUnit. $backupGlobalsExcludeListReflectionProperty = new \ReflectionProperty($this, 'backupGlobalsBlacklist'); +======= + $backupGlobalsExcludeListReflectionProperty = new ReflectionProperty($this, 'backupGlobalsExcludeList'); + $backupGlobalsExcludeListReflectionProperty->setAccessible(true); + } else { + // Older versions of PHPUnit. + $backupGlobalsExcludeListReflectionProperty = new ReflectionProperty($this, 'backupGlobalsBlacklist'); +>>>>>>> Stashed changes $backupGlobalsExcludeListReflectionProperty->setAccessible(true); } $backupGlobalsExcludeListReflectionProperty->setAccessible(true); @@ -142,17 +230,22 @@ public function __construct(?string $name = null, array $data = [], $dataName = parent::__construct($name, $data, $dataName); } - /** * @var array */ protected $additionalGlobalsBackup = []; +<<<<<<< Updated upstream +======= +>>>>>>> Stashed changes /** * @var array */ private static $coreTestCaseMap = []; +<<<<<<< Updated upstream +======= +>>>>>>> Stashed changes private static function getCoreTestCase(): WP_UnitTestCase { if (isset(self::$coreTestCaseMap[static::class])) { @@ -165,6 +258,7 @@ private static function getCoreTestCase(): WP_UnitTestCase return $coreTestCase; } +<<<<<<< Updated upstream public static function setUpBeforeClass(): void { @@ -172,6 +266,8 @@ public static function setUpBeforeClass(): void self::getCoreTestCase()->set_up_before_class(); } +======= +>>>>>>> Stashed changes protected function backupAdditionalGlobals(): void { foreach ([ @@ -183,6 +279,7 @@ protected function backupAdditionalGlobals(): void } } } +<<<<<<< Updated upstream protected function setUp(): void { @@ -191,6 +288,8 @@ protected function setUp(): void $this->backupAdditionalGlobals(); } +======= +>>>>>>> Stashed changes protected function restoreAdditionalGlobals(): void { foreach ($this->additionalGlobalsBackup as $key => $value) { @@ -198,6 +297,7 @@ protected function restoreAdditionalGlobals(): void unset($this->additionalGlobalsBackup[$key]); } } +<<<<<<< Updated upstream protected function tearDown(): void { @@ -213,18 +313,18 @@ public static function tearDownAfterClass(): void parent::tearDownAfterClass(); } +======= +>>>>>>> Stashed changes protected function assertPostConditions(): void { parent::assertPostConditions(); static::assert_post_conditions(); //@phpstan-ignore-line magic __callStatic } - public function __destruct() { // Allow garbage collection of the core test case instance. unset(self::$coreTestCaseMap[static::class]); } - /** * @param array $arguments * @throws ReflectionException @@ -232,12 +332,25 @@ public function __destruct() */ public static function __callStatic(string $name, array $arguments) { +<<<<<<< Updated upstream +======= + switch ($name) { + case '_setUpBeforeClass': + $name = 'setUpBeforeClass'; + break; + case '_tearDownAfterClass': + $name = 'tearDownAfterClass'; + break; + default: + $name = $name; + break; + } +>>>>>>> Stashed changes $coreTestCase = self::getCoreTestCase(); $reflectionMethod = new ReflectionMethod($coreTestCase, $name); $reflectionMethod->setAccessible(true); return $reflectionMethod->invokeArgs(null, $arguments); } - /** * @param array $arguments * @throws ReflectionException @@ -250,11 +363,85 @@ public function __call(string $name, array $arguments) $reflectionMethod->setAccessible(true); return $reflectionMethod->invokeArgs($coreTestCase, $arguments); } +<<<<<<< Updated upstream +======= + /** + * @throws ModuleException If the WPQueries module is not available under any name. + */ +>>>>>>> Stashed changes protected function queries(): WPQueries { /** @var WPQueries $wpQueries */ $wpQueries = $this->getModule(WPQueries::class); return $wpQueries; } +<<<<<<< Updated upstream +======= + private function isCoreTestCaseProperty(string $name): bool + { + if ($this->coreTestCaseProperties === null) { + $this->coreTestCaseProperties = array_map( + static function (ReflectionProperty $p) { + return $p->getName(); + }, + (new \ReflectionClass(self::getCoreTestCase()))->getProperties() + ); + } + + return in_array($name, $this->coreTestCaseProperties, true); + } + /** + * @throws ReflectionException + * @return mixed + */ + public function __get(string $name) + { + if (!$this->isCoreTestCaseProperty($name)) { + return $this->{$name} ?? null; + } + + $coreTestCase = self::getCoreTestCase(); + $reflectionProperty = new ReflectionProperty($coreTestCase, $name); + $reflectionProperty->setAccessible(true); + $value = $reflectionProperty->getValue($coreTestCase); + +// if (is_array($value)) { +// return new ArrayReflectionPropertyAccessor($reflectionProperty, $coreTestCase); +// } + + return $value; + } + /** + * @throws ReflectionException + * @param mixed $value + */ + public function __set(string $name, $value): void + { + if (!$this->isCoreTestCaseProperty($name)) { + // Just set a dynamic property on the test case. + $this->{$name} = $value; + return; + } + + $coreTestCase = self::getCoreTestCase(); + $reflectionProperty = new ReflectionProperty($coreTestCase, $name); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($coreTestCase, $value); + } + /** + * @throws ReflectionException + */ + public function __isset(string $name): bool + { + if (!$this->isCoreTestCaseProperty($name)) { + return isset($this->{$name}); + } + + $coreTestCase = self::getCoreTestCase(); + $reflectionProperty = new ReflectionProperty($coreTestCase, $name); + $reflectionProperty->setAccessible(true); + return $reflectionProperty->isInitialized($coreTestCase); + } +>>>>>>> Stashed changes } diff --git a/src/Utils/ChromedriverInstaller.php b/src/Utils/ChromedriverInstaller.php index 70f9f8822..0533ed250 100644 --- a/src/Utils/ChromedriverInstaller.php +++ b/src/Utils/ChromedriverInstaller.php @@ -16,6 +16,7 @@ class ChromedriverInstaller { +<<<<<<< Updated upstream public const ERR_INVALID_VERSION = 1; public const ERR_INVALID_BINARY = 2; public const ERR_UNSUPPORTED_PLATFORM = 3; @@ -30,6 +31,21 @@ class ChromedriverInstaller public const ERR_MOVE_BINARY = 15; public const ERR_DETECT_PLATFORM = 16; public const ERR_BINARY_CHMOD = 17; +======= + public const ERR_INVALID_BINARY = 1; + public const ERR_UNSUPPORTED_PLATFORM = 2; + public const ERR_REMOVE_EXISTING_ZIP_FILE = 3; + public const ERR_VERSION_NOT_STRING = 4; + public const ERR_INVALID_VERSION_FORMAT = 5; + public const ERR_DESTINATION_NOT_DIR = 6; + public const ERR_FETCH_MILESTONE_DOWNLOADS = 7; + public const ERR_DECODE_MILESTONE_DOWNLOADS = 8; + public const ERR_DOWNLOAD_URL_NOT_FOUND = 9; + public const ERR_REMOVE_EXISTING_BINARY = 10; + public const ERR_DETECT_PLATFORM = 11; + public const ERR_BINARY_CHMOD = 12; + +>>>>>>> Stashed changes /** * @var \Symfony\Component\Console\Output\OutputInterface */ @@ -74,6 +90,210 @@ public function __construct( } /** +<<<<<<< Updated upstream +======= + * @throws RuntimeException + */ + private function detectPlatform(): string + { + // Return one of `linux64`, `mac-arm64`,`mac-x64`, `win32`, `win64`. + $system = php_uname('s'); + $arch = php_uname('m'); + + if ($system === 'Darwin') { + if ($arch === 'arm64') { + return 'mac-arm64'; + } + + return 'mac-x64'; + } + + if ($system === 'Linux') { + return 'linux64'; + } + + if ($system === 'Windows NT') { + if (strpos($arch, '64') !== false) { + return 'win64'; + } + + return 'win32'; + } + + throw new RuntimeException('Failed to detect platform.', self::ERR_DETECT_PLATFORM); + } + + /** + * @return 'linux64'|'mac-x64'|'mac-arm64'|'win32'|'win64' + * + * @throws RuntimeException + * @param mixed $platform + */ + private function checkPlatform($platform): string + { + if (!(is_string($platform) && in_array($platform, [ + 'linux64', + 'mac-arm64', + 'mac-x64', + 'win32', + 'win64' + ]))) { + throw new RuntimeException( + 'Invalid platform, supported platforms are: linux64, mac-arm64, mac-x64, win32, win64.', + self::ERR_UNSUPPORTED_PLATFORM + ); + } + + /** @var 'linux64'|'mac-arm64'|'mac-x64'|'win32'|'win64' $platform */ + return $platform; + } + + /** + * @throws RuntimeException + */ + private function detectBinary(): string + { + switch ($this->platform) { + case 'linux64': + return $this->detectLinuxBinaryPath(); + case 'mac-x64': + case 'mac-arm64': + return '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; + case 'win32': + case 'win64': + return $this->detectWindowsBinaryPath(); + } + } + + private function detectLinuxBinaryPath(): string + { + foreach (['chromium', 'google-chrome'] as $bin) { + $path = exec("which $bin"); + + if (!empty($path)) { + return $path; + } + } + + return '/usr/bin/google-chrome'; + } + + private function detectWindowsBinaryPath(): string + { + $candidates = [ + getenv('ProgramFiles') . '\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe', + getenv('ProgramFiles(x86)') . '\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe', + getenv('LOCALAPPDATA') . '\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe' + ]; + + foreach ($candidates as $candidate) { + if (is_file($candidate)) { + return $candidate; + } + } + + return $candidate; + } + + /** + * @param mixed $binary + */ + private function checkBinary($binary): string + { + // Replace escaped spaces with spaces to check the binary. + if (!(is_string($binary) && is_executable(str_replace('\ ', ' ', $binary)))) { + throw new RuntimeException( + "Invalid Chrome binary: not executable or not existing.\n" . + "Checked paths: " . implode(', ', $this->getBinaryCandidateList()) . "\n", + self::ERR_INVALID_BINARY + ); + } + + return $binary; + } + + /** + * @return string[] + */ + private function getBinaryCandidateList(): array + { + switch ($this->platform) { + case 'linux64': + return ['chromium', 'google-chrome']; + case 'mac-x64': + case 'mac-arm64': + return ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome']; + case 'win32': + case 'win64': + return [ + getenv('ProgramFiles') . '\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe', + getenv('ProgramFiles(x86)') . '\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe', + getenv('LOCALAPPDATA') . '\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe' + ]; + } + } + + /** + * @throws RuntimeException + */ + private function detectVersion(): string + { + switch ($this->platform) { + case 'linux64': + case 'mac-x64': + case 'mac-arm64': + $process = new Process([$this->binary, ' --version']); + break; + case 'win32': + case 'win64': + $process = Process::fromShellCommandline( + 'reg query "HKEY_CURRENT_USER\Software\Google\Chrome\BLBeacon" /v version' + ); + break; + } + + $process->run(); + $chromeVersion = $process->getOutput(); + + if ($chromeVersion === '') { + throw new RuntimeException( + "Could not detect Chrome version from $this->binary", + self::ERR_VERSION_NOT_STRING + ); + } + + $matches = []; + if (!( + preg_match('/\s*\d+\.\d+\.\d+\.\d+\s*/', $chromeVersion, $matches) + && isset($matches[0]) && is_string($matches[0]) + )) { + throw new RuntimeException( + "Could not detect Chrome version from $this->binary", + self::ERR_INVALID_VERSION_FORMAT + ); + } + + return trim($matches[0]); + } + + /** + * @param mixed $version + */ + private function checkVersion($version): string + { + $matches = []; + if (!(is_string($version) && preg_match('/^.*?(?\d+)(\.\d+\.\d+\.\d+)*$/', $version, $matches))) { + throw new RuntimeException( + "Invalid Chrome version: must be in the form X.Y.Z.W.", + self::ERR_INVALID_VERSION_FORMAT + ); + } + + return $matches['major']; + } + + /** +>>>>>>> Stashed changes * @throws JsonException */ public function install(string $dir = null): string @@ -149,6 +369,7 @@ private function detectVersion(): string case 'linux64': case 'mac-x64': case 'mac-arm64': +<<<<<<< Updated upstream $process = new Process([$this->binary, ' --version']); break; case 'win32': @@ -304,6 +525,13 @@ private function checkVersion($version): string } return $matches['major']; +======= + return 'chromedriver'; + case 'win32': + case 'win64': + return 'chromedriver.exe'; + } +>>>>>>> Stashed changes } private function fetchChromedriverVersionUrl(): string diff --git a/src/Utils/Random.php b/src/Utils/Random.php index f8c5079a1..a5a5dca05 100644 --- a/src/Utils/Random.php +++ b/src/Utils/Random.php @@ -15,11 +15,19 @@ class Random * @var string */ private static $saltChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' . +<<<<<<< Updated upstream '!"#$%&()*+,-./:;<=>?@[]^_`{|}~'; /** * @var int */ private static $saltCharsCount = 92; +======= + '!#$%&()*+,-./:;<>?@[]^_`{|}~'; + /** + * @var int + */ + private static $saltCharsCount = 90; +>>>>>>> Stashed changes /** * @var string */ diff --git a/tests/_support/_generated/AcceptanceTesterActions.php b/tests/_support/_generated/AcceptanceTesterActions.php index 49eda7a32..d3fd98844 100644 --- a/tests/_support/_generated/AcceptanceTesterActions.php +++ b/tests/_support/_generated/AcceptanceTesterActions.php @@ -7706,7 +7706,11 @@ public function updateInDatabase(string $table, array $data, array $criteria = [ * @throws Exception If the path is a date string and is not parsable by the `strtotime` function. * @see \lucatume\WPBrowser\Module\WPFilesystem::amInUploadsPath() */ +<<<<<<< Updated upstream public function amInUploadsPath(?string $path = NULL): void { +======= + public function amInUploadsPath($path = NULL): void { +>>>>>>> Stashed changes $this->getScenario()->runStep(new \Codeception\Step\Condition('amInUploadsPath', func_get_args())); } diff --git a/tests/unit/lucatume/WPBrowser/Lib/Generator/GenerationCommandsTest.php b/tests/unit/lucatume/WPBrowser/Lib/Generator/GenerationCommandsTest.php new file mode 100644 index 000000000..89a0606bf --- /dev/null +++ b/tests/unit/lucatume/WPBrowser/Lib/Generator/GenerationCommandsTest.php @@ -0,0 +1,102 @@ +testCaseFile)) { + return; + } + + if (!unlink($this->testCaseFile)) { + throw new \RuntimeException('Cannot remove test case file.'); + } + } + + /** + * @return array + */ + public function commandsProvider(): array + { + return [ + GenerateWPAjax::class => ['GenerateWPAjax', GenerateWPAjax::getCommandName()], + GenerateWPCanonical::class => ['GenerateWPCanonical', GenerateWPCanonical::getCommandName()], + GenerateWPRestApi::class => ['GenerateWPRestApi', GenerateWPRestApi::getCommandName()], + GenerateWPRestController::class => ['GenerateWPRestController', GenerateWPRestController::getCommandName()], + GenerateWPRestPostTypeController::class => [ + 'GenerateWPRestPostTypeController', + GenerateWPRestPostTypeController::getCommandName() + ], + GenerateWPUnit::class => ['GenerateWPUnit', GenerateWPUnit::getCommandName()], + GenerateWPXML::class => ['GenerateWPXML', GenerateWPXML::getCommandName()], + GenerateWPXMLRPC::class => ['GenerateWPXMLRPC', GenerateWPXMLRPC::getCommandName()], + ]; + } + + /** + * @dataProvider commandsProvider + */ + public function test_testcase_generation(string $commandClass, string $commandName): void + { + $codeceptionBin = FS::realpath('vendor/bin/codecept'); + + $suite = static::$suite; + + // Generate the test example. + (new Process([PHP_BINARY, $codeceptionBin, $commandName, $suite, $commandClass]))->mustRun(); + + $testCaseFileRelativePath = "tests/{$suite}/{$commandClass}Test.php"; + $testCaseFile = codecept_root_dir($testCaseFileRelativePath); + + $this->assertFileExists($testCaseFile); + + $this->testCaseFile = $testCaseFile; + + $runProcess = new Process( + [PHP_BINARY, $codeceptionBin, 'codeception:run', $testCaseFileRelativePath] + ); + $runProcess->run(); + $exitCode = $runProcess->getExitCode(); + + if ($exitCode !== 0) { + $this->testCaseFile = null; + } + + $this->assertEquals(0, $exitCode, $this->formatRunProcessOutput($runProcess)); + } + + private function formatRunProcessOutput(Process $runProcess): string + { + return sprintf( + "\nSTDOUT\n---\n%s\nSTDERR\n---\n%s\n", + preg_replace('/^/mu', '> ', $runProcess->getOutput()), + preg_replace('/^/mu', '> ', $runProcess->getErrorOutput()) + ); + } +} diff --git a/tests/unit/lucatume/WPBrowser/Project/ThemeProjectTest.php b/tests/unit/lucatume/WPBrowser/Project/ThemeProjectTest.php index 726005399..4275603c1 100644 --- a/tests/unit/lucatume/WPBrowser/Project/ThemeProjectTest.php +++ b/tests/unit/lucatume/WPBrowser/Project/ThemeProjectTest.php @@ -113,4 +113,58 @@ public function should_build_correctly_on_child_theme_directory(): void $this->assertEquals('Some Theme', $themeProject->getName()); $this->assertEquals('theme', $themeProject->getType()); } +<<<<<<< Updated upstream +======= + + /** + * It should provide information about the failure to activate due to error + * + * @test + */ + public function should_provide_information_about_the_failure_to_activate_due_to_error(): void + { + $wpRootDir = FS::tmpDir('theme_project_'); + $dbName = Random::dbName(); + $db = new MysqlDatabase( + $dbName, + Env::get('WORDPRESS_DB_USER'), + Env::get('WORDPRESS_DB_PASSWORD'), + Env::get('WORDPRESS_DB_HOST') + ); + Installation::scaffold($wpRootDir) + ->configure($db) + ->install( + 'http://localhost:1234', + 'admin', + 'password', + 'admin@example.com', + 'Test' + ); + FS::mkdirp($wpRootDir . '/wp-content/themes/acme-theme', [ + 'style.css' => << ' 'assertFalse($themeProject->activate($wpRootDir, 1234)); + $expected = "Could not activate theme: Error: Current PHP version does not meet minimum requirements for Acme Theme. \n" . + "This might happen because the theme has unmet dependencies; wp-browser configuration will continue, " . + "but you will need to manually activate the theme and update the dump in tests/Support/Data/dump.sql."; + $this->assertEquals( + $expected, + trim(str_replace($wpRootDir, '{{wp_root_dir}}', $output->fetch())) + ); + } +>>>>>>> Stashed changes } diff --git a/tests/unit/lucatume/WPBrowser/Utils/ChromedriverInstallerTest.php b/tests/unit/lucatume/WPBrowser/Utils/ChromedriverInstallerTest.php index 314fd1abb..4bdac6b96 100644 --- a/tests/unit/lucatume/WPBrowser/Utils/ChromedriverInstallerTest.php +++ b/tests/unit/lucatume/WPBrowser/Utils/ChromedriverInstallerTest.php @@ -46,7 +46,62 @@ public function should_throw_if_specified_platform_is_not_supported(): void * * @test */ +<<<<<<< Updated upstream public function should_throw_if_binary_cannot_be_found(): void +======= + public function should_throw_if_specified_binary_cannot_be_found(): void + { + $this->uopzSetFunctionReturn('is_file', function (string $file) { + return strpos($file, 'chrome') === false; + }, true); + $this->expectException(RuntimeException::class); + $this->expectExceptionCode(ChromedriverInstaller::ERR_INVALID_BINARY); + + new ChromedriverInstaller(null, 'win32', '/path/to/chrome.exe'); + } + + /** + * @return string[] + */ + public function platforms_provider(): array + { + return [ + 'win32' => ['win32', 'chrome'], + 'win64' => ['win64', 'chrome'], + 'linux64' => ['linux64', 'chrom'], + 'mac-x64' => ['mac-x64', 'Chrome'], + 'mac-arm64' => ['mac-arm64', 'Chrome'], + ]; + } + + /** + * It should throw if binary cannot be found in default paths for platform + * + * @test + * @dataProvider platforms_provider + */ + public function should_throw_if_binary_cannot_be_found_in_default_paths_for_platform( + string $platform, + string $binNamePattern + ): void { + $isNotAnExecutableFile = function (string $file) use ($binNamePattern) { + return strpos($file, $binNamePattern) === false; + }; + $this->uopzSetFunctionReturn('is_file', $isNotAnExecutableFile, true); + $this->uopzSetFunctionReturn('is_executable', $isNotAnExecutableFile, true); + $this->expectException(RuntimeException::class); + $this->expectExceptionCode(ChromedriverInstaller::ERR_INVALID_BINARY); + + new ChromedriverInstaller(null, $platform); + } + + /** + * It should throw if binary cannot be executed + * + * @test + */ + public function should_throw_if_binary_cannot_be_executed(): void +>>>>>>> Stashed changes { $this->uopzSetFunctionReturn('is_executable', function (string $file): bool { return strpos($file, 'chrome') === false && is_executable($file); @@ -197,8 +252,13 @@ public function should_throw_if_download_url_for_chrome_version_cannot_be_found_ { $this->uopzSetFunctionReturn('file_get_contents', function (string $file) { return strpos($file, 'chrome-for-testing') !== false ? +<<<<<<< Updated upstream '{"milestones":{"116": {"downloads":{"chrome":{},"chromedriver":{}}}}}' : file_get_contents($file); +======= + '{"milestones":{"116": {"downloads":{"chrome":{},"chromedriver":{}}}}}' + : file_get_contents($file); +>>>>>>> Stashed changes }, true); $ci = new ChromedriverInstaller(null, 'linux64', codecept_data_dir('bins/chrome-mock')); diff --git a/tests/wploadersuite/RunInSeparateProcessAnnotationTest.php b/tests/wploadersuite/RunInSeparateProcessAnnotationTest.php new file mode 100644 index 000000000..8c7fbbbf3 --- /dev/null +++ b/tests/wploadersuite/RunInSeparateProcessAnnotationTest.php @@ -0,0 +1,74 @@ + [23], + 'case two' => [89] + ]; + } + + /** + * @dataProvider isolation_data_provider + * @runInSeparateProcess + */ + public function test_isolation_works(int $number): void + { + define('TEST_CONST', $number); + + $this->assertTrue(defined('TEST_CONST')); + $this->assertEquals($number, TEST_CONST); + } + + public function test_state_not_leaked_from_isolated_test(): void + { + $this->assertFalse(defined('TEST_CONST')); + } + + public function closures_provider(): Generator + { + yield 'empty return closure' => [ + function () { + return null; + }, + function ($value) { + return is_null($value); + } + ]; + + yield 'numeric return closure' => [ + function () { + return 23; + }, + function ($value) { + return is_int($value); + } + ]; + + yield 'post returning closure' => [ + function () { + return static::factory()->post->create(); + }, + function ($value) { + return get_post($value) instanceof WP_Post; + } + ]; + } + + /** + * @test + * @runInSeparateProcess + * @dataProvider closures_provider + */ + public function should_correctly_serialize_closures(Closure $createCurrent, Closure $check): void + { + $this->assertTrue($check($createCurrent())); + } +} diff --git a/tests/wploadersuite/RunInSeparateProcessAttributeTest.php b/tests/wploadersuite/RunInSeparateProcessAttributeTest.php new file mode 100644 index 000000000..eaafac449 --- /dev/null +++ b/tests/wploadersuite/RunInSeparateProcessAttributeTest.php @@ -0,0 +1,76 @@ + [23], + 'case two' => [89] + ]; + } + + public function test_isolation_works(int $number): void + { + define('TEST_CONST', $number); + $this->assertTrue(defined('TEST_CONST')); + $this->assertEquals($number, TEST_CONST); + } + + public function test_state_not_leaked_from_isolated_test(): void + { + $this->assertFalse(defined('TEST_CONST')); + } + + /** + * @test + */ + public function it_works(): void + { + $this->assertEquals(23, 23); + } + + /** + * @test + * @runInSeparateProcess + */ + public function it_works_2(): void + { + $this->assertEquals(23, 23); + } + + /** + * @runInSeparateProcess + * @test + */ + public function it_works_3(): void + { + $this->assertEquals(23, 23); + } + + /** + * @test + */ + public function it_works_4(): void + { + $this->assertEquals(23, 23); + } + + public function it_works_5(): void + { + $this->assertEquals(23, 23); + } + + public function it_works_6(int $number): void + { + define('TEST_CONST', $number); + $this->assertTrue(defined('TEST_CONST')); + $this->assertEquals($number, TEST_CONST); + } +} diff --git a/tests/wploadersuite/RunTestsInSeparateProcessesAttributeTest.php b/tests/wploadersuite/RunTestsInSeparateProcessesAttributeTest.php new file mode 100644 index 000000000..92972e2bc --- /dev/null +++ b/tests/wploadersuite/RunTestsInSeparateProcessesAttributeTest.php @@ -0,0 +1,27 @@ + [23], + 'case two' => [89] + ]; + } + public function test_isolation_works(int $number): void + { + define('TEST_CONST', $number); + $this->assertTrue(defined('TEST_CONST')); + $this->assertEquals($number, TEST_CONST); + } + public function test_state_not_leaked_from_isolated_test(): void + { + $this->assertFalse(defined('TEST_CONST')); + } +}