From 11d464fbbc5682dff53bc1252f94e7fd12ec348c Mon Sep 17 00:00:00 2001 From: Sonny Kieu Date: Fri, 2 May 2025 17:11:14 +1000 Subject: [PATCH 1/2] [DEVOPS-429] Updated behat tests. --- .docker/Dockerfile.test | 1 + behat.yml | 132 +++++++++++++----------------- tests/behat/features/home.feature | 20 +++-- 3 files changed, 74 insertions(+), 79 deletions(-) diff --git a/.docker/Dockerfile.test b/.docker/Dockerfile.test index bbf29f93..9bd66ef2 100644 --- a/.docker/Dockerfile.test +++ b/.docker/Dockerfile.test @@ -12,3 +12,4 @@ ENV WEBROOT=web COPY --from=cli /app /app COPY tests /app/tests/ +COPY behat.yml /app/tests/behat.yml diff --git a/behat.yml b/behat.yml index 466dc569..0cfb67bf 100644 --- a/behat.yml +++ b/behat.yml @@ -1,88 +1,74 @@ +# See https://github.com/govcms-tests/tests/blob/3.x-master/behat/behat.yml default: - autoload: [ '%paths.base%/tests/behat/bootstrap' ] + autoload: [ '%paths.base%/bootstrap' ] + gherkin: + # Disable caching during development. It is enabled for profiles below. + cache: ~ + filters: + # Allow skipping tests by tagging them with "@skipped". + tags: "~@skipped" suites: default: + paths: [ '%paths.base%/features' ] contexts: - - Drupal\DrupalExtension\Context\DrupalContext + - FeatureContext + - BehatCliContext - Drupal\DrupalExtension\Context\MinkContext - - Drupal\DrupalExtension\Context\MessageContext - - Drupal\DrupalExtension\Context\DrushContext - Drupal\DrupalExtension\Context\MarkupContext - - IntegratedExperts\BehatScreenshotExtension\Context\ScreenshotContext - - FeatureContext - paths: - features: '%paths.base%/tests/behat/features' + - Drupal\DrupalExtension\Context\MessageContext + - DrevOps\BehatScreenshotExtension\Context\ScreenshotContext filters: - tags: "~@drush&&~@skipped" + tags: "@d10&&~skipped" + extensions: - Behat\MinkExtension: - base_url: http://nginx:8080 - goutte: ~ - browser_name: chrome + Drupal\MinkExtension: + browserkit_http: ~ + base_url: http://nginx:8080 + files_path: '%paths.base%/fixtures' + browser_name: chrome selenium2: - wd_host: http://chrome:4444/wd/hub - capabilities: { "browser": "chrome", "version": "*", "marionette": true } - javascript_session: selenium2 - files_path: '%paths.base%/tests/behat/files' + wd_host: "http://chrome:4444/wd/hub" + capabilities: + browser: "chrome" + version: "*" + marionette: true + extra_capabilities: + chromeOptions: + w3c: false + args: + - --disable-dev-shm-usage + - --disable-extensions + - --disable-gpu + - --no-sandbox + - --headless + javascript_session: selenium2 + # Provides integration with Drupal APIs. Drupal\DrupalExtension: - subcontexts: - paths: - - '%paths.base%/tests/behat/bootstrap' - autoload: 0 - blackbox: ~ - api_driver: "drupal" + blackbox: ~ + api_driver: drupal + drush_driver: drush drupal: - drupal_root: /app + # Behat would run from within "build" dir. + drupal_root: /app/web drush: - root: /app + # Behat would run from within "build" dir. + root: /app/web selectors: + message_selector: '.messages' + error_message_selector: '.messages.error' success_message_selector: '.messages.status' - message_selector: '.messages' - error_message_selector: '.messages.error' - region_map: - sidebar_second: 'section.region-sidebar-second' - navigation: '.region.region-navigation' - highlighted: '.region.region-highlighted' - content: '#content' - footer: '.region.region-footer' - header: '#header' - IntegratedExperts\BehatScreenshotExtension: - dir: '%paths.base%/tests/behat/screenshots' - fail: true - purge: false + warning_message_selector: '.messages.warning' + # Allows to capture HTML and JPG screenshots (based on the driver used). + DrevOps\BehatScreenshotExtension: ~ -# Separate profile for testing using the Drush driver. -drush: - autoload: [ '%paths.base%/tests/behat/bootstrap' ] - suites: - default: - contexts: - - Drupal\DrupalExtension\Context\DrupalContext - - Drupal\DrupalExtension\Context\MinkContext - - Drupal\DrupalExtension\Context\MessageContext - - Drupal\DrupalExtension\Context\DrushContext - - Drupal\DrupalExtension\Context\MarkupContext - - FeatureContext - filters: - tags: "@drush&&~@skipped" - extensions: - Behat\MinkExtension: - goutte: ~ - selenium2: - wd_host: "http://chrome:4444/wd/hub" - javascript_session: selenium2 - files_path: '%paths.base%/files' - Drupal\DrupalExtension: - blackbox: ~ - api_driver: "drush" - drupal: - drupal_root: ../../ - drush: - root: ../../ - region_map: - sidebar_second: 'section.region-sidebar-second' - navigation: '.region.region-navigation' - highlighted: '.region.region-highlighted' - content: '#content' - footer: '.region.region-footer' - header: '#header' +d10: + gherkin: + cache: '/tmp/behat_gherkin_cache' + filters: + tags: "@d10&&~@skipped" + +d9: + gherkin: + cache: '/tmp/behat_gherkin_cache' + filters: + tags: "@d9&&~@skipped" diff --git a/tests/behat/features/home.feature b/tests/behat/features/home.feature index 17960923..626bf3e9 100644 --- a/tests/behat/features/home.feature +++ b/tests/behat/features/home.feature @@ -1,8 +1,16 @@ -Feature: Home Page +@d9 @d10 @smoke @homepage +Feature: Homepage - Ensure the home page is rendering correctly + Ensure that homepage is displayed as expected. - @javascript @smoke - Scenario: Anonymous user visits the homepage - Given I am on the homepage - And save screenshot + @api + Scenario: Anonymous user visits homepage + Given I go to the homepage + And I should be in the "" path + Then I save screenshot + + @api @javascript + Scenario: Anonymous user visits homepage + Given I go to the homepage + And I should be in the "" path + Then I save screenshot From 59f00272c0b0d28d5043e4739fb7da3211581344 Mon Sep 17 00:00:00 2001 From: Sonny Kieu Date: Fri, 2 May 2025 21:14:36 +1000 Subject: [PATCH 2/2] [DEVOPS-429] Updated for PaaS. --- .docker/Dockerfile.test | 1 - behat.yml | 74 --- docker-compose.yml | 3 + scripts/scaffold-init.sh | 2 +- tests/behat/behat.screenshot.yml | 8 - tests/behat/behat.travis.yml | 29 - tests/behat/behat.yml | 62 ++- tests/behat/bootstrap/BehatCliContext.php | 515 ++++++++++++++++++ tests/behat/bootstrap/BehatCliTrait.php | 258 +++++++++ tests/behat/bootstrap/FeatureContext.php | 95 ++-- tests/behat/bootstrap/FeatureContextTrait.php | 199 +++++++ tests/phpunit/bootstrap.php | 86 ++- tests/phpunit/extract.php | 146 +++++ tests/phpunit/phpunit.xml | 71 ++- 14 files changed, 1299 insertions(+), 250 deletions(-) delete mode 100644 behat.yml delete mode 100644 tests/behat/behat.screenshot.yml delete mode 100644 tests/behat/behat.travis.yml create mode 100644 tests/behat/bootstrap/BehatCliContext.php create mode 100644 tests/behat/bootstrap/BehatCliTrait.php create mode 100644 tests/behat/bootstrap/FeatureContextTrait.php create mode 100644 tests/phpunit/extract.php diff --git a/.docker/Dockerfile.test b/.docker/Dockerfile.test index 9bd66ef2..bbf29f93 100644 --- a/.docker/Dockerfile.test +++ b/.docker/Dockerfile.test @@ -12,4 +12,3 @@ ENV WEBROOT=web COPY --from=cli /app /app COPY tests /app/tests/ -COPY behat.yml /app/tests/behat.yml diff --git a/behat.yml b/behat.yml deleted file mode 100644 index 0cfb67bf..00000000 --- a/behat.yml +++ /dev/null @@ -1,74 +0,0 @@ -# See https://github.com/govcms-tests/tests/blob/3.x-master/behat/behat.yml -default: - autoload: [ '%paths.base%/bootstrap' ] - gherkin: - # Disable caching during development. It is enabled for profiles below. - cache: ~ - filters: - # Allow skipping tests by tagging them with "@skipped". - tags: "~@skipped" - suites: - default: - paths: [ '%paths.base%/features' ] - contexts: - - FeatureContext - - BehatCliContext - - Drupal\DrupalExtension\Context\MinkContext - - Drupal\DrupalExtension\Context\MarkupContext - - Drupal\DrupalExtension\Context\MessageContext - - DrevOps\BehatScreenshotExtension\Context\ScreenshotContext - filters: - tags: "@d10&&~skipped" - - extensions: - Drupal\MinkExtension: - browserkit_http: ~ - base_url: http://nginx:8080 - files_path: '%paths.base%/fixtures' - browser_name: chrome - selenium2: - wd_host: "http://chrome:4444/wd/hub" - capabilities: - browser: "chrome" - version: "*" - marionette: true - extra_capabilities: - chromeOptions: - w3c: false - args: - - --disable-dev-shm-usage - - --disable-extensions - - --disable-gpu - - --no-sandbox - - --headless - javascript_session: selenium2 - # Provides integration with Drupal APIs. - Drupal\DrupalExtension: - blackbox: ~ - api_driver: drupal - drush_driver: drush - drupal: - # Behat would run from within "build" dir. - drupal_root: /app/web - drush: - # Behat would run from within "build" dir. - root: /app/web - selectors: - message_selector: '.messages' - error_message_selector: '.messages.error' - success_message_selector: '.messages.status' - warning_message_selector: '.messages.warning' - # Allows to capture HTML and JPG screenshots (based on the driver used). - DrevOps\BehatScreenshotExtension: ~ - -d10: - gherkin: - cache: '/tmp/behat_gherkin_cache' - filters: - tags: "@d10&&~@skipped" - -d9: - gherkin: - cache: '/tmp/behat_gherkin_cache' - filters: - tags: "@d9&&~@skipped" diff --git a/docker-compose.yml b/docker-compose.yml index f7c09df9..36eb843b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,9 @@ x-volumes: &default-volumes x-volumes-paas: &paas-volumes volumes: - .:/app:delegated + - /app/tests + - ./tests/behat:/app/tests/behat:${VOLUME_FLAGS:-delegated} + - ./tests/phpunit/tests:/app/tests/phpunit/tests:${VOLUME_FLAGS:-delegated} x-environment: &default-environment RESTY_RESOLVER: 8.8.8.8 diff --git a/scripts/scaffold-init.sh b/scripts/scaffold-init.sh index 5289138c..29576f95 100755 --- a/scripts/scaffold-init.sh +++ b/scripts/scaffold-init.sh @@ -104,7 +104,7 @@ else rm -r .docker/config rm -r .docker/scripts rm composer.* - rm tests/behat/behat.screenshot.yml tests/behat/behat.travis.yml tests/behat/behat.yml tests/behat/bootstrap/FeatureContext.php tests/phpcs.xml tests/phpunit/bootstrap.php tests/phpunit/phpunit.xml + rm -r tests/behat/bootstrap tests/phpcs.xml tests/phpunit/bootstrap.php tests/phpunit/extract.php tests/phpunit/phpunit.xml rm .gitlab-ci-inputs.yml rm .gitlab-ci.paas.yml fi diff --git a/tests/behat/behat.screenshot.yml b/tests/behat/behat.screenshot.yml deleted file mode 100644 index c84ea04c..00000000 --- a/tests/behat/behat.screenshot.yml +++ /dev/null @@ -1,8 +0,0 @@ -default: - extensions: - Bex\Behat\ScreenshotExtension: - screenshot_taking_mode: failed_steps - image_drivers: - local: - screenshot_directory: '../tests/screenshots' - clear_screenshot_directory: true diff --git a/tests/behat/behat.travis.yml b/tests/behat/behat.travis.yml deleted file mode 100644 index 3415c825..00000000 --- a/tests/behat/behat.travis.yml +++ /dev/null @@ -1,29 +0,0 @@ -default: - autoload: - '': %paths.base%/bootstrap - suites: - default: - contexts: - - '\Drupal\DrupalExtension\Context\DrupalContext' - - '\Drupal\DrupalExtension\Context\MinkContext' - - '\Drupal\DrupalExtension\Context\MessageContext' - - '\Drupal\DrupalExtension\Context\DrushContext' - paths: - features: %paths.base%/features - extensions: - Behat\MinkExtension: - base_url: 'http://127.0.0.1:8080' - goutte: ~ - selenium2: - wd_host: 'http://localhost:4444/wd/hub' - browser: chrome - javascript_session: selenium2 - browser_name: chrome - Drupal\DrupalExtension: - blackbox: ~ - api_driver: 'drupal' - drush: - alias: 'local' - root: 'docroot' - drupal: - drupal_root: '/app/web' diff --git a/tests/behat/behat.yml b/tests/behat/behat.yml index a691f88f..0cfb67bf 100644 --- a/tests/behat/behat.yml +++ b/tests/behat/behat.yml @@ -1,29 +1,46 @@ +# See https://github.com/govcms-tests/tests/blob/3.x-master/behat/behat.yml default: - autoload: [ %paths.base%/bootstrap ] + autoload: [ '%paths.base%/bootstrap' ] gherkin: + # Disable caching during development. It is enabled for profiles below. + cache: ~ filters: - # Allow skipping tests by tagging them with "@skipped" + # Allow skipping tests by tagging them with "@skipped". tags: "~@skipped" suites: default: - paths: [ %paths.base%/features ] + paths: [ '%paths.base%/features' ] contexts: - - FeatureContext - - Drupal\DrupalExtension\Context\DrupalContext - - Drupal\DrupalExtension\Context\MinkContext - - Drupal\DrupalExtension\Context\MarkupContext - - Drupal\DrupalExtension\Context\MessageContext - - IntegratedExperts\BehatScreenshotExtension\Context\ScreenshotContext + - FeatureContext + - BehatCliContext + - Drupal\DrupalExtension\Context\MinkContext + - Drupal\DrupalExtension\Context\MarkupContext + - Drupal\DrupalExtension\Context\MessageContext + - DrevOps\BehatScreenshotExtension\Context\ScreenshotContext + filters: + tags: "@d10&&~skipped" extensions: - Behat\MinkExtension: - goutte: ~ + Drupal\MinkExtension: + browserkit_http: ~ base_url: http://nginx:8080 - files_path: %paths.base%/fixtures + files_path: '%paths.base%/fixtures' browser_name: chrome selenium2: wd_host: "http://chrome:4444/wd/hub" - capabilities: { "browser": "chrome", "version": "*", "marionette": true, "extra_capabilities": { "chromeOptions": { "w3c": false } } } + capabilities: + browser: "chrome" + version: "*" + marionette: true + extra_capabilities: + chromeOptions: + w3c: false + args: + - --disable-dev-shm-usage + - --disable-extensions + - --disable-gpu + - --no-sandbox + - --headless javascript_session: selenium2 # Provides integration with Drupal APIs. Drupal\DrupalExtension: @@ -31,8 +48,10 @@ default: api_driver: drupal drush_driver: drush drupal: + # Behat would run from within "build" dir. drupal_root: /app/web drush: + # Behat would run from within "build" dir. root: /app/web selectors: message_selector: '.messages' @@ -40,15 +59,16 @@ default: success_message_selector: '.messages.status' warning_message_selector: '.messages.warning' # Allows to capture HTML and JPG screenshots (based on the driver used). - IntegratedExperts\BehatScreenshotExtension: - dir: %paths.base%/screenshots - fail: true - purge: false -p0: + DrevOps\BehatScreenshotExtension: ~ + +d10: gherkin: + cache: '/tmp/behat_gherkin_cache' filters: - tags: "@p0&&~@skipped" -p1: + tags: "@d10&&~@skipped" + +d9: gherkin: + cache: '/tmp/behat_gherkin_cache' filters: - tags: "@p1&&~@skipped" + tags: "@d9&&~@skipped" diff --git a/tests/behat/bootstrap/BehatCliContext.php b/tests/behat/bootstrap/BehatCliContext.php new file mode 100644 index 00000000..5cc30d1d --- /dev/null +++ b/tests/behat/bootstrap/BehatCliContext.php @@ -0,0 +1,515 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Behat\Behat\Context\Context; +use Behat\Gherkin\Node\PyStringNode; +use PHPUnit\Framework\Assert; +use Symfony\Component\Process\PhpExecutableFinder; +use Symfony\Component\Process\Process; + +/** + * Behat test suite context. + * + * @author Konstantin Kudryashov + */ +class BehatCliContext implements Context +{ + use BehatCliTrait; + + /** + * @var string + */ + private $phpBin; + /** + * @var Process + */ + private $process; + /** + * @var string + */ + private $workingDir; + /** + * @var string + */ + private $options = '--format-settings=\'{"timer": false}\' --no-interaction'; + /** + * @var array + */ + private $env = array(); + /** + * @var string + */ + private $answerString; + + /** + * Cleans test folders in the temporary directory. + * + * @BeforeSuite + * @AfterSuite + */ + public static function cleanTestFolders() + { + if (is_dir($dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'behat')) { + self::clearDirectory($dir); + } + } + + /** + * Prepares test folders in the temporary directory. + * + * @BeforeScenario + */ + public function prepareTestFolders() + { + $dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'behat' . DIRECTORY_SEPARATOR . + md5(microtime() . rand(0, 10000)); + + mkdir($dir . '/features/bootstrap/i18n', 0777, true); + mkdir($dir . '/junit'); + + $phpFinder = new PhpExecutableFinder(); + if (false === $php = $phpFinder->find()) { + throw new \RuntimeException('Unable to find the PHP executable.'); + } + $this->workingDir = $dir; + $this->phpBin = $php; + } + + /** + * Creates a file with specified name and context in current workdir. + * + * @Given /^(?:there is )?a file named "([^"]*)" with:$/ + * + * @param string $filename name of the file (relative path) + * @param PyStringNode $content PyString string instance + */ + public function aFileNamedWith($filename, PyStringNode $content) + { + $content = strtr((string) $content, array("'''" => '"""')); + $this->createFile($this->workingDir . '/' . $filename, $content); + } + + /** + * Creates a empty file with specified name in current workdir. + * + * @Given /^(?:there is )?a file named "([^"]*)"$/ + * + * @param string $filename name of the file (relative path) + */ + public function aFileNamed($filename) + { + $this->createFile($this->workingDir . '/' . $filename, ''); + } + + /** + * Creates a noop feature context in current workdir. + * + * @Given /^(?:there is )?a some feature context$/ + */ + public function aNoopFeatureContext() + { + $filename = 'features/bootstrap/FeatureContext.php'; + $content = <<<'EOL' +createFile($this->workingDir . '/' . $filename, $content); + } + + /** + * Creates a noop feature in current workdir. + * + * @Given /^(?:there is )?a some feature scenarios/ + */ + public function aNoopFeature() + { + $filename = 'features/bootstrap/FeatureContext.php'; + $content = <<<'EOL' +Feature: + Scenario: + When this scenario executes +EOL; + $this->createFile($this->workingDir . '/' . $filename, $content); + } + + /** + * Moves user to the specified path. + * + * @Given /^I am in the "([^"]*)" path$/ + * + * @param string $path + */ + public function iAmInThePath($path) + { + $this->moveToNewPath($path); + } + + /** + * Checks whether a file at provided path exists. + * + * @Given /^file "([^"]*)" should exist$/ + * + * @param string $path + */ + public function fileShouldExist($path) + { + Assert::assertFileExists($this->workingDir . DIRECTORY_SEPARATOR . $path); + } + + /** + * Sets specified ENV variable + * + * @When /^the "([^"]*)" environment variable is set to "([^"]*)"$/ + */ + public function iSetEnvironmentVariable($name, $value) + { + $this->env[$name] = (string) $value; + } + + /** + * Sets the BEHAT_PARAMS env variable + * + * @When /^"BEHAT_PARAMS" environment variable is set to:$/ + * + * @param PyStringNode $value + */ + public function iSetBehatParamsEnvironmentVariable(PyStringNode $value) + { + $this->env['BEHAT_PARAMS'] = (string) $value; + } + + /** + * Runs behat command with provided parameters + * + * @When /^I run "behat(?: ((?:\"|[^"])*))?"$/ + * + * @param string $argumentsString + */ + public function iRunBehat($argumentsString = '') + { + $argumentsString = strtr($argumentsString, array('\'' => '"')); + + $cmd = sprintf( + '%s %s %s %s', + $this->phpBin, + escapeshellarg(BEHAT_BIN_PATH), + $argumentsString, + strtr($this->options, array('\'' => '"', '"' => '\"')) + ); + + $this->process = Process::fromShellCommandline($cmd); + + // Prepare the process parameters. + $this->process->setTimeout(20); + $this->process->setEnv($this->env); + $this->process->setWorkingDirectory($this->workingDir); + + if (!empty($this->answerString)) { + $this->process->setInput($this->answerString); + } + + // Don't reset the LANG variable on HHVM, because it breaks HHVM itself + if (!defined('HHVM_VERSION')) { + $env = $this->process->getEnv(); + $env['LANG'] = 'en'; // Ensures that the default language is en, whatever the OS locale is. + $this->process->setEnv($env); + } + + $this->process->run(); + } + + /** + * Runs behat command with provided parameters in interactive mode + * + * @When /^I answer "([^"]+)" when running "behat(?: ((?:\"|[^"])*))?"$/ + * + * @param string $answerString + * @param string $argumentsString + */ + public function iRunBehatInteractively($answerString, $argumentsString) + { + $this->env['SHELL_INTERACTIVE'] = true; + + $this->answerString = $answerString; + + $this->options = '--format-settings=\'{"timer": false}\''; + $this->iRunBehat($argumentsString); + } + + /** + * Runs behat command in debug mode + * + * @When /^I run behat in debug mode$/ + */ + public function iRunBehatInDebugMode() + { + $this->options = ''; + $this->iRunBehat('--debug'); + } + + /** + * Checks whether previously ran command passes|fails with provided output. + * + * @Then /^it should (fail|pass) with:$/ + * + * @param string $success "fail" or "pass" + * @param PyStringNode $text PyString text instance + */ + public function itShouldPassWith($success, PyStringNode $text) + { + $this->itShouldFail($success); + $this->theOutputShouldContain($text); + } + + /** + * Checks whether previously runned command passes|failes with no output. + * + * @Then /^it should (fail|pass) with no output$/ + * + * @param string $success "fail" or "pass" + */ + public function itShouldPassWithNoOutput($success) + { + $this->itShouldFail($success); + Assert::assertEmpty($this->getOutput()); + } + + /** + * Checks whether specified file exists and contains specified string. + * + * @Then /^"([^"]*)" file should contain:$/ + * + * @param string $path file path + * @param PyStringNode $text file content + */ + public function fileShouldContain($path, PyStringNode $text) + { + $path = $this->workingDir . '/' . $path; + Assert::assertFileExists($path); + + $fileContent = trim(file_get_contents($path)); + // Normalize the line endings in the output + if ("\n" !== PHP_EOL) { + $fileContent = str_replace(PHP_EOL, "\n", $fileContent); + } + + Assert::assertEquals($this->getExpectedOutput($text), $fileContent); + } + + /** + * Checks whether specified content and structure of the xml is correct without worrying about layout. + * + * @Then /^"([^"]*)" file xml should be like:$/ + * + * @param string $path file path + * @param PyStringNode $text file content + */ + public function fileXmlShouldBeLike($path, PyStringNode $text) + { + $path = $this->workingDir . '/' . $path; + Assert::assertFileExists($path); + + $fileContent = trim(file_get_contents($path)); + + $fileContent = preg_replace('/time="(.*)"/U', 'time="-IGNORE-VALUE-"', $fileContent); + + // The placeholder is necessary because of different separators on Unix and Windows environments + $text = str_replace('-DIRECTORY-SEPARATOR-', DIRECTORY_SEPARATOR, $text); + + $dom = new DOMDocument(); + $dom->loadXML($text); + $dom->formatOutput = true; + + Assert::assertEquals(trim($dom->saveXML(null, LIBXML_NOEMPTYTAG)), $fileContent); + } + + + /** + * Checks whether last command output contains provided string. + * + * @Then the output should contain: + * + * @param PyStringNode $text PyString text instance + */ + public function theOutputShouldContain(PyStringNode $text) + { + Assert::assertStringContainsString($this->getExpectedOutput($text), $this->getOutput()); + } + + private function getExpectedOutput(PyStringNode $expectedText) + { + $text = strtr($expectedText, array( + '\'\'\'' => '"""', + '%%TMP_DIR%%' => sys_get_temp_dir() . DIRECTORY_SEPARATOR, + '%%WORKING_DIR%%' => realpath($this->workingDir . DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, + '%%DS%%' => DIRECTORY_SEPARATOR, + )); + + // windows path fix + if ('/' !== DIRECTORY_SEPARATOR) { + $text = preg_replace_callback( + '/[ "]features\/[^\n "]+/', function ($matches) { + return str_replace('/', DIRECTORY_SEPARATOR, $matches[0]); + }, $text + ); + $text = preg_replace_callback( + '/\features\/[^\<]+/', function ($matches) { + return str_replace('/', DIRECTORY_SEPARATOR, $matches[0]); + }, $text + ); + $text = preg_replace_callback( + '/\+[fd] [^ ]+/', function ($matches) { + return str_replace('/', DIRECTORY_SEPARATOR, $matches[0]); + }, $text + ); + + // error stacktrace + $text = preg_replace_callback( + '/#\d+ [^:]+:/', function ($matches) { + return str_replace('/', DIRECTORY_SEPARATOR, $matches[0]); + }, $text + ); + } + + return $text; + } + + /** + * Checks whether previously ran command failed|passed. + * + * @Then /^it should (fail|pass)$/ + * + * @param string $success "fail" or "pass" + */ + public function itShouldFail($success) + { + if ('fail' === $success) { + if (0 === $this->getExitCode()) { + echo 'Actual output:' . PHP_EOL . PHP_EOL . $this->getOutput(); + } + + Assert::assertNotEquals(0, $this->getExitCode()); + } else { + if (0 !== $this->getExitCode()) { + echo 'Actual output:' . PHP_EOL . PHP_EOL . $this->getOutput(); + } + + Assert::assertEquals(0, $this->getExitCode()); + } + } + + /** + * Checks whether the file is valid according to an XML schema. + * + * @Then /^the file "([^"]+)" should be a valid document according to "([^"]+)"$/ + * + * @param string $xmlFile + * @param string $schemaPath relative to features/bootstrap/schema + */ + public function xmlShouldBeValid($xmlFile, $schemaPath) + { + $dom = new DomDocument(); + $dom->load($this->workingDir . '/' . $xmlFile); + + $dom->schemaValidate(__DIR__ . '/schema/' . $schemaPath); + } + + private function getExitCode() + { + return $this->process->getExitCode(); + } + + private function getOutput() + { + $output = $this->process->getErrorOutput() . $this->process->getOutput(); + + // Normalize the line endings and directory separators in the output + if ("\n" !== PHP_EOL) { + $output = str_replace(PHP_EOL, "\n", $output); + } + + // Remove location of the project + $output = str_replace(realpath(dirname(dirname(__DIR__))).DIRECTORY_SEPARATOR, '', $output); + + // Replace wrong warning message of HHVM + $output = str_replace('Notice: Undefined index: ', 'Notice: Undefined offset: ', $output); + + // replace error messages that changed in PHP8 + $output = str_replace('Warning: Undefined array key','Notice: Undefined offset:', $output); + $output = preg_replace('/Class "([^"]+)" not found/', 'Class \'$1\' not found', $output); + + return trim(preg_replace("/ +$/m", '', $output)); + } + + private function createFile($filename, $content) + { + $path = dirname($filename); + $this->createDirectory($path); + + file_put_contents($filename, $content); + } + + private function createDirectory($path) + { + if (!is_dir($path)) { + mkdir($path, 0777, true); + } + } + + private function moveToNewPath($path) + { + $newWorkingDir = $this->workingDir .'/' . $path; + if (!file_exists($newWorkingDir)) { + mkdir($newWorkingDir, 0777, true); + } + + $this->workingDir = $newWorkingDir; + } + + private static function clearDirectory($path) + { + $files = scandir($path); + array_shift($files); + array_shift($files); + + foreach ($files as $file) { + $file = $path . DIRECTORY_SEPARATOR . $file; + if (is_dir($file)) { + self::clearDirectory($file); + } else { + unlink($file); + } + } + + rmdir($path); + } +} diff --git a/tests/behat/bootstrap/BehatCliTrait.php b/tests/behat/bootstrap/BehatCliTrait.php new file mode 100644 index 00000000..d1c1231a --- /dev/null +++ b/tests/behat/bootstrap/BehatCliTrait.php @@ -0,0 +1,258 @@ +getScenario()->getTags() as $tag) { + if (strpos($tag, 'trait:') === 0) { + $tags = trim(substr($tag, strlen('trait:'))); + $tags = explode(',', $tags); + $tags = array_map(function ($value) { + return trim(str_replace('\\\\', '\\', $value)); + }, $tags); + $traits = array_merge($traits, $tags); + break; + } + } + + $traits = array_filter($traits); + $traits = array_unique($traits); + + // Only create FeatureContext.php if there is at least one '@trait:' tag. + if (empty($traits)) { + return; + } + + $this->behatCliWriteFeatureContextFile($traits); + } + + /** + * @BeforeStep + */ + public function behatCliBeforeStep() { + // Drupal Extension >= ^5 is coupled with Drupal core's DrupalTestBrowser. + // This requires Drupal root to be discoverable when running Behat from a + // random directory using Drupal Finder. + // + // Set environment variables for Drupal Finder. + // This requires Drupal Finder version > 1.2 at commit: + // @see https://github.com/webflo/drupal-finder/commit/2663b117878f4a45ca56df028460350c977f92c0 + $this->iSetEnvironmentVariable('DRUPAL_FINDER_DRUPAL_ROOT', '/app/build/web'); + $this->iSetEnvironmentVariable('DRUPAL_FINDER_COMPOSER_ROOT', '/app/build'); + $this->iSetEnvironmentVariable('DRUPAL_FINDER_VENDOR_DIR', '/app/build/vendor'); + } + + /** + * Create FeatureContext.php file. + * + * @param array $traits + * Optional array of trait classes. + * + * @return string + * Path to written file. + */ + public function behatCliWriteFeatureContextFile(array $traits = []) { + $tokens = [ + '{{USE_DECLARATION}}' => '', + '{{USE_IN_CLASS}}' => '', + ]; + foreach ($traits as $trait) { + $tokens['{{USE_DECLARATION}}'] .= sprintf('use DrevOps\\BehatSteps\\%s;' . PHP_EOL, $trait); + $trait_name__parts = explode('\\', $trait); + $trait_name = end($trait_name__parts); + $tokens['{{USE_IN_CLASS}}'] .= sprintf('use %s;' . PHP_EOL, $trait_name); + } + + $content = <<<'EOL' +log($level, 'test'); + } + +} +EOL; + + $content = strtr($content, $tokens); + $content = preg_replace('/\{\{[^\}]+\}\}/', '', $content); + + $filename = $this->workingDir . DIRECTORY_SEPARATOR . 'features/bootstrap/FeatureContext.php'; + $this->createFile($filename, $content); + + if (static::behatCliIsDebug()) { + static::behatCliPrintFileContents($filename, 'FeatureContext.php'); + } + + return $filename; + } + + /** + * @Given /^scenario steps(?: tagged with "([^"]*)")?:$/ + */ + public function behatCliWriteScenarioSteps(PyStringNode $content, $tags = '') { + $content = strtr((string) $content, ["'''" => '"""']); + + // Make sure that indentation in provided content is accurate. + $content_lines = explode(PHP_EOL, $content); + foreach ($content_lines as $k => $content_line) { + $content_lines[$k] = str_repeat(' ', 4) . trim($content_line); + } + $content = implode(PHP_EOL, $content_lines); + + $tokens = [ + '{{SCENARIO_CONTENT}}' => $content, + '{{ADDITIONAL_TAGS}}' => $tags, + ]; + + $content = <<<'EOL' +Feature: Stub feature'; + @api {{ADDITIONAL_TAGS}} + Scenario: Stub scenario title +{{SCENARIO_CONTENT}} +EOL; + + $content = strtr($content, $tokens); + $content = preg_replace('/\{\{[^\}]+\}\}/', '', $content); + + $filename = $this->workingDir . DIRECTORY_SEPARATOR . 'features/stub.feature'; + $this->createFile($filename, $content); + + if (static::behatCliIsDebug()) { + static::behatCliPrintFileContents($filename, 'Feature Stub'); + } + } + + /** + * @Given some behat configuration + */ + public function behatCliWriteBehatYml() { + $content = <<<'EOL' +default: + suites: + default: + contexts: + - FeatureContext + - Drupal\DrupalExtension\Context\MinkContext + extensions: + Drupal\MinkExtension: + browserkit_http: ~ + selenium2: ~ + base_url: http://nginx:8080 + browser_name: chrome + selenium2: + wd_host: "http://chrome:4444/wd/hub" + capabilities: { "browser": "chrome", "version": "*", "marionette": true, "extra_capabilities": { "chromeOptions": { "w3c": false } } } + javascript_session: selenium2 + + Drupal\DrupalExtension: + api_driver: drupal + drupal: + drupal_root: /app/build/web +EOL; + + $filename = $this->workingDir . DIRECTORY_SEPARATOR . 'behat.yml'; + $this->createFile($filename, $content); + + if (static::behatCliIsDebug()) { + static::behatCliPrintFileContents($filename, 'Behat Config'); + } + } + + /** + * @Then it should fail with an error: + */ + public function behatCliAssertFailWithError(PyStringNode $message) { + $this->itShouldFail('fail'); + Assert::assertStringContainsString(trim((string) $message), $this->getOutput()); + // Enforce \Exception for all assertion exceptions. Non-assertion + // exceptions should be thrown as \RuntimeException. + Assert::assertStringContainsString(' (Exception)', $this->getOutput()); + Assert::assertStringNotContainsString(' (RuntimeException)', $this->getOutput()); + } + + /** + * @Then it should fail with an exception: + */ + public function behatCliAssertFailWithException(PyStringNode $message) { + $this->itShouldFail('fail'); + Assert::assertStringContainsString(trim((string) $message), $this->getOutput()); + // Enforce \RuntimeException for all non-assertion exceptions. Assertion + // exceptions should be thrown as \Exception. + Assert::assertStringContainsString(' (RuntimeException)', $this->getOutput()); + Assert::assertStringNotContainsString(' (Exception)', $this->getOutput()); + } + + /** + * Helper to print file comments. + */ + protected static function behatCliPrintFileContents($filename, $title = '') { + if (!is_readable($filename)) { + throw new \RuntimeException(sprintf('Unable to access file "%s"', $filename)); + } + + $content = file_get_contents($filename); + + print "-------------------- $title START --------------------" . PHP_EOL; + print $filename . PHP_EOL; + print_r($content); + print PHP_EOL; + print "-------------------- $title FINISH --------------------" . PHP_EOL; + } + + /** + * Helper to check if debug mode is enabled. + */ + protected static function behatCliIsDebug() { + // Change to TRUE to see debug messages for this trait. + return FALSE; + } + +} diff --git a/tests/behat/bootstrap/FeatureContext.php b/tests/behat/bootstrap/FeatureContext.php index 5d20a35d..3e212c9a 100644 --- a/tests/behat/bootstrap/FeatureContext.php +++ b/tests/behat/bootstrap/FeatureContext.php @@ -1,41 +1,74 @@ userLog = array(); - } + use FeatureContextTrait; } diff --git a/tests/behat/bootstrap/FeatureContextTrait.php b/tests/behat/bootstrap/FeatureContextTrait.php new file mode 100644 index 00000000..ccfbeb9f --- /dev/null +++ b/tests/behat/bootstrap/FeatureContextTrait.php @@ -0,0 +1,199 @@ +schema()->tableExists('watchdog')) { + $database->truncate('watchdog')->execute(); + } + } + + /** + * @Then user :name does not exist + */ + public function userDoesNotExist($name) { + // We need to check that user was removed from both DB and test variables. + $users = $this->userLoadMultiple(['name' => $name]); + $user = reset($users); + + if ($user) { + throw new \Exception(sprintf('User "%s" exists in DB but should not', $name)); + } + + try { + $this->getUserManager()->getUser($name); + } + catch (\Exception $exception) { + return; + } + + throw new \Exception(sprintf('User "%s" does not exist in DB, but still exists in test variables', $name)); + } + + /** + * @Given set watchdog error level :level + * @Given set watchdog error level :level of type :type + */ + public function setWatchdogErrorDrupal9($level, $type = 'php') { + \Drupal::logger($type)->log($level, 'test'); + } + + /** + * @Given cookie :name exists + */ + public function assertCookieExists($name) { + $cookies = $this->getCookies(); + + if (!isset($cookies[$name])) { + throw new \Exception(sprintf('Cookie "%s" does not exist.', $name)); + } + } + + /** + * Get a list of cookies. + */ + protected function getCookies() { + $cookie_list = []; + + /** @var Behat\Mink\Driver\BrowserKitDriver $driver */ + $driver = $this->getSession()->getDriver(); + if ($driver instanceof Selenium2Driver) { + $cookies = $driver->getWebDriverSession()->getAllCookies(); + foreach ($cookies as $cookie) { + $cookie_list[$cookie['name']] = $cookie['value']; + } + } + else { + $cookie_list = $driver->getClient()->getCookieJar()->allValues($driver->getCurrentUrl()); + } + + return $cookie_list; + } + + /** + * @Given cookie :name does not exist + */ + public function assertCookieNotExists($name) { + $cookies = $this->getCookies(); + + if (isset($cookies[$name])) { + throw new \Exception(sprintf('Cookie "%s" exists but should not.', $name)); + } + } + + /** + * @Given I install a :name module + */ + public function installModule($name) { + /** @var \Drupal\Core\Extension\ModuleHandler $module_handler */ + $module_handler = \Drupal::service('module_handler'); + if ($module_handler->moduleExists($name)) { + return; + } + + /** @var \Drupal\Core\Extension\ModuleInstallerInterface $module_installer */ + $module_installer = \Drupal::service('module_installer'); + + try { + $result = $module_installer->install([$name]); + } + catch (MissingDependencyException $exception) { + throw new \Exception(sprintf('Unable to install a module "%s": %s.', $name, $exception->getMessage())); + } + + if (!$result) { + throw new \Exception(sprintf('Unable to install a module "%s".', $name)); + } + } + + /** + * @Given I uninstall a :name module + */ + public function uninstallModule($name) { + /** @var \Drupal\Core\Extension\ModuleHandler $module_handler */ + $module_handler = \Drupal::service('module_handler'); + if (!$module_handler->moduleExists($name)) { + throw new \RuntimeException(sprintf('Module "%s" does not exist.', $name)); + } + + /** @var \Drupal\Core\Extension\ModuleInstallerInterface $module_installer */ + $module_installer = \Drupal::service('module_installer'); + + $result = $module_installer->uninstall([$name]); + + if (!$result) { + throw new \Exception(sprintf('Unable to uninstall a module "%s".', $name)); + } + } + + /** + * @When I send test email to :email with + * @When I send test email to :email with: + */ + public function sendTestEmail($email, PyStringNode $string) { + \Drupal::service('plugin.manager.mail')->mail( + 'mysite_core', + 'test_email', + $email, + \Drupal::languageManager()->getDefaultLanguage(), + ['body' => strval($string)], + FALSE + ); + } + + /** + * @Then :file_name file object exists + */ + public function fileObjectExist($file_name) { + $file_name = basename($file_name); + $fids = $this->fileLoadMultiple(['filename' => $file_name]); + if (empty($fids)) { + throw new \Exception(sprintf('"%s" file does not exist in DB, but it should', $file_name)); + } + + $fid = reset($fids); + $file = File::load($fid); + + if ($file_name !== $file->label()) { + throw new \Exception(sprintf('"%s" file does not exist in DB, but it should', $file_name)); + } + } + + /** + * @Then no :file_name file object exists + */ + public function noFileObjectExist($file_name) { + $file_name = basename($file_name); + $fids = $this->fileLoadMultiple(['filename' => $file_name]); + if ($fids) { + throw new \Exception(sprintf('"%s" file does exist in DB, but it should not', $file_name)); + } + } + +} diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php index d299a5b8..8284f3a7 100644 --- a/tests/phpunit/bootstrap.php +++ b/tests/phpunit/bootstrap.php @@ -2,29 +2,28 @@ /** * @file - * Autoloader for Drupal PHPUnit testing. + * Autoloader for GovCMS Drupal PHPUnit testing. * - * @see phpunit.xml.dist + * @see phpunit.xml */ -use Drupal\Component\Assertion\Handle; -use PHPUnit\Runner\Version; +use Drupal\TestTools\PhpUnitCompatibility\ClassWriter; /** * Finds all valid extension directories recursively within a given directory. * * @param string $scan_directory * The directory that should be recursively scanned. + * * @return array * An associative array of extension directories found within the scanned * directory, keyed by extension name. */ -function drupal_phpunit_find_extension_directories($scan_directory) -{ +function drupal_phpunit_find_extension_directories($scan_directory) { $extensions = []; $dirs = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($scan_directory, \RecursiveDirectoryIterator::FOLLOW_SYMLINKS)); foreach ($dirs as $dir) { - if (strpos($dir->getPathname(), '.info.yml') !== FALSE) { + if (str_contains($dir->getPathname(), '.info.yml')) { // Cut off ".info.yml" from the filename for use as the extension name. We // use getRealPath() so that we can scan extensions represented by // directory aliases. @@ -44,14 +43,14 @@ function drupal_phpunit_find_extension_directories($scan_directory) * @return array * An array of directories under which contributed extensions may exist. */ -function drupal_phpunit_contrib_extension_directory_roots($root = NULL) -{ +function drupal_phpunit_contrib_extension_directory_roots($root = NULL) { if ($root === NULL) { $root = '/app/web'; } $paths = [ $root . '/core/modules', $root . '/core/profiles', + $root . '/core/themes', $root . '/modules', $root . '/profiles', $root . '/themes', @@ -68,7 +67,7 @@ function drupal_phpunit_contrib_extension_directory_roots($root = NULL) $paths[] = is_dir("$path/profiles") ? realpath("$path/profiles") : NULL; $paths[] = is_dir("$path/themes") ? realpath("$path/themes") : NULL; } - return array_filter($paths, 'file_exists'); + return array_filter($paths); } /** @@ -80,9 +79,8 @@ function drupal_phpunit_contrib_extension_directory_roots($root = NULL) * @return array * An associative array of extension directories, keyed by their namespace. */ -function drupal_phpunit_get_extension_namespaces($dirs) -{ - $suite_names = ['Unit', 'Kernel', 'Functional', 'FunctionalJavascript']; +function drupal_phpunit_get_extension_namespaces($dirs) { + $suite_names = ['Unit', 'Kernel', 'Functional', 'Build', 'FunctionalJavascript']; $namespaces = []; foreach ($dirs as $extension => $dir) { if (is_dir($dir . '/src')) { @@ -98,7 +96,7 @@ function drupal_phpunit_get_extension_namespaces($dirs) $namespaces['Drupal\\Tests\\' . $extension . '\\' . $suite_name . '\\'][] = $suite_dir; } } - // Extensions can have a \Drupal\extension\Traits namespace for + // Extensions can have a \Drupal\Tests\extension\Traits namespace for // cross-suite trait code. $trait_dir = $test_dir . '/Traits'; if (is_dir($trait_dir)) { @@ -121,22 +119,33 @@ function drupal_phpunit_get_extension_namespaces($dirs) * Populate class loader with additional namespaces for tests. * * We run this in a function to avoid setting the class loader to a global - * that can change. This change can cause unpredictable false positives for - * phpunit's global state change watcher. The class loader can be retrieved from + * that can change. This change can cause unpredictable false positives for the + * PHPUnit global state change watcher. The class loader can be retrieved from * composer at any time by requiring autoload.php. */ -function drupal_phpunit_populate_class_loader() -{ +function drupal_phpunit_populate_class_loader() { /** @var \Composer\Autoload\ClassLoader $loader */ $loader = require '/app/web/autoload.php'; // Start with classes in known locations. + $loader->add('Drupal\\BuildTests', '/app/web/core/tests'); $loader->add('Drupal\\Tests', '/app/web/core/tests'); $loader->add('Drupal\\TestSite', '/app/web/core/tests'); $loader->add('Drupal\\KernelTests', '/app/web/core/tests'); $loader->add('Drupal\\FunctionalTests', '/app/web/core/tests'); $loader->add('Drupal\\FunctionalJavascriptTests', '/app/web/core/tests'); + $loader->add('Drupal\\TestTools', '/app/web/core/tests'); + + // Add multiple paths for GovCMS\Tests + $loader->addPsr4('GovCMS\\Tests\\', [ + '/app/tests/phpunit/tests', + ]); + + // Add a separate path for GovCMS\Tests\Integration + $loader->addPsr4('GovCMS\\Tests\\Integration\\', [ + '/app/tests/phpunit/integration' + ]); if (!isset($GLOBALS['namespaces'])) { // Scan for arbitrary extension namespaces from core and contrib. @@ -151,17 +160,13 @@ function drupal_phpunit_populate_class_loader() } return $loader; -}; +} // Do class loader population. -drupal_phpunit_populate_class_loader(); +$loader = drupal_phpunit_populate_class_loader(); +class_alias('\Drupal\Tests\DocumentElement', '\Behat\Mink\Element\DocumentElement', TRUE); -// Ensure we have the correct PHPUnit version for the version of PHP. -if (class_exists('\PHPUnit_Runner_Version')) { - $phpunit_version = \PHPUnit_Runner_Version::id(); -} else { - $phpunit_version = Version::id(); -} +ClassWriter::mutateTestBase($loader); // Set sane locale settings, to ensure consistent string, dates, times and // numbers handling. @@ -179,24 +184,13 @@ function drupal_phpunit_populate_class_loader() // reduce the fragility of the testing system in general. date_default_timezone_set('Australia/Sydney'); -// Runtime assertions. PHPUnit follows the php.ini assert.active setting for -// runtime assertions. By default this setting is on. Ensure exceptions are -// thrown if an assert fails, but this call does not turn runtime assertions on -// if they weren't on already. -Handle::register(); - -// PHPUnit 4 to PHPUnit 6 bridge. Tests written for PHPUnit 4 need to work on -// PHPUnit 6 with a minimum of fuss. -if (version_compare($phpunit_version, '6.1', '>=')) { - class_alias('\PHPUnit\Framework\AssertionFailedError', '\PHPUnit_Framework_AssertionFailedError'); - class_alias('\PHPUnit\Framework\Constraint\Count', '\PHPUnit_Framework_Constraint_Count'); - class_alias('\PHPUnit\Framework\Error\Error', '\PHPUnit_Framework_Error'); - class_alias('\PHPUnit\Framework\Error\Warning', '\PHPUnit_Framework_Error_Warning'); - class_alias('\PHPUnit\Framework\ExpectationFailedException', '\PHPUnit_Framework_ExpectationFailedException'); - class_alias('\PHPUnit\Framework\Exception', '\PHPUnit_Framework_Exception'); - class_alias('\PHPUnit\Framework\MockObject\Matcher\InvokedRecorder', '\PHPUnit_Framework_MockObject_Matcher_InvokedRecorder'); - class_alias('\PHPUnit\Framework\SkippedTestError', '\PHPUnit_Framework_SkippedTestError'); - class_alias('\PHPUnit\Framework\TestCase', '\PHPUnit_Framework_TestCase'); - class_alias('\PHPUnit\Util\Test', '\PHPUnit_Util_Test'); - class_alias('\PHPUnit\Util\Xml', '\PHPUnit_Util_XML'); +// Ensure ignored deprecation patterns listed in .deprecation-ignore.txt are +// considered in testing. +if (getenv('SYMFONY_DEPRECATIONS_HELPER') === FALSE) { + $deprecation_ignore_filename = realpath("/app/web/core/.deprecation-ignore.txt"); + putenv("SYMFONY_DEPRECATIONS_HELPER=ignoreFile=$deprecation_ignore_filename"); } + +// Drupal expects to be run from its root directory. This ensures all test types +// are consistent. +chdir('/app/web'); diff --git a/tests/phpunit/extract.php b/tests/phpunit/extract.php new file mode 100644 index 00000000..1e58f3b6 --- /dev/null +++ b/tests/phpunit/extract.php @@ -0,0 +1,146 @@ + get_class_phpdoc($file) ); + $func_docs = array_merge($class_doc, $func_docs); + $suite[basename($file)] = $func_docs; + } + return $suite; +} + +function list_files_in_dir(string $dir): array { + $result = []; + // Ensure the directory path ends with a slash + if (substr($dir, -1) !== DIRECTORY_SEPARATOR) { + $dir .= DIRECTORY_SEPARATOR; + } + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ); + foreach ($iterator as $fileinfo) { + if ($fileinfo->isFile()) { + $filename = $fileinfo->getRealPath(); + // Only include files ending in Test.php + if (!preg_match('/.*Test.php/', $filename)) { + continue; + } + $result[] = $filename; + } + } + return $result; +} + +function get_functions_with_phpdoc(string $file): array { + // Read the file content + $code = file_get_contents($file); + // Get all tokens from the code + $tokens = token_get_all($code); + + $functions = []; + $phpdoc = ''; + $captureNextString = false; + + // Loop through each token + foreach ($tokens as $token) { + if (is_array($token)) { + if ($token[0] == T_DOC_COMMENT) { + // Capture PHPDoc comment + $phpdoc = $token[1]; + } elseif ($token[0] == T_FUNCTION) { + // When we see a T_FUNCTION, the next string token is the function name + $captureNextString = true; + } elseif ($captureNextString && $token[0] == T_STRING) { + // Ignore functions not starting with 'test' + if (!preg_match('/test.*/', $token[1])) { + continue; + } + // Capture the function name and associate it with the last PHPDoc comment + $functions[$token[1]] = clean_phpdoc($phpdoc); + $captureNextString = false; + $phpdoc = ''; // Reset PHPDoc after associating it with a function + } + } + } + + return $functions; +} + +function get_class_phpdoc(string $file): string { + $code = file_get_contents($file); + $tokens = token_get_all($code); + + $phpdoc = ''; + $captureNextString = false; + + $class = []; + + foreach ($tokens as $token) { + if (!is_array($token)) { + continue; + } + + if ($token[0] == T_DOC_COMMENT) { + $phpdoc = $token[1]; + } elseif ($token[0] == T_CLASS) { + $captureNextString = true; + } elseif ($captureNextString && $token[0] == T_STRING) { + return clean_phpdoc($phpdoc); + + $phpdoc = ''; + $captureNextString = false; + } + } + return 'No class PHPDoc found.'; +} + +function clean_phpdoc(string $docstring): string { + $string = substr($docstring, 3, -2); // Strips opening/closing tags + $trimmed = trim(preg_replace('/^\s*\*\s*?(\S|$)/m', '\1', $string)); + $clean = strstr($trimmed, PHP_EOL, true); + return $clean ? $clean : $trimmed; +} + +// Usage example: +$docs = get_docs_in_directory($argv[1]); +echo getcwd(); +print_r($docs); +$csv_string = convert_doc_array_to_csv($docs); +echo $csv_string; +create_csv_file($csv_string); + + +function convert_doc_array_to_csv(array $docs): string { + $lines = array(); + $lines[] = 'Test, Case, About'; + foreach ($docs as $test_name => $test_array) { + $lines[] = convert_test_array($test_name, $test_array); + } + return implode(PHP_EOL, $lines) . PHP_EOL; +} + +function convert_test_array(string $test_name, array $test_array): string { + $csv_lines = array(); + foreach ($test_array as $function => $phpdoc) { + $csv_lines[] = $test_name . ', ' . $function . ', ' . $phpdoc; + } + return implode(PHP_EOL, $csv_lines); +} + +function create_csv_file(string $csv_string): void { + $rows = explode(PHP_EOL, $csv_string); + $filename = 'phpunit/docs/testcases.csv'; + $fd = fopen($filename, 'w'); + if ($fd === false) { + die(); + } + foreach ($rows as $row) { + $fields = explode(', ', $row); + fputcsv($fd, $fields); + } + fclose($fd); +} diff --git a/tests/phpunit/phpunit.xml b/tests/phpunit/phpunit.xml index a9f3fad4..db86c050 100644 --- a/tests/phpunit/phpunit.xml +++ b/tests/phpunit/phpunit.xml @@ -1,39 +1,21 @@ - - - - - + beStrictAboutChangesToGlobalState="true" + failOnWarning="true" + cacheResult="false" + defaultTestSuite="govcms" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"> - - - - - - - - - - - + @@ -48,31 +30,42 @@ /app/web/core/tests/TestSuites/FunctionalJavascriptTestSuite.php + + /app/web/core/tests/TestSuites/BuildTestSuite.php + /app/web/themes + + /app/tests/phpunit/integration + /app/tests/phpunit/tests - - + - - - + + + /app/web/core/includes /app/web/core/lib /app/web/core/modules /app/web/modules /app/web/sites - - - ./ - ./ - - - + + + /app/web/core/modules/*/src/Tests + /app/web/core/modules/*/tests + /app/web/modules/*/src/Tests + /app/web/modules/*/tests + /app/web/modules/*/*/src/Tests + /app/web/modules/*/*/tests + /app/web/core/lib/** + /app/web/core/modules/** + /app/web/modules/** + +