From 0da15338b2d917c58bbfdb4214241c1e7361885a Mon Sep 17 00:00:00 2001 From: Edward Surov Date: Tue, 19 Oct 2021 20:33:30 +0300 Subject: [PATCH 01/10] Version 2 prototype --- .github/workflows/build.yml | 100 ++- .gitignore | 1 + README.md | 26 +- codeception.yml | 12 +- composer.json | 52 +- phpcs.xml.dist | 15 + phpunit.xml => phpunit.xml.dist | 2 +- psalm.xml.dist | 17 + src/AllureCodeception.php | 279 +++++++++ src/Internal/ArgumentAsString.php | 92 +++ src/Internal/CeptInfoBuilder.php | 28 + src/Internal/CeptProvider.php | 204 +++++++ src/Internal/CestInfoBuilder.php | 53 ++ src/Internal/CestProvider.php | 116 ++++ src/Internal/DefaultThreadDetector.php | 29 + src/Internal/GherkinInfoBuilder.php | 35 ++ src/Internal/GherkinProvider.php | 73 +++ src/Internal/StepStartInfo.php | 27 + src/Internal/SuiteInfo.php | 34 ++ src/Internal/SuiteProvider.php | 84 +++ src/Internal/TestInfo.php | 55 ++ src/Internal/TestInfoBuilderInterface.php | 11 + src/Internal/TestInfoProvider.php | 69 +++ src/Internal/TestLifecycle.php | 385 ++++++++++++ src/Internal/TestLifecycleInterface.php | 50 ++ src/Internal/TestStartInfo.php | 25 + src/Internal/UnitInfoBuilder.php | 44 ++ src/Internal/UnitProvider.php | 126 ++++ src/Internal/UnknownInfoBuilder.php | 24 + src/Setup/ThreadDetectorInterface.php | 13 + src/StatusDetector.php | 69 +++ .../Allure/Codeception/AllureCodeception.php | 567 ------------------ test/codeception/AnnotationTest.php | 68 --- .../codeception/_support/AcceptanceTester.php | 98 +++ .../codeception/_support/FunctionalTester.php | 26 + test/codeception/acceptance.suite.yml | 2 + test/codeception/acceptance/sample.feature | 32 + test/codeception/functional.suite.yml | 2 + .../functional/BasicScenarioCept.php | 12 + .../codeception/functional/ClassTitleCest.php | 18 + .../functional/CustomizedScenarioCept.php | 19 + .../functional/NoClassTitleCest.php | 30 + .../ScenarioWithLegacyAnnotationsCept.php | 20 + .../{_support/.gitkeep => unit.suite.yml} | 0 test/codeception/unit/AnnotationTest.php | 50 ++ test/codeception/unit/DataProviderTest.php | 38 ++ test/codeception/{ => unit}/StepsTest.php | 11 +- test/report/ReportTest.php | 452 ++++++++------ 48 files changed, 2669 insertions(+), 926 deletions(-) create mode 100644 phpcs.xml.dist rename phpunit.xml => phpunit.xml.dist (95%) create mode 100644 psalm.xml.dist create mode 100644 src/AllureCodeception.php create mode 100644 src/Internal/ArgumentAsString.php create mode 100644 src/Internal/CeptInfoBuilder.php create mode 100644 src/Internal/CeptProvider.php create mode 100644 src/Internal/CestInfoBuilder.php create mode 100644 src/Internal/CestProvider.php create mode 100644 src/Internal/DefaultThreadDetector.php create mode 100644 src/Internal/GherkinInfoBuilder.php create mode 100644 src/Internal/GherkinProvider.php create mode 100644 src/Internal/StepStartInfo.php create mode 100644 src/Internal/SuiteInfo.php create mode 100644 src/Internal/SuiteProvider.php create mode 100644 src/Internal/TestInfo.php create mode 100644 src/Internal/TestInfoBuilderInterface.php create mode 100644 src/Internal/TestInfoProvider.php create mode 100644 src/Internal/TestLifecycle.php create mode 100644 src/Internal/TestLifecycleInterface.php create mode 100644 src/Internal/TestStartInfo.php create mode 100644 src/Internal/UnitInfoBuilder.php create mode 100644 src/Internal/UnitProvider.php create mode 100644 src/Internal/UnknownInfoBuilder.php create mode 100644 src/Setup/ThreadDetectorInterface.php create mode 100644 src/StatusDetector.php delete mode 100644 src/Yandex/Allure/Codeception/AllureCodeception.php delete mode 100644 test/codeception/AnnotationTest.php create mode 100644 test/codeception/_support/AcceptanceTester.php create mode 100644 test/codeception/_support/FunctionalTester.php create mode 100644 test/codeception/acceptance.suite.yml create mode 100644 test/codeception/acceptance/sample.feature create mode 100644 test/codeception/functional.suite.yml create mode 100644 test/codeception/functional/BasicScenarioCept.php create mode 100644 test/codeception/functional/ClassTitleCest.php create mode 100644 test/codeception/functional/CustomizedScenarioCept.php create mode 100644 test/codeception/functional/NoClassTitleCest.php create mode 100644 test/codeception/functional/ScenarioWithLegacyAnnotationsCept.php rename test/codeception/{_support/.gitkeep => unit.suite.yml} (100%) create mode 100644 test/codeception/unit/AnnotationTest.php create mode 100644 test/codeception/unit/DataProviderTest.php rename test/codeception/{ => unit}/StepsTest.php (89%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 40b2fe9..4af2d46 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,68 +10,48 @@ on: - 'hotfix-*' jobs: - build71: - runs-on: ubuntu-latest + tests: + name: PHP ${{ matrix.php-version }} on ${{ matrix.os }} (${{ matrix.composer-options }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + php-version: + - "8.0" + - "8.1" + os: + - ubuntu-latest + - windows-latest + - macOS-latest + composer-options: + - "" + - "--prefer-lowest" steps: - - uses: actions/checkout@v2.3.4 - - uses: shivammathur/setup-php@2.14.0 - with: - php-version: '7.1.3' - - name: Install - run: composer install - - name: Install - run: composer validate - - name: Test - run: composer test - build72: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2.3.4 - - uses: shivammathur/setup-php@2.14.0 - with: - php-version: '7.2' - - name: Install - run: composer install - - name: Install - run: composer validate - - name: Test - run: composer test - build73: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2.3.4 - - uses: shivammathur/setup-php@2.14.0 - with: - php-version: '7.3' - - name: Install - run: composer install - - name: Install + - name: Checkout + uses: actions/checkout@v2.3.4 + + - name: Validate composer.json and composer.lock run: composer validate - - name: Test - run: composer test - build74: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2.3.4 - - uses: shivammathur/setup-php@2.14.0 + + - name: Set up PHP ${{ matrix.php-version }} + uses: shivammathur/setup-php@v2 with: - php-version: '7.4' - - name: Install - run: composer install - - name: Install - run: composer validate - - name: Test + php-version: ${{ matrix.php-version }} + extensions: pcntl, posix + coverage: xdebug + ini-values: error_reporting=E_ALL + + - name: Install dependencies + run: composer update + --prefer-dist + --no-progress + ${{ matrix.composer-options }} + + - name: Run tests + if: ${{ matrix.php-version != '8.1' }} run: composer test - build80: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2.3.4 - - uses: shivammathur/setup-php@2.14.0 - with: - php-version: '8.0' - - name: Install - run: composer install - - name: Install - run: composer validate - - name: Test + + - name: Run tests (experimental) + if: ${{ matrix.php-version == '8.1' }} + continue-on-error: true run: composer test diff --git a/.gitignore b/.gitignore index 404ae0d..f1f7aef 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ vendor/* composer.phar composer.lock /build/ +/test/codeception/_support/_generated/ .phpunit.result.cache diff --git a/README.md b/README.md index 94ed6ce..51ef322 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # Allure Codeception Adapter +[![Latest Stable Version](http://poser.pugx.org/allure-framework/allure-codeception/v)](https://packagist.org/packages/allure-framework/allure-codeception) [![Build](https://github.com/allure-framework/allure-codeception/actions/workflows/build.yml/badge.svg)](https://github.com/allure-framework/allure-codeception/actions/workflows/build.yml) +[![Type Coverage](https://shepherd.dev/github/allure-framework/allure-codeception/coverage.svg)](https://shepherd.dev/github/allure-framework/allure-codeception) +[![Psalm Level](https://shepherd.dev/github/allure-framework/allure-codeception/level.svg)](https://shepherd.dev/github/allure-framework/allure-codeception) +[![Total Downloads](http://poser.pugx.org/allure-framework/allure-codeception/downloads)](https://packagist.org/packages/allure-framework/allure-codeception) +[![License](http://poser.pugx.org/allure-framework/allure-codeception/license)](https://packagist.org/packages/allure-framework/allure-codeception) This is an official [Codeception](http://codeception.com) adapter for Allure Framework. @@ -15,8 +20,8 @@ In order to use this adapter you need to add a new dependency to your **composer ``` { "require": { - "php": ">=5.4.0", - "allure-framework/allure-codeception": ">=1.1.0" + "php": "^8", + "allure-framework/allure-codeception": "^2" } } ``` @@ -24,36 +29,27 @@ To enable this adapter in Codeception tests simply put it in "enabled" extension ```yaml extensions: enabled: - - Yandex\Allure\Codeception\AllureCodeception + - Qameta\Allure\Codeception\AllureCodeception config: - Yandex\Allure\Codeception\AllureCodeception: - deletePreviousResults: false + Qameta\Allure\Codeception\AllureCodeception: outputDirectory: allure-results - ignoredAnnotations: - - env - - dataprovider ``` -`deletePreviousResults` will clear all `.xml` files from output directory (this -behavior may change to complete cleanup later). It is set to `false` by default. - `outputDirectory` is used to store Allure results and will be calculated relatively to Codeception output directory (also known as `paths: log` in codeception.yml) unless you specify an absolute path. You can traverse up using `..` as usual. `outputDirectory` defaults to `allure-results`. -`ignoredAnnotations` is used to define extra custom annotations to ignore. It is empty by default. - To generate report from your favourite terminal, [install](https://github.com/allure-framework/allure-cli#installation) allure-cli and run following command (assuming you're in project root and using default configuration): ```bash -allure generate --report-version 1.4.5 --report-path tests/_output/allure-report -- tests/_output/allure-results +allure generate -o ./build/allure-report ./build/allure-results ``` -Report will be generated in `tests/_output/allure-report`. +Report will be generated in `build/allure-report`. ## Main features See respective [PHPUnit](https://github.com/allure-framework/allure-phpunit#advanced-features) section. diff --git a/codeception.yml b/codeception.yml index 9db5f91..72402e6 100644 --- a/codeception.yml +++ b/codeception.yml @@ -1,7 +1,4 @@ -namespace: Yandex\Allure\Codeception -suites: - unit: - path: . +namespace: Qameta\Allure\Codeception\Test settings: lint: true @@ -9,13 +6,12 @@ paths: tests: test/codeception output: build support: test/codeception/_support - data: test/codeception + data: test/codeception/_data extensions: enabled: - - Yandex\Allure\Codeception\AllureCodeception + - Qameta\Allure\Codeception\AllureCodeception config: - Yandex\Allure\Codeception\AllureCodeception: - deletePreviousResults: true + Qameta\Allure\Codeception\AllureCodeception: outputDirectory: allure-results diff --git a/composer.json b/composer.json index bf74156..2051574 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,12 @@ { "name": "Ivan Krutov", "email": "vania-pooh@aerokube.com", - "role": "Developer" + "role": "Developer" + }, + { + "name": "Edward Surov", + "email": "zoohie@gmail.com", + "role": "Developer" } ], "support": { @@ -16,37 +21,52 @@ "source": "https://github.com/allure-framework/allure-codeception" }, "require": { - "php": ">=7.1.3", + "php": "^8", "ext-json": "*", - "codeception/codeception": "^2.5 | ^3 | ^4", - "allure-framework/allure-php-api": "^1.3", - "symfony/filesystem": "^2.7 | ^3 | ^4 | ^5", - "symfony/finder": "^2.7 | ^3 | ^4 | ^5" + "codeception/codeception": "^4", + "allure-framework/allure-php-commons": "2.0.0-rc3" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^7.2 | ^8 | ^9" + "phpunit/phpunit": "^9", + "psalm/plugin-phpunit": "^0.16.1", + "remorhaz/php-json-data": "^0.5.3", + "remorhaz/php-json-path": "^0.7.7", + "squizlabs/php_codesniffer": "^3.6.1" }, "autoload": { - "psr-0": { - "Yandex": "src/" + "psr-4": { + "Qameta\\Allure\\Codeception\\": "src/" } }, "autoload-dev": { "psr-4": { - "Yandex\\Allure\\Codeception\\": [ - "test/report/", - "test/unit/" + "Qameta\\Allure\\Codeception\\Test\\Functional\\": "test/codeception/functional/", + "Qameta\\Allure\\Codeception\\Test\\Acceptance\\": "test/codeception/acceptance/", + "Qameta\\Allure\\Codeception\\Test\\Unit\\": "test/codeception/unit/", + "Qameta\\Allure\\Codeception\\Test\\": [ + "test/codeception/_support/", + "test/report/" ] } }, "scripts": { - "test-report": [ - "vendor/bin/codecept run --no-exit --report", - "vendor/bin/phpunit --testsuite=report" + "build": [ + "vendor/bin/codecept build", + "vendor/bin/codecept gherkin:snippets acceptance" + ], + "test-cs": "vendor/bin/phpcs -sp", + "test-report-generate": [ + "rm -rf ./build/log/", + "vendor/bin/codecept run --no-exit --report" ], + "test-report-check": "vendor/bin/phpunit --testsuite=report", + "test-psalm": "vendor/bin/psalm --shepherd", "test": [ - "@test-report" + "@test-cs", + "@test-report-generate", + "@test-report-check", + "@test-psalm" ] } } diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..227a3bb --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,15 @@ + + + Qameta Coding Standards + + src + test + + + + + + + */test/*Test.php + + diff --git a/phpunit.xml b/phpunit.xml.dist similarity index 95% rename from phpunit.xml rename to phpunit.xml.dist index dcb4000..8d84520 100644 --- a/phpunit.xml +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ diff --git a/psalm.xml.dist b/psalm.xml.dist new file mode 100644 index 0000000..4b5f86f --- /dev/null +++ b/psalm.xml.dist @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/src/AllureCodeception.php b/src/AllureCodeception.php new file mode 100644 index 0000000..d603690 --- /dev/null +++ b/src/AllureCodeception.php @@ -0,0 +1,279 @@ + 'suiteBefore', + Events::SUITE_AFTER => 'suiteAfter', + Events::TEST_START => 'testStart', + Events::TEST_FAIL => 'testFail', + Events::TEST_ERROR => 'testError', + Events::TEST_INCOMPLETE => 'testIncomplete', + Events::TEST_SKIPPED => 'testSkipped', + Events::TEST_SUCCESS => 'testSuccess', + Events::TEST_END => 'testEnd', + Events::STEP_BEFORE => 'stepBefore', + Events::STEP_AFTER => 'stepAfter' + ]; + + /** + * @var array + */ + private array $linkTemplates = []; + + private ?ThreadDetectorInterface $threadDetector = null; + + private ?TestLifecycleInterface $testLifecycle = null; + + /** + * {@inheritDoc} + * + * @throws ConfigurationException + * phpcs:disable PSR2.Methods.MethodDeclaration.Underscore + */ + public function _initialize(): void + { + // phpcs:enable PSR2.Methods.MethodDeclaration.Underscore + parent::_initialize(); + QametaAllure::reset(); + QametaAllure::getLifecycleConfigurator() + ->setStatusDetector(new StatusDetector(new DefaultStatusDetector())); + $this->callSetupHook(); + QametaAllure::setOutputDirectory($this->getOutputDirectory()); + } + + private function callSetupHook(): void + { + /** + * @var mixed $hookClass + * @psalm-var array $this->config + */ + $hookClass = $this->config[self::SETUP_HOOK_PARAMETER] ?? ''; + /** @psalm-suppress MixedMethodCall */ + $hook = is_string($hookClass) && class_exists($hookClass) + ? new $hookClass() + : null; + + if (is_callable($hook)) { + $hook(); + } + } + + /** + * @throws ConfigurationException + */ + private function getOutputDirectory(): string + { + /** + * @var mixed $outputCfg + * @psalm-var array $this->config + */ + $outputCfg = $this->config[self::OUTPUT_DIRECTORY_PARAMETER] ?? null; + $outputLocal = is_string($outputCfg) + ? trim($outputCfg, '\\/') + : null; + + return Configuration::outputDir() . ($outputLocal ?? self::DEFAULT_RESULTS_DIRECTORY) . DIRECTORY_SEPARATOR; + } + + /** + * @psalm-suppress MissingDependency + */ + public function suiteBefore(SuiteEvent $suiteEvent): void + { + /** @psalm-suppress InternalMethod */ + $suiteName = $suiteEvent->getSuite()->getName(); + $this + ->getTestLifecycle() + ->switchToSuite(new SuiteInfo($suiteName)); + } + + public function suiteAfter(): void + { + $this + ->getTestLifecycle() + ->resetSuite(); + } + + /** + * @psalm-suppress MissingDependency + */ + public function testStart(TestEvent $testEvent): void + { + $test = $testEvent->getTest(); + $this + ->getTestLifecycle() + ->switchToTest($test) + ->create() + ->updateTest() + ->startTest(); + } + + private function getThreadDetector(): ThreadDetectorInterface + { + return $this->threadDetector ??= new DefaultThreadDetector(); + } + + /** + * @psalm-suppress MissingDependency + */ + public function testError(FailEvent $failEvent): void + { + /** @var Throwable $error */ + $error = $failEvent->getFail(); + $this + ->getTestLifecycle() + ->switchToTest($failEvent->getTest()) + ->updateTestFailure($error); + } + + /** + * @psalm-suppress MissingDependency + */ + public function testFail(FailEvent $failEvent): void + { + /** @var Throwable $error */ + $error = $failEvent->getFail(); + $this + ->getTestLifecycle() + ->switchToTest($failEvent->getTest()) + ->updateTestFailure($error, Status::failed()); + } + + /** + * @psalm-suppress MissingDependency + */ + public function testIncomplete(FailEvent $failEvent): void + { + /** @var Throwable $error */ + $error = $failEvent->getFail(); + $this + ->getTestLifecycle() + ->switchToTest($failEvent->getTest()) + ->updateTestFailure( + $error, + Status::broken(), + new StatusDetails(message: $error->getMessage(), trace: $error->getTraceAsString()), + ); + } + + /** + * @psalm-suppress MissingDependency + */ + public function testSkipped(FailEvent $failEvent): void + { + /** @var Throwable $error */ + $error = $failEvent->getFail(); + $this + ->getTestLifecycle() + ->switchToTest($failEvent->getTest()) + ->updateTestFailure( + $error, + Status::skipped(), + new StatusDetails(message: $error->getMessage(), trace: $error->getTraceAsString()), + ); + } + + /** + * @psalm-suppress MissingDependency + */ + public function testSuccess(TestEvent $testEvent): void + { + $this + ->getTestLifecycle() + ->switchToTest($testEvent->getTest()) + ->updateTestSuccess(); + } + + /** + * @psalm-suppress MissingDependency + */ + public function testEnd(TestEvent $testEvent): void + { + $this + ->getTestLifecycle() + ->switchToTest($testEvent->getTest()) + ->updateTestResult() + ->attachReports() + ->stopTest(); + } + + /** + * @psalm-suppress MissingDependency + */ + public function stepBefore(StepEvent $stepEvent): void + { + /** @psalm-var Step $step */ + $step = $stepEvent->getStep(); + $this + ->getTestLifecycle() + ->switchToTest($stepEvent->getTest()) + ->startStep($step) + ->updateStep(); + } + + /** + * @psalm-suppress MissingDependency + */ + public function stepAfter(StepEvent $stepEvent): void + { + /** @psalm-var Step $step */ + $step = $stepEvent->getStep(); + $this + ->getTestLifecycle() + ->switchToTest($stepEvent->getTest()) + ->switchToStep($step) + ->updateStepResult() + ->stopStep(); + } + + private function getTestLifecycle(): TestLifecycleInterface + { + return $this->testLifecycle ??= new TestLifecycle( + Allure::getLifecycle(), + Allure::getResultFactory(), + Allure::getStatusDetector(), + $this->getThreadDetector(), + $this->linkTemplates, + ); + } +} diff --git a/src/Internal/ArgumentAsString.php b/src/Internal/ArgumentAsString.php new file mode 100644 index 0000000..bfaed8e --- /dev/null +++ b/src/Internal/ArgumentAsString.php @@ -0,0 +1,92 @@ + $this->prepareString($argument), + is_resource($argument) => $this->prepareResource($argument), + is_array($argument) => $this->prepareArray($argument), + is_object($argument) => $this->prepareObject($argument), + default => $argument, + }; + } + + private function prepareString(string $argument): string + { + return strtr($argument, ["\n" => '\n', "\r" => '\r', "\t" => ' ']); + } + + /** + * @param resource $argument + * @return string + */ + private function prepareResource($argument): string + { + return (string) $argument; + } + + private function prepareArray(array $argument): array + { + return array_map( + fn (mixed $element): mixed => $this->prepareArgument($element), + $argument, + ); + } + + private function prepareObject(object $argument): string + { + if (isset($argument->__mocked) && is_object($argument->__mocked)) { + $argument = $argument->__mocked; + } + if ($argument instanceof Stringable) { + return (string) $argument; + } + + if (is_a($argument, 'Facebook\WebDriver\WebDriverBy')) { + return Locator::humanReadableString($argument); + } + + return trim($argument::class, "\\"); + } + + public function __toString(): string + { + return json_encode( + $this->prepareArgument($this->argument), + JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, + ); + } +} diff --git a/src/Internal/CeptInfoBuilder.php b/src/Internal/CeptInfoBuilder.php new file mode 100644 index 0000000..8ad377d --- /dev/null +++ b/src/Internal/CeptInfoBuilder.php @@ -0,0 +1,28 @@ +test, + signature: (string) $this->test->getSignature(), + class: (string) $this->test->getName(), + method: (string) $this->test->getName(), + host: $host, + thread: $thread, + ); + } +} diff --git a/src/Internal/CeptProvider.php b/src/Internal/CeptProvider.php new file mode 100644 index 0000000..a436da9 --- /dev/null +++ b/src/Internal/CeptProvider.php @@ -0,0 +1,204 @@ + + */ + private array $legacyLabels = []; + + /** + * @var list + */ + private array $legacyLinks = []; + + private ?string $legacyTitle = null; + + private ?string $legacyDescription = null; + + /** + * @param Cept $test + * @param array $linkTemplates + */ + public function __construct( + private Cept $test, + private array $linkTemplates = [], + ) { + } + + /** + * @param Cept $test + * @param array $linkTemplates + * @return list + */ + public static function createForChain(Cept $test, array $linkTemplates): array + { + return [new self($test, $linkTemplates)]; + } + + public function getLinks(): array + { + $this->loadLegacyModels(); + + return $this->legacyLinks; + } + + public function getLabels(): array + { + $this->loadLegacyModels(); + + return $this->legacyLabels; + } + + public function getParameters(): array + { + return []; + } + + /** + * @deprecated Please use {@see getDisplayName()} method + */ + public function getTitle(): ?string + { + return $this->getDisplayName(); + } + + public function getDisplayName(): ?string + { + $this->loadLegacyModels(); + + if (isset($this->legacyTitle)) { + return $this->legacyTitle; + } + + /** @psalm-var mixed $testName */ + $testName = $this->test->getName(); + + return is_string($testName) + ? $testName + : null; + } + + public function getDescription(): ?string + { + $this->loadLegacyModels(); + + return $this->legacyDescription; + } + + public function getDescriptionHtml(): ?string + { + return null; + } + + private function getLegacyAnnotation(string $name): ?string + { + /** + * @var mixed $annotations + * @psalm-suppress InvalidArgument + */ + $annotations = $this->test->getMetadata()->getParam($name); + if (!is_array($annotations)) { + return null; + } + /** @var mixed $lastAnnotation */ + $lastAnnotation = array_pop($annotations); + + return is_string($lastAnnotation) + ? $this->getStringFromTagContent(trim($lastAnnotation, '()')) + : null; + } + + /** + * @param string $name + * @return list + */ + private function getLegacyAnnotations(string $name): array + { + /** + * @var mixed $annotations + * @psalm-suppress InvalidArgument + */ + $annotations = $this->test->getMetadata()->getParam($name); + $stringAnnotations = is_array($annotations) + ? array_values(array_filter($annotations, 'is_string')) + : []; + + return array_merge( + ...array_map( + fn (string $annotation) => $this->getStringsFromTagContent(trim($annotation, '()')), + $stringAnnotations, + ), + ); + } + + private function loadLegacyModels(): void + { + if ($this->isLoaded) { + return; + } + $this->isLoaded = true; + + $this->legacyTitle = $this->getLegacyAnnotation('Title'); + $this->legacyDescription = $this->getLegacyAnnotation('Description'); + $this->legacyLabels = [ + ...array_map( + fn (string $value): Label => Label::feature($value), + $this->getLegacyAnnotations('Features'), + ), + ...array_map( + fn (string $value): Label => Label::story($value), + $this->getLegacyAnnotations('Stories'), + ), + ]; + $linkTemplate = $this->linkTemplates[(string) LinkType::issue()] ?? null; + $this->legacyLinks = array_map( + fn (string $value): Link => Link::issue($value, $linkTemplate?->buildUrl($value)), + $this->getLegacyAnnotations('Issues'), + ); + } + + private function getStringFromTagContent(string $tagContent): string + { + return str_replace('"', '', $tagContent); + } + + /** + * @param string $string + * @return list + */ + private function getStringsFromTagContent(string $string): array + { + $detected = str_replace(['{', '}', '"'], '', $string); + + return explode(',', $detected); + } +} diff --git a/src/Internal/CestInfoBuilder.php b/src/Internal/CestInfoBuilder.php new file mode 100644 index 0000000..4fa8cdf --- /dev/null +++ b/src/Internal/CestInfoBuilder.php @@ -0,0 +1,53 @@ +test->getTestClass(); + /** @var mixed $testMethod */ + $testMethod = $this->test->getTestMethod(); + + return new TestInfo( + originalTest: $this->test, + signature: (string) $this->test->getSignature(), + class: is_object($testClass) ? $testClass::class : null, + method: is_string($testMethod) ? $testMethod : null, + dataLabel: $this->getDataLabel(), + host: $host, + thread: $thread, + ); + } + + private function getDataLabel(): ?string + { + /** @psalm-var mixed $index */ + $index = $this->test->getMetadata()->getIndex(); + + if (is_string($index)) { + return $index; + } + if (is_int($index)) { + return "#$index"; + } + + return null; + } +} diff --git a/src/Internal/CestProvider.php b/src/Internal/CestProvider.php new file mode 100644 index 0000000..6f52f67 --- /dev/null +++ b/src/Internal/CestProvider.php @@ -0,0 +1,116 @@ + $linkTemplates + * @return list + * @throws ReflectionException + */ + public static function createForChain(Cest $test, array $linkTemplates = []): array + { + /** @var mixed $testClass */ + $testClass = $test->getTestClass(); + /** @var mixed $testMethod */ + $testMethod = $test->getTestMethod(); + /** @var callable-string|null $callableTestMethod */ + $callableTestMethod = is_string($testMethod) ? $testMethod : null; + + return [ + ...AttributeParser::createForChain( + classOrObject: is_object($testClass) ? $testClass : null, + methodOrFunction: $callableTestMethod, + linkTemplates: $linkTemplates, + ), + new self($test), + ]; + } + + public function getLinks(): array + { + return []; + } + + public function getLabels(): array + { + return []; + } + + public function getParameters(): array + { + /** @var mixed $currentExample */ + $currentExample = $this + ->test + ->getMetadata() + ->getCurrent('example') ?? []; + if (!is_array($currentExample)) { + return []; + } + + return array_map( + fn (mixed $value, int|string $name) => new Parameter( + is_int($name) ? "#$name" : $name, + ArgumentAsString::get($value), + ), + array_values($currentExample), + array_keys($currentExample), + ); + } + + /** + * @deprecated Please, use {@see getDisplayName()} method + */ + public function getTitle(): ?string + { + return $this->getDisplayName(); + } + + public function getDisplayName(): ?string + { + /** @psalm-var mixed $displayName */ + $displayName = $this->test->getName(); + + return is_string($displayName) + ? $displayName + : null; + } + + public function getDescription(): ?string + { + return null; + } + + public function getDescriptionHtml(): ?string + { + return null; + } +} diff --git a/src/Internal/DefaultThreadDetector.php b/src/Internal/DefaultThreadDetector.php new file mode 100644 index 0000000..a926dba --- /dev/null +++ b/src/Internal/DefaultThreadDetector.php @@ -0,0 +1,29 @@ +host ??= gethostname(); + + return $this->host === false + ? null + : $this->host; + } + + public function getThread(): ?string + { + return null; + } +} diff --git a/src/Internal/GherkinInfoBuilder.php b/src/Internal/GherkinInfoBuilder.php new file mode 100644 index 0000000..af74a4c --- /dev/null +++ b/src/Internal/GherkinInfoBuilder.php @@ -0,0 +1,35 @@ +test->getFeature(); + /** @psalm-var mixed $methodName */ + $methodName = $this->test->getScenarioTitle(); + + return new TestInfo( + originalTest: $this->test, + signature: (string) $this->test->getSignature(), + class: is_string($className) ? $className : null, + method: is_string($methodName) ? $methodName : null, + host: $host, + thread: $thread, + ); + } +} diff --git a/src/Internal/GherkinProvider.php b/src/Internal/GherkinProvider.php new file mode 100644 index 0000000..a831c92 --- /dev/null +++ b/src/Internal/GherkinProvider.php @@ -0,0 +1,73 @@ + Label::feature($value), + [ + ...array_values($this->test->getFeatureNode()->getTags()), + ...array_values($this->test->getScenarioNode()->getTags()), + ], + ); + } + + public function getParameters(): array + { + return []; + } + + /** + * @deprecated Please use {@see getDisplayName()} method + */ + public function getTitle(): ?string + { + return $this->getDisplayName(); + } + + public function getDisplayName(): ?string + { + return (string) $this->test->toString(); + } + + public function getDescription(): ?string + { + return null; + } + + public function getDescriptionHtml(): ?string + { + return null; + } +} diff --git a/src/Internal/StepStartInfo.php b/src/Internal/StepStartInfo.php new file mode 100644 index 0000000..099afe3 --- /dev/null +++ b/src/Internal/StepStartInfo.php @@ -0,0 +1,27 @@ +originalStep; + } + + public function getUuid(): string + { + return $this->uuid; + } +} diff --git a/src/Internal/SuiteInfo.php b/src/Internal/SuiteInfo.php new file mode 100644 index 0000000..2c085fd --- /dev/null +++ b/src/Internal/SuiteInfo.php @@ -0,0 +1,34 @@ +name; + } + + /** + * @return class-string|null + */ + public function getClass(): ?string + { + return class_exists($this->name, false) + ? $this->name + : null; + } +} diff --git a/src/Internal/SuiteProvider.php b/src/Internal/SuiteProvider.php new file mode 100644 index 0000000..ea85d6e --- /dev/null +++ b/src/Internal/SuiteProvider.php @@ -0,0 +1,84 @@ + $linkTemplates + * @return list + * @throws ReflectionException + */ + public static function createForChain( + ?SuiteInfo $suiteInfo, + array $linkTemplates, + ): array { + $providers = [new self($suiteInfo)]; + $suiteClass = $suiteInfo?->getClass(); + + return isset($suiteClass) + ? [ + ...$providers, + ...AttributeParser::createForChain(classOrObject: $suiteClass, linkTemplates: $linkTemplates), + ] + : $providers; + } + + public function getLinks(): array + { + return []; + } + + public function getLabels(): array + { + return [ + Label::language(null), + Label::framework('codeception'), + Label::parentSuite($this->suiteInfo?->getName()), + Label::package($this->suiteInfo?->getName()), + ]; + } + + public function getParameters(): array + { + return []; + } + + public function getTitle(): ?string + { + return $this->getDisplayName(); + } + + public function getDisplayName(): ?string + { + return null; + } + + public function getDescription(): ?string + { + return null; + } + + public function getDescriptionHtml(): ?string + { + return null; + } +} diff --git a/src/Internal/TestInfo.php b/src/Internal/TestInfo.php new file mode 100644 index 0000000..8ceecfd --- /dev/null +++ b/src/Internal/TestInfo.php @@ -0,0 +1,55 @@ +originalTest; + } + + public function getSignature(): string + { + return $this->signature; + } + + public function getClass(): ?string + { + return $this->class; + } + + public function getMethod(): ?string + { + return $this->method; + } + + public function getDataLabel(): ?string + { + return $this->dataLabel; + } + + public function getHost(): ?string + { + return $this->host; + } + + public function getThread(): ?string + { + return $this->thread; + } +} diff --git a/src/Internal/TestInfoBuilderInterface.php b/src/Internal/TestInfoBuilderInterface.php new file mode 100644 index 0000000..cb29ee3 --- /dev/null +++ b/src/Internal/TestInfoBuilderInterface.php @@ -0,0 +1,11 @@ + + */ + public static function createForChain(TestInfo $info): array + { + return [new self($info)]; + } + + public function getLinks(): array + { + return []; + } + + public function getLabels(): array + { + return [ + Label::testClass($this->info->getClass()), + Label::testMethod($this->info->getMethod()), + Label::host($this->info->getHost()), + Label::thread($this->info->getThread()), + ]; + } + + public function getParameters(): array + { + return []; + } + + /** + * @deprecated Please use {@see getDisplayName()} method + */ + public function getTitle(): ?string + { + return $this->getDisplayName(); + } + + public function getDisplayName(): ?string + { + return null; + } + + public function getDescription(): ?string + { + return null; + } + + public function getDescriptionHtml(): ?string + { + return null; + } +} diff --git a/src/Internal/TestLifecycle.php b/src/Internal/TestLifecycle.php new file mode 100644 index 0000000..32eea66 --- /dev/null +++ b/src/Internal/TestLifecycle.php @@ -0,0 +1,385 @@ + + */ + private WeakMap $stepStarts; + + /** + * @param AllureLifecycleInterface $lifecycle + * @param ResultFactoryInterface $resultFactory + * @param StatusDetectorInterface $statusDetector + * @param ThreadDetectorInterface $threadDetector + * @param array $linkTemplates + */ + public function __construct( + private AllureLifecycleInterface $lifecycle, + private ResultFactoryInterface $resultFactory, + private StatusDetectorInterface $statusDetector, + private ThreadDetectorInterface $threadDetector, + private array $linkTemplates = [], + ) { + /** @var WeakMap */ + $this->stepStarts = new WeakMap(); + } + + public function getCurrentSuite(): SuiteInfo + { + return $this->currentSuite ?? throw new RuntimeException("Current suite not found"); + } + + public function getCurrentTest(): TestInfo + { + return $this->currentTest ?? throw new RuntimeException("Current test not found"); + } + + public function getCurrentTestStart(): TestStartInfo + { + return $this->currentTestStart ?? throw new RuntimeException("Current test start not found"); + } + + public function getCurrentStepStart(): StepStartInfo + { + return $this->currentStepStart ?? throw new RuntimeException("Current step start not found"); + } + + public function switchToSuite(SuiteInfo $suiteInfo): self + { + $this->currentSuite = $suiteInfo; + + return $this; + } + + public function resetSuite(): self + { + $this->currentSuite = null; + + return $this; + } + + public function switchToTest(object $test): self + { + $thread = $this->threadDetector->getThread(); + $this->lifecycle->switchThread($thread); + + $this->currentTest = $this + ->getTestInfoBuilder($test) + ->build( + $this->threadDetector->getHost(), + $thread, + ); + + return $this; + } + + private function getTestInfoBuilder(object $test): TestInfoBuilderInterface + { + return match (true) { + $test instanceof Cest => new CestInfoBuilder($test), + $test instanceof Gherkin => new GherkinInfoBuilder($test), + $test instanceof Cept => new CeptInfoBuilder($test), + $test instanceof TestCase => new UnitInfoBuilder($test), + default => new UnknownInfoBuilder($test), + }; + } + + public function create(): self + { + $containerResult = $this->resultFactory->createContainer(); + $this->lifecycle->startContainer($containerResult); + + $testResult = $this->resultFactory->createTest(); + $this->lifecycle->scheduleTest($testResult, $containerResult->getUuid()); + + $this->currentTestStart = new TestStartInfo( + containerUuid: $containerResult->getUuid(), + testUuid: $testResult->getUuid(), + ); + + return $this; + } + + public function updateTest(): self + { + $provider = new ModelProviderChain( + ...SuiteProvider::createForChain($this->getCurrentSuite(), $this->linkTemplates), + ...TestInfoProvider::createForChain($this->getCurrentTest()), + ...$this->createModelProvidersForTest($this->getCurrentTest()->getOriginalTest()), + ); + $this->lifecycle->updateTest( + fn (TestResult $t) => $t + ->setName($provider->getDisplayName()) + ->setFullName($this->getCurrentTest()->getSignature()) + ->setDescription($provider->getDescription()) + ->setDescriptionHtml($provider->getDescriptionHtml()) + ->addLinks(...$provider->getLinks()) + ->addLabels(...$provider->getLabels()) + ->addParameters(...$provider->getParameters()), + $this->getCurrentTestStart()->getTestUuid(), + ); + + return $this; + } + + private function createModelProvidersForTest(mixed $test): array + { + return match (true) { + $test instanceof Cest => CestProvider::createForChain($test, $this->linkTemplates), + $test instanceof Gherkin => GherkinProvider::createForChain($test), + $test instanceof Cept => CeptProvider::createForChain($test, $this->linkTemplates), + $test instanceof TestCase => UnitProvider::createForChain($test, $this->linkTemplates), + default => [], + }; + } + + public function startTest(): self + { + $this->lifecycle->startTest($this->getCurrentTestStart()->getTestUuid()); + + return $this; + } + + public function stopTest(): self + { + $testUuid = $this->getCurrentTestStart()->getTestUuid(); + $this + ->lifecycle + ->stopTest($testUuid); + $this->lifecycle->writeTest($testUuid); + + $containerUuid = $this->getCurrentTestStart()->getContainerUuid(); + $this + ->lifecycle + ->stopContainer($containerUuid); + $this->lifecycle->writeContainer($containerUuid); + + $this->currentTest = null; + $this->currentTestStart = null; + + return $this; + } + + public function updateTestFailure( + Throwable $error, + ?Status $status = null, + ?StatusDetails $statusDetails = null, + ): self { + $this->lifecycle->updateTest( + fn (TestResult $t) => $t + ->setStatus($status ?? $this->statusDetector->getStatus($error)) + ->setStatusDetails($statusDetails ?? $this->statusDetector->getStatusDetails($error)), + ); + + return $this; + } + + public function updateTestSuccess(): self + { + $this->lifecycle->updateTest( + fn (TestResult $t) => $t->setStatus(Status::passed()), + ); + + return $this; + } + + public function attachReports(): self + { + $originalTest = $this->getCurrentTest()->getOriginalTest(); + if ($originalTest instanceof TestInterface) { + $artifacts = $originalTest->getMetadata()->getReports(); + /** + * @psalm-var mixed $artifact + */ + foreach ($artifacts as $name => $artifact) { + $attachment = $this + ->resultFactory + ->createAttachment() + ->setName((string) $name); + if (!is_string($artifact)) { + continue; + } + $dataSource = @file_exists($artifact) && !is_file($artifact) + ? DataSourceFactory::fromFile($artifact) + : DataSourceFactory::fromString($artifact); + $this + ->lifecycle + ->addAttachment($attachment, $dataSource); + } + } + + return $this; + } + + public function updateTestResult(): self + { + $this->lifecycle->updateTest( + fn (TestResult $t) => $t + ->setTestCaseId($testCaseId = $this->buildTestCaseId($this->getCurrentTest(), ...$t->getParameters())) + ->setHistoryId($this->buildHistoryId($testCaseId, $this->getCurrentTest(), ...$t->getParameters())), + $this->getCurrentTestStart()->getTestUuid(), + ); + + return $this; + } + + private function buildTestCaseId(TestInfo $testInfo, Parameter ...$parameters): string + { + $parameterNames = implode( + '::', + array_map( + fn (Parameter $parameter): string => $parameter->getName(), + array_filter( + $parameters, + fn (Parameter $parameter): bool => !$parameter->getExcluded(), + ), + ), + ); + + return md5("{$testInfo->getSignature()}::$parameterNames"); + } + + private function buildHistoryId(string $testCaseId, TestInfo $testInfo, Parameter ...$parameters): string + { + $parameterNames = implode( + '::', + array_map( + fn (Parameter $parameter): string => $parameter->getValue() ?? '', + array_filter( + $parameters, + fn (Parameter $parameter): bool => !$parameter->getExcluded(), + ), + ), + ); + + return md5("$testCaseId::{$testInfo->getSignature()}::$parameterNames"); + } + + public function startStep(Step $step): self + { + $parent = $this->currentStepStart?->getUuid() ?? $this->currentTestStart?->getTestUuid(); + $stepResult = $this->resultFactory->createStep(); + $this->lifecycle->startStep($stepResult, $parent); + + $stepStart = new StepStartInfo( + $step, + $stepResult->getUuid(), + ); + $this->stepStarts[$step] = $stepStart; + $this->currentStepStart = $stepStart; + + return $this; + } + + public function switchToStep(Step $step): self + { + $this->currentStepStart = + $this->stepStarts[$step] ?? throw new RuntimeException("Step start info not found"); + + return $this; + } + + public function stopStep(): self + { + $stepStart = $this->getCurrentStepStart(); + $this->lifecycle->stopStep($stepStart->getUuid()); + /** + * @var Step $step + * @psalm-ignore-var + */ + foreach ($this->stepStarts as $step => $storedStart) { + if ($storedStart === $stepStart) { + unset($this->stepStarts[$step]); + } + } + $this->currentStepStart = null; + + return $this; + } + + public function updateStep(): self + { + $stepStart = $this->getCurrentStepStart(); + $step = $stepStart->getOriginalStep(); + if (null === $step->getAction()) { + $step = $step->getMetaStep(); + } + + $params = []; + /** + * @var array-key $name + * @var mixed $value + */ + foreach ($step->getArguments() as $name => $value) { + $params[] = new Parameter( + is_int($name) ? "#$name" : $name, + ArgumentAsString::get($value), + ); + } + /** @var mixed $humanizedAction */ + $humanizedAction = $step->getHumanizedActionWithoutArguments(); + $this->lifecycle->updateStep( + fn (StepResult $s) => $s + ->setName(is_string($humanizedAction) ? $humanizedAction : null) + ->setParameters(...$params), + $stepStart->getUuid(), + ); + + return $this; + } + + public function updateStepResult(): self + { + $this->lifecycle->updateStep( + fn (StepResult $s) => $s + ->setStatus( + $this->getCurrentStepStart()->getOriginalStep()->hasFailed() + ? Status::failed() + : Status::passed(), + ), + ); + + return $this; + } +} diff --git a/src/Internal/TestLifecycleInterface.php b/src/Internal/TestLifecycleInterface.php new file mode 100644 index 0000000..9e61ed8 --- /dev/null +++ b/src/Internal/TestLifecycleInterface.php @@ -0,0 +1,50 @@ +containerUuid; + } + + public function getTestUuid(): string + { + return $this->testUuid; + } +} diff --git a/src/Internal/UnitInfoBuilder.php b/src/Internal/UnitInfoBuilder.php new file mode 100644 index 0000000..e0593c7 --- /dev/null +++ b/src/Internal/UnitInfoBuilder.php @@ -0,0 +1,44 @@ +test->getName(false); + + return new TestInfo( + originalTest: $this->test, + signature: $this->test::class . ':' . $methodName, + class: $this->test::class, + method: $methodName, + dataLabel: $this->getDataLabel(), + host: $host, + thread: $thread, + ); + } + + private function getDataLabel(): ?string + { + /** @psalm-suppress InternalMethod */ + $dataSet = $this->test->getDataSetAsString(false); + + return 1 === preg_match('#^ with data set (.+)$#', $dataSet, $matches) + ? $matches[1] + : null; + } +} diff --git a/src/Internal/UnitProvider.php b/src/Internal/UnitProvider.php new file mode 100644 index 0000000..d43e583 --- /dev/null +++ b/src/Internal/UnitProvider.php @@ -0,0 +1,126 @@ + $linkTemplates + */ + public function __construct( + private TestCase $test, + array $linkTemplates = [], + ) { + } + + /** + * @param TestCase $test + * @param array $linkTemplates + * @throws ReflectionException + * @return list + */ + public static function createForChain(TestCase $test, array $linkTemplates = []): array + { + /** + * @var callable-string|null $methodOrFunction + * @psalm-suppress InternalMethod + */ + $methodOrFunction = $test->getName(false); + + return [ + ...AttributeParser::createForChain( + classOrObject: $test, + methodOrFunction: $methodOrFunction, + linkTemplates: $linkTemplates, + ), + new self($test, $linkTemplates), + ]; + } + + public function getLinks(): array + { + return []; + } + + public function getLabels(): array + { + return []; + } + + /** + * @throws ReflectionException + */ + public function getParameters(): array + { + /** @psalm-suppress InternalMethod */ + if (!$this->test->usesDataProvider()) { + return []; + } + + $dataMethod = new ReflectionMethod($this->test, 'getProvidedData'); + $dataMethod->setAccessible(true); + /** @psalm-suppress InternalMethod */ + $methodName = $this->test->getName(false); + $testMethod = new ReflectionMethod($this->test, $methodName); + $argNames = $testMethod->getParameters(); + + $params = []; + /** + * @var array-key $key + * @var mixed $param + */ + foreach ($dataMethod->invoke($this->test) as $key => $param) { + $argName = array_shift($argNames); + $name = $argName?->getName() ?? $key; + $params[] = new Parameter( + is_int($name) ? "#$name" : $name, + ArgumentAsString::get($param), + ); + } + + return $params; + } + + /** + * @deprecated Please use {@see getDisplayName()} method + */ + public function getTitle(): ?string + { + return $this->getDisplayName(); + } + + public function getDisplayName(): ?string + { + /** @psalm-suppress InternalMethod */ + return $this->test->getName(); + } + + public function getDescription(): ?string + { + return null; + } + + public function getDescriptionHtml(): ?string + { + return null; + } +} diff --git a/src/Internal/UnknownInfoBuilder.php b/src/Internal/UnknownInfoBuilder.php new file mode 100644 index 0000000..328c7a3 --- /dev/null +++ b/src/Internal/UnknownInfoBuilder.php @@ -0,0 +1,24 @@ +test, + signature: 'Unknown test: ' . $this->test::class, + host: $host, + thread: $thread, + ); + } +} diff --git a/src/Setup/ThreadDetectorInterface.php b/src/Setup/ThreadDetectorInterface.php new file mode 100644 index 0000000..8643516 --- /dev/null +++ b/src/Setup/ThreadDetectorInterface.php @@ -0,0 +1,13 @@ +getUnwrappedStatus( + $this->unwrapError($error), + ); + } + + private function getUnwrappedStatus(Throwable $error): Status + { + return match (true) { + $error instanceof SkippedTestError => Status::skipped(), + $error instanceof AssertionFailedError => Status::failed(), + default => Status::broken(), + }; + } + + public function getStatusDetails(Throwable $error): ?StatusDetails + { + $unwrappedError = $this->unwrapError($error); + $unwrappedStatus = $this->getUnwrappedStatus($unwrappedError); + + return match (true) { + Status::skipped() === $unwrappedStatus, + Status::failed() === $unwrappedStatus => new StatusDetails( + message: $error->getMessage(), + trace: $error->getTraceAsString(), + ), + default => $this->defaultStatusDetector->getStatusDetails($unwrappedError), + }; + } + + private function unwrapError(Throwable $error): Throwable + { + /** @psalm-suppress InternalMethod */ + return $error instanceof ExceptionWrapper + ? $error->getOriginalException() ?? $error + : $error; + } + + private function buildMessage(Throwable $error): string + { + /** @psalm-suppress InternalMethod */ + return $error instanceof AssertionFailedError + ? $error->toString() + : $error->getMessage(); + } +} diff --git a/src/Yandex/Allure/Codeception/AllureCodeception.php b/src/Yandex/Allure/Codeception/AllureCodeception.php deleted file mode 100644 index 0c758c4..0000000 --- a/src/Yandex/Allure/Codeception/AllureCodeception.php +++ /dev/null @@ -1,567 +0,0 @@ - 'suiteBefore', - Events::SUITE_AFTER => 'suiteAfter', - Events::TEST_START => 'testStart', - Events::TEST_FAIL => 'testFail', - Events::TEST_ERROR => 'testError', - Events::TEST_INCOMPLETE => 'testIncomplete', - Events::TEST_SKIPPED => 'testSkipped', - Events::TEST_END => 'testEnd', - Events::STEP_BEFORE => 'stepBefore', - Events::STEP_AFTER => 'stepAfter' - ]; - - /** - * Annotations that should be ignored by the annotaions parser (especially PHPUnit annotations). - * - * @var array - */ - private $ignoredAnnotations = [ - 'after', 'afterClass', 'backupGlobals', 'backupStaticAttributes', 'before', 'beforeClass', - 'codeCoverageIgnore', 'codeCoverageIgnoreStart', 'codeCoverageIgnoreEnd', 'covers', - 'coversDefaultClass', 'coversNothing', 'dataProvider', 'depends', 'expectedException', - 'expectedExceptionCode', 'expectedExceptionMessage', 'group', 'large', 'medium', - 'preserveGlobalState', 'requires', 'runTestsInSeparateProcesses', 'runInSeparateProcess', - 'small', 'test', 'testdox', 'ticket', 'uses', - ]; - - /** - * Extra annotations to ignore in addition to standard PHPUnit annotations. - * - * @param array $ignoredAnnotations - */ - public function _initialize(array $ignoredAnnotations = []) - { - parent::_initialize(); - Annotation\AnnotationProvider::registerAnnotationNamespaces(); - // Add standard PHPUnit annotations - Annotation\AnnotationProvider::addIgnoredAnnotations($this->ignoredAnnotations); - // Add custom ignored annotations - $ignoredAnnotations = $this->tryGetOption(IGNORED_ANNOTATION_PARAMETER, []); - Annotation\AnnotationProvider::addIgnoredAnnotations($ignoredAnnotations); - $outputDirectory = $this->getOutputDirectory(); - $deletePreviousResults = - $this->tryGetOption(DELETE_PREVIOUS_RESULTS_PARAMETER, false); - $this->prepareOutputDirectory($outputDirectory, $deletePreviousResults); - if (is_null(Model\Provider::getOutputDirectory())) { - Model\Provider::setOutputDirectory($outputDirectory); - } - $this->setOption(INITIALIZED_PARAMETER, true); - } - - /** - * Sets runtime option which will be live - * - * @param string $key - * @param mixed $value - */ - private function setOption($key, $value) - { - $config = []; - $cursor = &$config; - $path = ['extensions', 'config', get_class()]; - foreach ($path as $segment) { - $cursor[$segment] = []; - $cursor = &$cursor[$segment]; - } - $cursor[$key] = $this->config[$key] = $value; - Configuration::append($config); - } - - /** - * Retrieves option or returns default value. - * - * @param string $optionKey Configuration option key. - * @param mixed $defaultValue Value to return in case option isn't set. - * - * @return mixed Option value. - * @since 0.1.0 - */ - private function tryGetOption($optionKey, $defaultValue = null) - { - if (array_key_exists($optionKey, $this->config)) { - return $this->config[$optionKey]; - } - return $defaultValue; - } - - /** @noinspection PhpUnusedPrivateMethodInspection */ - /** - * Retrieves option or dies. - * - * @param string $optionKey Configuration option key. - * - * @throws ConfigurationException Thrown if option can't be retrieved. - * - * @return mixed Option value. - * @since 0.1.0 - */ - private function getOption($optionKey) - { - if (!array_key_exists($optionKey, $this->config)) { - $template = '%s: Couldn\'t find required configuration option `%s`'; - $message = sprintf($template, __CLASS__, $optionKey); - throw new ConfigurationException($message); - } - return $this->config[$optionKey]; - } - - /** - * Returns output directory. - * - * @throws ConfigurationException Thrown if there is Codeception-wide - * problem with output directory - * configuration. - * - * @return string Absolute path to output directory. - * @since 0.1.0 - */ - private function getOutputDirectory() - { - $outputDirectory = $this->tryGetOption( - OUTPUT_DIRECTORY_PARAMETER, - DEFAULT_RESULTS_DIRECTORY - ); - $filesystem = new Filesystem; - if (!$filesystem->isAbsolutePath($outputDirectory)) { - $outputDirectory = Configuration::outputDir() . $outputDirectory; - } - return $outputDirectory; - } - - /** - * Creates output directory (if it hasn't been created yet) and cleans it - * up (if corresponding argument has been set to true). - * - * @param string $outputDirectory - * @param bool $deletePreviousResults Whether to delete previous results - * or keep 'em. - * - * @since 0.1.0 - */ - private function prepareOutputDirectory( - $outputDirectory, - $deletePreviousResults = false - ) { - $filesystem = new Filesystem; - $filesystem->mkdir($outputDirectory, 0775); - $initialized = $this->tryGetOption(INITIALIZED_PARAMETER, false); - if ($deletePreviousResults && !$initialized) { - $finder = new Finder; - $files = $finder->files()->in($outputDirectory)->name('*.xml'); - $filesystem->remove($files); - } - } - - public function suiteBefore(SuiteEvent $suiteEvent) - { - $suite = $suiteEvent->getSuite(); - $suiteName = $suite->getName(); - $event = new TestSuiteStartedEvent($suiteName); - if (class_exists($suiteName, false)) { - $annotationManager = new Annotation\AnnotationManager( - Annotation\AnnotationProvider::getClassAnnotations($suiteName) - ); - $annotationManager->updateTestSuiteEvent($event); - } - $this->uuid = $event->getUuid(); - $this->getLifecycle()->fire($event); - } - - public function suiteAfter() - { - $this->getLifecycle()->fire(new TestSuiteFinishedEvent($this->uuid)); - } - - private $testInvocations = array(); - private function buildTestName($test) { - $testName = $test->getName(); - if ($test instanceof Cest) { - $testFullName = get_class($test->getTestClass()) . '::' . $testName; - if(isset($this->testInvocations[$testFullName])) { - $this->testInvocations[$testFullName]++; - } else { - $this->testInvocations[$testFullName] = 0; - } - $currentExample = $test->getMetadata()->getCurrent(); - if ($currentExample && isset($currentExample['example']) ) { - $testName .= ' with data set #' . $this->testInvocations[$testFullName]; - } - } else if($test instanceof Gherkin) { - $testName = $test->getScenarioNode()->getTitle(); - } - return $testName; - } - - public function testStart(TestEvent $testEvent) - { - $test = $testEvent->getTest(); - $testName = $this->buildTestName($test); - $event = new TestCaseStartedEvent($this->uuid, $testName); - if ($test instanceof Cest) { - $methodName = $test->getName(); - $className = get_class($test->getTestClass()); - $event->setLabels(array_merge($event->getLabels(), [ - new Label("testMethod", $methodName), - new Label("testClass", $className) - ])); - $annotations = []; - if (class_exists($className, false)) { - $annotations = array_merge($annotations, Annotation\AnnotationProvider::getClassAnnotations($className)); - } - if (method_exists($className, $test->getName())){ - $annotations = array_merge($annotations, Annotation\AnnotationProvider::getMethodAnnotations($className, $test->getName())); - } - $annotationManager = new Annotation\AnnotationManager($annotations); - $annotationManager->updateTestCaseEvent($event); - } else if ($test instanceof Gherkin) { - $featureTags = $test->getFeatureNode()->getTags(); - $scenarioTags = $test->getScenarioNode()->getTags(); - $event->setLabels( - array_map( - function ($a) { - return new Label($a, LabelType::FEATURE); - }, - array_merge($featureTags, $scenarioTags) - ) - ); - } else if ($test instanceof Cept) { - $annotations = $this->getCeptAnnotations($test); - if (count($annotations) > 0) { - $annotationManager = new Annotation\AnnotationManager($annotations); - $annotationManager->updateTestCaseEvent($event); - } - } else if ($test instanceof \PHPUnit\Framework\TestCase) { - $methodName = $this->methodName = $test->getName(false); - $className = get_class($test); - if (class_exists($className, false)) { - $annotationManager = new Annotation\AnnotationManager( - Annotation\AnnotationProvider::getClassAnnotations($className) - ); - $annotationManager->updateTestCaseEvent($event); - } - if (method_exists($test, $methodName)) { - $annotationManager = new Annotation\AnnotationManager( - Annotation\AnnotationProvider::getMethodAnnotations(get_class($test), $methodName) - ); - $annotationManager->updateTestCaseEvent($event); - } - } - $this->getLifecycle()->fire($event); - - if ($test instanceof Cest) { - $currentExample = $test->getMetadata()->getCurrent(); - if ($currentExample && isset($currentExample['example']) ) { - foreach ($currentExample['example'] as $name => $param) { - $paramEvent = new AddParameterEvent( - $name, $this->stringifyArgument($param), ParameterKind::ARGUMENT); - $this->getLifecycle()->fire($paramEvent); - } - } - } else if ($test instanceof \PHPUnit_Framework_TestCase) { - if ($test->usesDataProvider()) { - $method = new \ReflectionMethod(get_class($test), 'getProvidedData'); - $method->setAccessible(true); - $testMethod = new \ReflectionMethod(get_class($test), $test->getName(false)); - $paramNames = $testMethod->getParameters(); - foreach ($method->invoke($test) as $key => $param) { - $paramName = array_shift($paramNames); - $paramEvent = new AddParameterEvent( - is_null($paramName) - ? $key - : $paramName->getName(), - $this->stringifyArgument($param), - ParameterKind::ARGUMENT); - $this->getLifecycle()->fire($paramEvent); - } - } - } - } - - /** - * @param FailEvent $failEvent - */ - public function testError(FailEvent $failEvent) - { - $event = new TestCaseBrokenEvent(); - $e = $failEvent->getFail(); - $message = $e->getMessage(); - $this->getLifecycle()->fire($event->withException($e)->withMessage($message)); - } - - /** - * @param FailEvent $failEvent - */ - public function testFail(FailEvent $failEvent) - { - $event = new TestCaseFailedEvent(); - $e = $failEvent->getFail(); - $message = $e->getMessage(); - $this->getLifecycle()->fire($event->withException($e)->withMessage($message)); - } - - /** - * @param FailEvent $failEvent - */ - public function testIncomplete(FailEvent $failEvent) - { - $event = new TestCasePendingEvent(); - $e = $failEvent->getFail(); - $message = $e->getMessage(); - $this->getLifecycle()->fire($event->withException($e)->withMessage($message)); - } - - /** - * @param FailEvent $failEvent - */ - public function testSkipped(FailEvent $failEvent) - { - $event = new TestCaseCanceledEvent(); - $e = $failEvent->getFail(); - $message = $e->getMessage(); - $this->getLifecycle()->fire($event->withException($e)->withMessage($message)); - } - - public function testEnd(TestEvent $testEvent) - { - // attachments supported since Codeception 3.0 - if (version_compare(Codecept::VERSION, '3.0.0') > -1 && $testEvent->getTest() instanceof Cest) { - $artifacts = $testEvent->getTest()->getMetadata()->getReports(); - foreach ($artifacts as $name => $artifact) { - Allure::lifecycle()->fire(new AddAttachmentEvent($artifact, $name, null)); - } - } elseif (version_compare(Codecept::VERSION, '3.0.0') > -1 && $testEvent->getTest() instanceof Gherkin) { - $artifacts = $testEvent->getTest()->getMetadata()->getReports(); - foreach ($artifacts as $name => $artifact) { - Allure::lifecycle()->fire(new AddAttachmentEvent($artifact, $name, null)); - } - } - $this->getLifecycle()->fire(new TestCaseFinishedEvent()); - } - - public function stepBefore(StepEvent $stepEvent) - { - $argumentsLength = $this->tryGetOption(ARGUMENTS_LENGTH, 200); - - $stepAction = $stepEvent->getStep()->getHumanizedActionWithoutArguments(); - $stepArgs = $stepEvent->getStep()->getArgumentsAsString($argumentsLength); - - if (!trim($stepAction)) { - $stepAction = $stepEvent->getStep()->getMetaStep()->getHumanizedActionWithoutArguments(); - $stepArgs = $stepEvent->getStep()->getMetaStep()->getArgumentsAsString($argumentsLength); - } - - $stepName = $stepAction . ' ' . $stepArgs; - - $this->emptyStep = false; - $this->getLifecycle()->fire(new StepStartedEvent($stepName)); -} - - public function stepAfter(StepEvent $stepEvent) - { - if ($stepEvent->getStep()->hasFailed()) { - $this->getLifecycle()->fire(new StepFailedEvent()); - } - $this->getLifecycle()->fire(new StepFinishedEvent()); - } - - - /** - * @return Allure - */ - public function getLifecycle() - { - if (!isset($this->lifecycle)){ - $this->lifecycle = Allure::lifecycle(); - } - return $this->lifecycle; - } - - public function setLifecycle(Allure $lifecycle) - { - $this->lifecycle = $lifecycle; - } - - /** - * - * @param \Codeception\TestInterface $test - * @return array - */ - private function getCeptAnnotations($test) - { - $tokens = token_get_all($test->getSourceCode()); - $comments = array(); - $annotations = []; - foreach($tokens as $token) { - if($token[0] == T_DOC_COMMENT || $token[0] == T_COMMENT) { - $comments[] = $token[1]; - } - } - foreach($comments as $comment) { - $lines = preg_split ('/$\R?^/m', $comment); - foreach($lines as $line) { - $output = []; - if (preg_match('/\*\s\@(.*)\((.*)\)/', $line, $output) > 0) { - if ($output[1] == "Features") { - $feature = new Features(); - $features = $this->splitAnnotationContent($output[2]); - foreach($features as $featureName) { - $feature->featureNames[] = $featureName; - } - $annotations[get_class($feature)] = $feature; - } else if ($output[1] == 'Title') { - $title = new Title(); - $title_content = str_replace('"', '', $output[2]); - $title->value = $title_content; - $annotations[get_class($title)] = $title; - } else if ($output[1] == 'Description') { - $description = new Description(); - $description_content = str_replace('"', '', $output[2]); - $description->value = $description_content; - $annotations[get_class($description)] = $description; - } else if ($output[1] == 'Stories') { - $stories = $this->splitAnnotationContent($output[2]); - $story = new Stories(); - foreach($stories as $storyName) { - $story->stories[] = $storyName; - } - $annotations[get_class($story)] = $story; - } else if ($output[1] == 'Issues') { - $issues = $this->splitAnnotationContent($output[2]); - $issue = new Issues(); - foreach($issues as $issueName) { - $issue->issueKeys[] = $issueName; - } - $annotations[get_class($issue)] = $issue; - } else { - Debug::debug("Tag not detected: ".$output[1]); - } - } - } - } - return $annotations; - } - - /** - * - * @param string $string - * @return array - */ - private function splitAnnotationContent($string) - { - $parts = []; - $detected = str_replace('{', '', $string); - $detected = str_replace('}', '', $detected); - $detected = str_replace('"', '', $detected); - $parts = explode(',', $detected); - if (count($parts) == 0 && count($detected) > 0) { - $parts[] = $detected; - } - return $parts; - } - - protected function stringifyArgument($argument) - { - if (is_string($argument)) { - return '"' . strtr($argument, ["\n" => '\n', "\r" => '\r', "\t" => ' ']) . '"'; - } elseif (is_resource($argument)) { - $argument = (string)$argument; - } elseif (is_array($argument)) { - foreach ($argument as $key => $value) { - if (is_object($value)) { - $argument[$key] = $this->getClassName($value); -} - } - } elseif (is_object($argument)) { - if (method_exists($argument, '__toString')) { - $argument = (string)$argument; - } elseif (get_class($argument) == 'Facebook\WebDriver\WebDriverBy') { - $argument = Locator::humanReadableString($argument); - } else { - $argument = $this->getClassName($argument); - } - } - - return json_encode($argument, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - } - - protected function getClassName($argument) - { - if ($argument instanceof \Closure) { - return 'Closure'; - } elseif ((isset($argument->__mocked))) { - return $this->formatClassName($argument->__mocked); - } else { - return $this->formatClassName(get_class($argument)); - } - } - - protected function formatClassName($classname) - { - return trim($classname, "\\"); - } -} diff --git a/test/codeception/AnnotationTest.php b/test/codeception/AnnotationTest.php deleted file mode 100644 index 373be55..0000000 --- a/test/codeception/AnnotationTest.php +++ /dev/null @@ -1,68 +0,0 @@ -expectNotToPerformAssertions(); - } - - /** - * @Description ("Test description with `markdown`", type = DescriptionType::MARKDOWN) - */ - public function testDescriptionAnnotation(): void - { - $this->expectNotToPerformAssertions(); - } - - /** - * @Severity (level = SeverityLevel::MINOR) - */ - public function testSeverityAnnotation(): void - { - $this->expectNotToPerformAssertions(); - } - - /** - * @Parameter (name = "foo", value = "bar", kind = ParameterKind::ARGUMENT) - */ - public function testParameterAnnotation(): void - { - $this->expectNotToPerformAssertions(); - } - - /** - * @Stories ("Story 1", "Story 2") - */ - public function testStoriesAnnotation(): void - { - $this->expectNotToPerformAssertions(); - } - - /** - * @Features ("Feature 1", "Feature 2") - */ - public function testFeaturesAnnotation(): void - { - $this->expectNotToPerformAssertions(); - } -} \ No newline at end of file diff --git a/test/codeception/_support/AcceptanceTester.php b/test/codeception/_support/AcceptanceTester.php new file mode 100644 index 0000000..30da28d --- /dev/null +++ b/test/codeception/_support/AcceptanceTester.php @@ -0,0 +1,98 @@ + + */ + private array $inputs = []; + + /** + * @var list + */ + private array $outputs = []; + + /** + * @Given I have input as :num + */ + public function iHaveInputAs($num) + { + $this->inputs = [$num]; + $this->calculate(); + } + + private function calculate(): void + { + $this->outputs = array_map( + fn (int $num): int => abs($num), + $this->inputs, + ); + } + + /** + * @Then I should get output as :num + */ + public function iShouldGetOutputAs($num) + { + Assert::assertSame([(int) $num], $this->outputs); + } + + /** + * @Given I have no input + */ + public function iHaveNoInput() + { + $this->inputs = []; + $this->calculate(); + } + + /** + * @Given I have inputs + */ + public function iHaveInputs(TableNode $table) + { + $this->inputs = array_map( + fn (array $row): int => (int) $row['num'], + iterator_to_array($table), + ); + $this->calculate(); + } + + /** + * @Then I should get non-negative outputs + */ + public function iShouldGetNonNegativeOutputs() + { + foreach ($this->outputs as $num) { + Assert::assertGreaterThanOrEqual(0, $num); + } + } +} diff --git a/test/codeception/_support/FunctionalTester.php b/test/codeception/_support/FunctionalTester.php new file mode 100644 index 0000000..c91850c --- /dev/null +++ b/test/codeception/_support/FunctionalTester.php @@ -0,0 +1,26 @@ + + Then I should get output as + + Examples: + | in | out | + | 1 | 1 | + | 2 | 2 | + + Scenario: various numbers + Given I have inputs + | num | + | -1 | + | 0 | + | 1 | + Then I should get non-negative outputs \ No newline at end of file diff --git a/test/codeception/functional.suite.yml b/test/codeception/functional.suite.yml new file mode 100644 index 0000000..ab76c9b --- /dev/null +++ b/test/codeception/functional.suite.yml @@ -0,0 +1,2 @@ + +actor: FunctionalTester \ No newline at end of file diff --git a/test/codeception/functional/BasicScenarioCept.php b/test/codeception/functional/BasicScenarioCept.php new file mode 100644 index 0000000..a4cbaaa --- /dev/null +++ b/test/codeception/functional/BasicScenarioCept.php @@ -0,0 +1,12 @@ +expect('some condition'); diff --git a/test/codeception/functional/ClassTitleCest.php b/test/codeception/functional/ClassTitleCest.php new file mode 100644 index 0000000..695d48c --- /dev/null +++ b/test/codeception/functional/ClassTitleCest.php @@ -0,0 +1,18 @@ +expect('some condition'); + } +} diff --git a/test/codeception/functional/CustomizedScenarioCept.php b/test/codeception/functional/CustomizedScenarioCept.php new file mode 100644 index 0000000..d5d273c --- /dev/null +++ b/test/codeception/functional/CustomizedScenarioCept.php @@ -0,0 +1,19 @@ +expect('some condition'); diff --git a/test/codeception/functional/NoClassTitleCest.php b/test/codeception/functional/NoClassTitleCest.php new file mode 100644 index 0000000..660a6aa --- /dev/null +++ b/test/codeception/functional/NoClassTitleCest.php @@ -0,0 +1,30 @@ +expect("some condition"); + } + + /** + * @example ["condition 1"] + * @example {"condition":"condition 2"} + */ + public function makeActionWithExamples(FunctionalTester $I, Example $example): void + { + $I->expect($example[0] ?? $example['condition']); + } +} diff --git a/test/codeception/functional/ScenarioWithLegacyAnnotationsCept.php b/test/codeception/functional/ScenarioWithLegacyAnnotationsCept.php new file mode 100644 index 0000000..2006db3 --- /dev/null +++ b/test/codeception/functional/ScenarioWithLegacyAnnotationsCept.php @@ -0,0 +1,20 @@ +expect('some condition'); diff --git a/test/codeception/_support/.gitkeep b/test/codeception/unit.suite.yml similarity index 100% rename from test/codeception/_support/.gitkeep rename to test/codeception/unit.suite.yml diff --git a/test/codeception/unit/AnnotationTest.php b/test/codeception/unit/AnnotationTest.php new file mode 100644 index 0000000..39b6f46 --- /dev/null +++ b/test/codeception/unit/AnnotationTest.php @@ -0,0 +1,50 @@ +expectNotToPerformAssertions(); + } + + #[Attribute\Description('Test description with `markdown`')] + public function testDescriptionAnnotation(): void + { + $this->expectNotToPerformAssertions(); + } + + #[Attribute\Severity(Attribute\Severity::MINOR)] + public function testSeverityAnnotation(): void + { + $this->expectNotToPerformAssertions(); + } + + #[Attribute\Parameter('foo', 'bar')] + public function testParameterAnnotation(): void + { + $this->expectNotToPerformAssertions(); + } + + #[Attribute\Story('Story 1')] + #[Attribute\Story('Story 2')] + public function testStoriesAnnotation(): void + { + $this->expectNotToPerformAssertions(); + } + + #[Attribute\Feature('Feature 1')] + #[Attribute\Feature('Feature 2')] + public function testFeaturesAnnotation(): void + { + $this->expectNotToPerformAssertions(); + } +} diff --git a/test/codeception/unit/DataProviderTest.php b/test/codeception/unit/DataProviderTest.php new file mode 100644 index 0000000..ca4518e --- /dev/null +++ b/test/codeception/unit/DataProviderTest.php @@ -0,0 +1,38 @@ + ['foo', 'foo'], + 'a' => ['bar', 'bar'], + 'b' => ['foo', 'bar'], + ]; + } +} diff --git a/test/codeception/StepsTest.php b/test/codeception/unit/StepsTest.php similarity index 89% rename from test/codeception/StepsTest.php rename to test/codeception/unit/StepsTest.php index 88f47d1..7f3dbb7 100644 --- a/test/codeception/StepsTest.php +++ b/test/codeception/unit/StepsTest.php @@ -2,14 +2,12 @@ declare(strict_types=1); -namespace Yandex\Allure\Codeception; +namespace Qameta\Allure\Codeception\Test\Unit; use Codeception\Lib\ModuleContainer; use Codeception\Scenario; -use Codeception\Step\Assertion; use Codeception\Step\Comment; use Codeception\Step\Meta; -use Codeception\Step\TryTo; use Codeception\Test\Unit; use Exception; use PHPUnit\Framework\Assert; @@ -47,6 +45,13 @@ public function testSingleSuccessfulStepWithTitle(): void $scenario->runStep(new Comment('Step 1 name')); } + public function testSingleSuccessfulStepWithArguments(): void + { + $this->expectNotToPerformAssertions(); + $scenario = new Scenario($this); + $scenario->runStep(new Comment('Step 1 name', ['foo' => 'bar'])); + } + public function testTwoSuccessfulSteps(): void { $this->expectNotToPerformAssertions(); diff --git a/test/report/ReportTest.php b/test/report/ReportTest.php index 9e4f02f..15a2c9c 100644 --- a/test/report/ReportTest.php +++ b/test/report/ReportTest.php @@ -2,17 +2,24 @@ declare(strict_types=1); -namespace Yandex\Allure\Codeception; +namespace Qameta\Allure\Codeception\Test; -use DOMDocument; -use DOMXPath; use PHPUnit\Framework\TestCase; +use Qameta\Allure\Codeception\Test\Unit\AnnotationTest; +use Qameta\Allure\Codeception\Test\Unit\StepsTest; +use Remorhaz\JSON\Data\Value\EncodedJson\NodeValueFactory; +use Remorhaz\JSON\Data\Value\NodeValueInterface; +use Remorhaz\JSON\Path\Processor\Processor; +use Remorhaz\JSON\Path\Processor\ProcessorInterface; +use Remorhaz\JSON\Path\Query\QueryFactory; +use Remorhaz\JSON\Path\Query\QueryFactoryInterface; use RuntimeException; +use function file_get_contents; use function is_file; use function pathinfo; use function scandir; -use function sprintf; +use function str_ends_with; use const DIRECTORY_SEPARATOR; use const PATHINFO_EXTENSION; @@ -21,243 +28,312 @@ class ReportTest extends TestCase { /** - * @var string + * @var array> */ - private $buildPath; + private static array $testResults = []; - /** - * @var DOMXPath[] - */ - private $sources = []; + private ?ProcessorInterface $jsonPathProcessor = null; + + private ?QueryFactoryInterface $jsonPathQueryFactory = null; - public function setUp(): void + public static function setUpBeforeClass(): void { - $this->buildPath = __DIR__ . '/../../build/allure-results'; - $files = scandir($this->buildPath); + $buildPath = __DIR__ . '/../../build/allure-results'; + $files = scandir($buildPath); + + $jsonValueFactory = NodeValueFactory::create(); + $jsonPathProcessor = Processor::create(); + $jsonPathQueryFactory = QueryFactory::create(); + $testMethodsQuery = $jsonPathQueryFactory + ->createQuery('$.labels[?(@.name=="testMethod")].value'); + $testClassesQuery = $jsonPathQueryFactory + ->createQuery('$.labels[?(@.name=="testClass")].value'); foreach ($files as $fileName) { - $file = $this->buildPath . DIRECTORY_SEPARATOR . $fileName; + $file = $buildPath . DIRECTORY_SEPARATOR . $fileName; if (!is_file($file)) { continue; } $extension = pathinfo($file, PATHINFO_EXTENSION); - if ('xml' == $extension) { - $dom = new DOMDocument(); - $dom->load($file); - - $path = new DOMXPath($dom); - $name = $path->query('/alr:test-suite/name')->item(0)->textContent; - if (isset($this->sources[$name])) { - throw new RuntimeException("Duplicate test suite: {$name}"); + if ('json' == $extension) { + $fileName = pathinfo($file, PATHINFO_FILENAME); + if (!str_ends_with($fileName, '-result')) { + continue; + } + $fileContent = file_get_contents($file); + $data = $jsonValueFactory->createValue($fileContent); + /** @var mixed $class */ + $class = $jsonPathProcessor + ->select($testClassesQuery, $data) + ->decode()[0] ?? null; + /** @var mixed $method */ + $method = $jsonPathProcessor + ->select($testMethodsQuery, $data) + ->decode()[0] ?? null; + if (!isset($class, $method)) { + throw new RuntimeException("Test not found in file $file"); } - $this->sources[$name] = $path; + self::assertIsString($class); + self::assertIsString($method); + self::$testResults[$class][$method] = $data; } } } /** * @param string $class - * @param string $xpath + * @param string $method + * @param string $jsonPath * @param string $expectedValue - * @dataProvider providerSingleTextNode + * @dataProvider providerSingleNodeValueStartsFromString */ - public function testSingleTextNode(string $class, string $xpath, string $expectedValue): void + public function testSingleNodeValueStartsFromString( + string $class, + string $method, + string $jsonPath, + string $expectedValue + ): void { + /** @psalm-var mixed $nodes */ + $nodes = $this + ->getJsonPathProcessor() + ->select( + $this->getJsonPathQueryFactory()->createQuery($jsonPath), + self::$testResults[$class][$method] + ?? throw new RuntimeException("Result not found for $class::$method"), + ) + ->decode(); + self::assertIsArray($nodes); + self::assertCount(1, $nodes); + $value = $nodes[0] ?? null; + self::assertIsString($value); + self::assertStringStartsWith($expectedValue, $value); + } + + /** + * @return iterable + */ + public function providerSingleNodeValueStartsFromString(): iterable { - self::assertArrayHasKey($class, $this->sources); - $actualValue = $this - ->sources[$class] - ->query($xpath) - ->item(0) - ->textContent; - self::assertSame($expectedValue, $actualValue); + return [ + 'Error message in test case without steps' => [ + StepsTest::class, + 'testNoStepsError', + '$.statusDetails.message', + "Error\nException(0)", + ], + ]; + } + + /** + * @dataProvider providerExistingNodeValue + */ + public function testExistingNodeValue( + string $class, + string $method, + string $jsonPath, + array $expected + ): void { + $nodes = $this + ->getJsonPathProcessor() + ->select( + $this->getJsonPathQueryFactory()->createQuery($jsonPath), + self::$testResults[$class][$method] + ?? throw new RuntimeException("Result not found for $class::$method"), + ) + ->decode(); + self::assertSame($expected, $nodes); } - public function providerSingleTextNode(): iterable + /** + * @return iterable}> + */ + public function providerExistingNodeValue(): iterable { return [ 'Test case title annotation' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testTitleAnnotation', - '/title' - ), - 'Test title', + AnnotationTest::class, + 'testTitleAnnotation', + '$.name', + ['Test title'], ], 'Test case severity annotation' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testSeverityAnnotation', - '/labels/label[@name="severity" and @value="minor"]' - ), - '', + AnnotationTest::class, + 'testSeverityAnnotation', + '$.labels[?(@.name=="severity")].value', + ['minor'], ], 'Test case parameter annotation' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testParameterAnnotation', - '/parameters/parameter[@name="foo" and @value="bar" and @kind="argument"]' - ), - '', - ], - 'Test case stories annotation: first story' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testStoriesAnnotation', - '/labels/label[@name="story" and @value="Story 1"]' - ), - '', - ], - 'Test case stories annotation: second story' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testStoriesAnnotation', - '/labels/label[@name="story" and @value="Story 2"]' - ), - '', + AnnotationTest::class, + 'testParameterAnnotation', + '$.parameters[?(@.name=="foo")].value', + ['bar'], ], - 'Test case features annotation: first feature' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testFeaturesAnnotation', - '/labels/label[@name="feature" and @value="Feature 1"]' - ), - '', + 'Test case stories annotation' => [ + AnnotationTest::class, + 'testStoriesAnnotation', + '$.labels[?(@.name=="story")].value', + ['Story 1', 'Story 2'], ], - 'Test case features annotation: second feature' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testFeaturesAnnotation', - '/labels/label[@name="feature" and @value="Feature 2"]' - ), - '', + 'Test case features annotation' => [ + AnnotationTest::class, + 'testFeaturesAnnotation', + '$.labels[?(@.name=="feature")].value', + ['Feature 1', 'Feature 2'], ], 'Successful test case without steps' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testNoStepsSuccess', - '[@status="passed"]/name' - ), + StepsTest::class, + 'testNoStepsSuccess', + '$.status', + ['passed'], + ], + 'Successful test case without steps: no steps' => [ + StepsTest::class, 'testNoStepsSuccess', + '$.steps[*]', + [], ], 'Error in test case without steps' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testNoStepsError', - '[@status="broken"]/failure/message' - ), - 'Error', + StepsTest::class, + 'testNoStepsError', + '$.status', + ['broken'], ], - 'Failure in test case without steps' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testNoStepsFailure', - '[@status="failed"]/failure/message' - ), - 'Failure', + 'Failure message in test case without steps' => [ + StepsTest::class, + 'testNoStepsFailure', + '$.statusDetails.message', + ['Failure'], ], 'Test case without steps skipped' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testNoStepsSkipped', - '[@status="canceled"]/failure/message' - ), - 'Skipped', + StepsTest::class, + 'testNoStepsSkipped', + '$.status', + ['skipped'], ], - 'Successful test case with single step: name' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testSingleSuccessfulStepWithTitle', - '[@status="passed"]/steps/step[1][@status="passed"]/name' - ), - 'step 1 name ', // Codeception processes action internally + 'Skipped message in test case without steps' => [ + StepsTest::class, + 'testNoStepsSkipped', + '$.statusDetails.message', + ['Skipped'], ], - 'Successful test case with two successful steps: step 2 name' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testTwoSuccessfulSteps', - '[@status="passed"]/steps/step[2][@status="passed"]/name' - ), - 'step 2 name ', // Codeception processes action internally + 'Successful test case with single step: status' => [ + StepsTest::class, + 'testSingleSuccessfulStepWithTitle', + '$.status', + ['passed'], ], - 'First step in test case with two steps fails: failure' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testTwoStepsFirstFails', - '[@status="failed"]/failure/message' - ), - 'Failure', + 'Successful test case with single step: step status' => [ + StepsTest::class, + 'testSingleSuccessfulStepWithTitle', + '$.steps[*].status', + ['passed'], ], - 'First step in test case with two steps fails: step 1 name' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testTwoStepsFirstFails', - '[@status="failed"]/steps/step[1][@status="failed"]/name' - ), - 'step 1 name ', // Codeception processes action internally + 'Successful test case with single step: step name' => [ + StepsTest::class, + 'testSingleSuccessfulStepWithTitle', + '$.steps[*].name', + ['step 1 name'], ], - 'Second step in test case with two steps fails: failure' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testTwoStepsSecondFails', - '[@status="failed"]/failure/message' - ), - 'Failure', + 'Successful test case with arguments in step: status' => [ + StepsTest::class, + 'testSingleSuccessfulStepWithArguments', + '$.status', + ['passed'], ], - 'Second step in test case with two steps fails: step 1 name' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testTwoStepsSecondFails', - '[@status="failed"]/steps/step[1][@status="passed"]/name' - ), - 'step 1 name ', // Codeception processes action internally + 'Successful test case with arguments in step: step status' => [ + StepsTest::class, + 'testSingleSuccessfulStepWithArguments', + '$.steps[*].status', + ['passed'], ], - 'Second step in test case with two steps fails: step 2 name' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testTwoStepsSecondFails', - '[@status="failed"]/steps/step[2][@status="failed"]/name' - ), - 'step 2 name ', // Codeception processes action internally + 'Successful test case with arguments in step: step name' => [ + StepsTest::class, + 'testSingleSuccessfulStepWithArguments', + '$.steps[*].name', + ['step 1 name'], + ], + 'Successful test case with arguments in step: step parameter' => [ + StepsTest::class, + 'testSingleSuccessfulStepWithArguments', + '$.steps[*].parameters[?(@.name=="foo")].value', + ['"bar"'], + ], + 'Successful test case with two successful steps: status' => [ + StepsTest::class, + 'testTwoSuccessfulSteps', + '$.status', + ['passed'], + ], + 'Successful test case with two successful steps: step status' => [ + StepsTest::class, + 'testTwoSuccessfulSteps', + '$.steps[*].status', + ['passed', 'passed'], + ], + 'Successful test case with two successful steps: step name' => [ + StepsTest::class, + 'testTwoSuccessfulSteps', + '$.steps[*].name', + ['step 1 name', 'step 2 name'], + ], + 'First step in test case with two steps fails: status' => [ + StepsTest::class, + 'testTwoStepsFirstFails', + '$.status', + ['failed'], + ], + 'First step in test case with two steps fails: message' => [ + StepsTest::class, + 'testTwoStepsFirstFails', + '$.statusDetails.message', + ['Failure'], + ], + 'First step in test case with two steps fails: step status' => [ + StepsTest::class, + 'testTwoStepsFirstFails', + '$.steps[*].status', + ['failed'], + ], + 'First step in test case with two steps fails: step name' => [ + StepsTest::class, + 'testTwoStepsFirstFails', + '$.steps[*].name', + ['step 1 name'], + ], + 'Second step in test case with two steps fails: status' => [ + StepsTest::class, + 'testTwoStepsSecondFails', + '$.status', + ['failed'], + ], + 'Second step in test case with two steps fails: message' => [ + StepsTest::class, + 'testTwoStepsSecondFails', + '$.statusDetails.message', + ['Failure'], + ], + 'Second step in test case with two steps fails: step status' => [ + StepsTest::class, + 'testTwoStepsSecondFails', + '$.steps[*].status', + ['passed', 'failed'], + ], + 'Second step in test case with two steps fails: step name' => [ + StepsTest::class, + 'testTwoStepsSecondFails', + '$.steps[*].name', + ['step 1 name', 'step 2 name'], ], ]; } - /** - * @param string $class - * @param string $xpath - * @dataProvider providerNodeNotExists - */ - public function testNodeNotExists(string $class, string $xpath): void - { - $testNode = $this - ->sources[$class] - ->query($xpath) - ->item(0); - self::assertNull($testNode); - } - - public function providerNodeNotExists(): iterable + private function getJsonPathProcessor(): ProcessorInterface { - return [ - 'Successful test case without steps: no steps' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testNoStepsSuccess', - '/steps' - ) - ], - 'First step fails in test case with two steps: no second step' => [ - 'Yandex\Allure\Codeception.unit', - $this->buildTestXPath( - 'testTwoStepsFirstFails', - '/steps/step[2]' - ) - ], - ]; + return $this->jsonPathProcessor ??= Processor::create(); } - private function buildTestXPath(string $testName, string $tail): string + private function getJsonPathQueryFactory(): QueryFactoryInterface { - return sprintf('/alr:test-suite/test-cases/test-case[name="%s"]%s', $testName, $tail); + return $this->jsonPathQueryFactory ??= QueryFactory::create(); } } From 8c6b9269f5d5a56e7a345e9139d0b805eae21def Mon Sep 17 00:00:00 2001 From: Edward Surov Date: Tue, 19 Oct 2021 20:41:44 +0300 Subject: [PATCH 02/10] Tuning pipeline --- .gitattributes | 25 +++++++++++++++++++++++++ .github/workflows/build.yml | 2 +- composer.json | 8 ++++++-- 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b2ea9a7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,25 @@ +# Define the line ending behavior of the different file extensions +# Set default behavior, in case users don't have core.autocrlf set. +* text text=auto eol=lf + +.php diff=php + +# Declare files that will always have CRLF line endings on checkout. +*.bat eol=crlf + +# Declare files that will always have LF line endings on checkout. +*.pem eol=lf + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary +*.gif binary +*.ico binary +*.mo binary +*.pdf binary +*.phar binary +*.woff binary +*.woff2 binary +*.ttf binary +*.otf binary +*.eot binary diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4af2d46..2a9d69c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,7 +37,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - extensions: pcntl, posix + extensions: pcntl, posix, intl coverage: xdebug ini-values: error_reporting=E_ALL diff --git a/composer.json b/composer.json index 2051574..f252a86 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "require": { "php": "^8", "ext-json": "*", - "codeception/codeception": "^4", + "codeception/codeception": "^4.1", "allure-framework/allure-php-commons": "2.0.0-rc3" }, "require-dev": { @@ -32,7 +32,11 @@ "psalm/plugin-phpunit": "^0.16.1", "remorhaz/php-json-data": "^0.5.3", "remorhaz/php-json-path": "^0.7.7", - "squizlabs/php_codesniffer": "^3.6.1" + "squizlabs/php_codesniffer": "^3.6.1", + "vimeo/psalm": "^4.10" + }, + "conflict": { + "codeception/phpunit-wrapper": "<9.0.1" }, "autoload": { "psr-4": { From f534d3063fdf0f619b2e9739e303b6a1b8ae48f6 Mon Sep 17 00:00:00 2001 From: Edward Surov Date: Tue, 15 Feb 2022 17:25:34 +0200 Subject: [PATCH 03/10] Commons 2.0.0 supported --- composer.json | 4 +-- src/AllureCodeception.php | 17 ++++------- src/Internal/ArgumentAsString.php | 5 ++-- src/Internal/CeptInfoBuilder.php | 1 - src/Internal/CeptProvider.php | 30 ++++++++----------- src/Internal/CestInfoBuilder.php | 1 - src/Internal/CestProvider.php | 22 ++++++-------- src/Internal/DefaultThreadDetector.php | 1 - src/Internal/GherkinInfoBuilder.php | 1 - src/Internal/GherkinProvider.php | 14 ++++----- src/Internal/StepStartInfo.php | 1 - src/Internal/SuiteInfo.php | 1 - src/Internal/SuiteProvider.php | 19 ++++++------ src/Internal/TestInfo.php | 1 - src/Internal/TestInfoBuilderInterface.php | 1 - src/Internal/TestInfoProvider.php | 14 ++++----- src/Internal/TestLifecycle.php | 17 +++++------ src/Internal/TestLifecycleInterface.php | 1 - src/Internal/TestStartInfo.php | 1 - src/Internal/UnitInfoBuilder.php | 1 - src/Internal/UnitProvider.php | 29 ++++++++---------- src/Internal/UnknownInfoBuilder.php | 1 - src/Setup/ThreadDetectorInterface.php | 1 - src/StatusDetector.php | 1 - .../codeception/functional/ClassTitleCest.php | 1 - .../functional/NoClassTitleCest.php | 1 - test/codeception/unit/AnnotationTest.php | 1 - test/codeception/unit/DataProviderTest.php | 4 ++- test/codeception/unit/StepsTest.php | 2 -- test/report/ReportTest.php | 1 - 30 files changed, 74 insertions(+), 121 deletions(-) diff --git a/composer.json b/composer.json index f252a86..b836150 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "php": "^8", "ext-json": "*", "codeception/codeception": "^4.1", - "allure-framework/allure-php-commons": "2.0.0-rc3" + "allure-framework/allure-php-commons": "^2" }, "require-dev": { "ext-dom": "*", @@ -33,7 +33,7 @@ "remorhaz/php-json-data": "^0.5.3", "remorhaz/php-json-path": "^0.7.7", "squizlabs/php_codesniffer": "^3.6.1", - "vimeo/psalm": "^4.10" + "vimeo/psalm": "^4.16.1" }, "conflict": { "codeception/phpunit-wrapper": "<9.0.1" diff --git a/src/AllureCodeception.php b/src/AllureCodeception.php index d603690..ecbfec4 100644 --- a/src/AllureCodeception.php +++ b/src/AllureCodeception.php @@ -15,7 +15,6 @@ use Codeception\Step; use Qameta\Allure\Allure; use Qameta\Allure\Allure as QametaAllure; -use Qameta\Allure\Attribute\LinkTemplateInterface; use Qameta\Allure\Codeception\Internal\DefaultThreadDetector; use Qameta\Allure\Codeception\Internal\SuiteInfo; use Qameta\Allure\Codeception\Internal\TestLifecycle; @@ -35,7 +34,6 @@ final class AllureCodeception extends Extension { - private const SETUP_HOOK_PARAMETER = 'setupHook'; private const OUTPUT_DIRECTORY_PARAMETER = 'outputDirectory'; @@ -55,11 +53,6 @@ final class AllureCodeception extends Extension Events::STEP_AFTER => 'stepAfter' ]; - /** - * @var array - */ - private array $linkTemplates = []; - private ?ThreadDetectorInterface $threadDetector = null; private ?TestLifecycleInterface $testLifecycle = null; @@ -76,9 +69,9 @@ public function _initialize(): void parent::_initialize(); QametaAllure::reset(); QametaAllure::getLifecycleConfigurator() - ->setStatusDetector(new StatusDetector(new DefaultStatusDetector())); + ->setStatusDetector(new StatusDetector(new DefaultStatusDetector())) + ->setOutputDirectory($this->getOutputDirectory()); $this->callSetupHook(); - QametaAllure::setOutputDirectory($this->getOutputDirectory()); } private function callSetupHook(): void @@ -270,10 +263,10 @@ private function getTestLifecycle(): TestLifecycleInterface { return $this->testLifecycle ??= new TestLifecycle( Allure::getLifecycle(), - Allure::getResultFactory(), - Allure::getStatusDetector(), + Allure::getConfig()->getResultFactory(), + Allure::getConfig()->getStatusDetector(), $this->getThreadDetector(), - $this->linkTemplates, + Allure::getConfig()->getLinkTemplates(), ); } } diff --git a/src/Internal/ArgumentAsString.php b/src/Internal/ArgumentAsString.php index bfaed8e..4667065 100644 --- a/src/Internal/ArgumentAsString.php +++ b/src/Internal/ArgumentAsString.php @@ -8,6 +8,7 @@ use Stringable; use function array_map; +use function class_exists; use function is_a; use function is_array; use function is_object; @@ -22,7 +23,6 @@ */ final class ArgumentAsString implements Stringable { - public function __construct( private mixed $argument, ) { @@ -75,7 +75,8 @@ private function prepareObject(object $argument): string return (string) $argument; } - if (is_a($argument, 'Facebook\WebDriver\WebDriverBy')) { + $webdriverByClass = '\Facebook\WebDriver\WebDriverBy'; + if (class_exists($webdriverByClass) && is_a($argument, $webdriverByClass)) { return Locator::humanReadableString($argument); } diff --git a/src/Internal/CeptInfoBuilder.php b/src/Internal/CeptInfoBuilder.php index 8ad377d..bd8876f 100644 --- a/src/Internal/CeptInfoBuilder.php +++ b/src/Internal/CeptInfoBuilder.php @@ -8,7 +8,6 @@ final class CeptInfoBuilder implements TestInfoBuilderInterface { - public function __construct( private Cept $test, ) { diff --git a/src/Internal/CeptProvider.php b/src/Internal/CeptProvider.php index a436da9..c33ea46 100644 --- a/src/Internal/CeptProvider.php +++ b/src/Internal/CeptProvider.php @@ -5,7 +5,7 @@ namespace Qameta\Allure\Codeception\Internal; use Codeception\Test\Cept; -use Qameta\Allure\Attribute\LinkTemplateInterface; +use Qameta\Allure\Setup\LinkTemplateCollectionInterface; use Qameta\Allure\Model\Label; use Qameta\Allure\Model\Link; use Qameta\Allure\Model\LinkType; @@ -27,7 +27,6 @@ */ final class CeptProvider implements ModelProviderInterface { - private bool $isLoaded = false; /** @@ -45,21 +44,21 @@ final class CeptProvider implements ModelProviderInterface private ?string $legacyDescription = null; /** - * @param Cept $test - * @param array $linkTemplates + * @param Cept $test + * @param LinkTemplateCollectionInterface $linkTemplates */ public function __construct( private Cept $test, - private array $linkTemplates = [], + private LinkTemplateCollectionInterface $linkTemplates, ) { } /** - * @param Cept $test - * @param array $linkTemplates + * @param Cept $test + * @param LinkTemplateCollectionInterface $linkTemplates * @return list */ - public static function createForChain(Cept $test, array $linkTemplates): array + public static function createForChain(Cept $test, LinkTemplateCollectionInterface $linkTemplates): array { return [new self($test, $linkTemplates)]; } @@ -83,14 +82,6 @@ public function getParameters(): array return []; } - /** - * @deprecated Please use {@see getDisplayName()} method - */ - public function getTitle(): ?string - { - return $this->getDisplayName(); - } - public function getDisplayName(): ?string { $this->loadLegacyModels(); @@ -107,6 +98,11 @@ public function getDisplayName(): ?string : null; } + public function getFullName(): ?string + { + return (string) $this->test->getSignature(); + } + public function getDescription(): ?string { $this->loadLegacyModels(); @@ -179,7 +175,7 @@ private function loadLegacyModels(): void $this->getLegacyAnnotations('Stories'), ), ]; - $linkTemplate = $this->linkTemplates[(string) LinkType::issue()] ?? null; + $linkTemplate = $this->linkTemplates->get(LinkType::issue()) ?? null; $this->legacyLinks = array_map( fn (string $value): Link => Link::issue($value, $linkTemplate?->buildUrl($value)), $this->getLegacyAnnotations('Issues'), diff --git a/src/Internal/CestInfoBuilder.php b/src/Internal/CestInfoBuilder.php index 4fa8cdf..1187040 100644 --- a/src/Internal/CestInfoBuilder.php +++ b/src/Internal/CestInfoBuilder.php @@ -12,7 +12,6 @@ final class CestInfoBuilder implements TestInfoBuilderInterface { - public function __construct( private Cest $test, ) { diff --git a/src/Internal/CestProvider.php b/src/Internal/CestProvider.php index 6f52f67..9ff8019 100644 --- a/src/Internal/CestProvider.php +++ b/src/Internal/CestProvider.php @@ -6,7 +6,7 @@ use Codeception\Test\Cest; use Qameta\Allure\Attribute\AttributeParser; -use Qameta\Allure\Attribute\LinkTemplateInterface; +use Qameta\Allure\Setup\LinkTemplateCollectionInterface; use Qameta\Allure\Model\ModelProviderInterface; use Qameta\Allure\Model\Parameter; use ReflectionException; @@ -24,19 +24,18 @@ */ final class CestProvider implements ModelProviderInterface { - public function __construct( private Cest $test, ) { } /** - * @param Cest $test - * @param array $linkTemplates + * @param Cest $test + * @param LinkTemplateCollectionInterface $linkTemplates * @return list * @throws ReflectionException */ - public static function createForChain(Cest $test, array $linkTemplates = []): array + public static function createForChain(Cest $test, LinkTemplateCollectionInterface $linkTemplates): array { /** @var mixed $testClass */ $testClass = $test->getTestClass(); @@ -86,14 +85,6 @@ public function getParameters(): array ); } - /** - * @deprecated Please, use {@see getDisplayName()} method - */ - public function getTitle(): ?string - { - return $this->getDisplayName(); - } - public function getDisplayName(): ?string { /** @psalm-var mixed $displayName */ @@ -113,4 +104,9 @@ public function getDescriptionHtml(): ?string { return null; } + + public function getFullName(): ?string + { + return null; + } } diff --git a/src/Internal/DefaultThreadDetector.php b/src/Internal/DefaultThreadDetector.php index a926dba..06bbd63 100644 --- a/src/Internal/DefaultThreadDetector.php +++ b/src/Internal/DefaultThreadDetector.php @@ -10,7 +10,6 @@ final class DefaultThreadDetector implements ThreadDetectorInterface { - private string|false|null $host = null; public function getHost(): ?string diff --git a/src/Internal/GherkinInfoBuilder.php b/src/Internal/GherkinInfoBuilder.php index af74a4c..d56d3c6 100644 --- a/src/Internal/GherkinInfoBuilder.php +++ b/src/Internal/GherkinInfoBuilder.php @@ -10,7 +10,6 @@ final class GherkinInfoBuilder implements TestInfoBuilderInterface { - public function __construct( private Gherkin $test, ) { diff --git a/src/Internal/GherkinProvider.php b/src/Internal/GherkinProvider.php index a831c92..66fdd8d 100644 --- a/src/Internal/GherkinProvider.php +++ b/src/Internal/GherkinProvider.php @@ -16,7 +16,6 @@ */ final class GherkinProvider implements ModelProviderInterface { - public function __construct( private Gherkin $test, ) { @@ -48,14 +47,6 @@ public function getParameters(): array return []; } - /** - * @deprecated Please use {@see getDisplayName()} method - */ - public function getTitle(): ?string - { - return $this->getDisplayName(); - } - public function getDisplayName(): ?string { return (string) $this->test->toString(); @@ -70,4 +61,9 @@ public function getDescriptionHtml(): ?string { return null; } + + public function getFullName(): ?string + { + return null; + } } diff --git a/src/Internal/StepStartInfo.php b/src/Internal/StepStartInfo.php index 099afe3..fb3f016 100644 --- a/src/Internal/StepStartInfo.php +++ b/src/Internal/StepStartInfo.php @@ -8,7 +8,6 @@ final class StepStartInfo { - public function __construct( private Step $originalStep, private string $uuid, diff --git a/src/Internal/SuiteInfo.php b/src/Internal/SuiteInfo.php index 2c085fd..5ac916d 100644 --- a/src/Internal/SuiteInfo.php +++ b/src/Internal/SuiteInfo.php @@ -11,7 +11,6 @@ */ final class SuiteInfo { - public function __construct( private string $name, ) { diff --git a/src/Internal/SuiteProvider.php b/src/Internal/SuiteProvider.php index ea85d6e..5b6a01f 100644 --- a/src/Internal/SuiteProvider.php +++ b/src/Internal/SuiteProvider.php @@ -5,7 +5,7 @@ namespace Qameta\Allure\Codeception\Internal; use Qameta\Allure\Attribute\AttributeParser; -use Qameta\Allure\Attribute\LinkTemplateInterface; +use Qameta\Allure\Setup\LinkTemplateCollectionInterface; use Qameta\Allure\Model\Label; use Qameta\Allure\Model\ModelProviderInterface; use ReflectionException; @@ -15,21 +15,20 @@ */ final class SuiteProvider implements ModelProviderInterface { - public function __construct( private ?SuiteInfo $suiteInfo, ) { } /** - * @param SuiteInfo|null $suiteInfo - * @param array $linkTemplates + * @param SuiteInfo|null $suiteInfo + * @param LinkTemplateCollectionInterface $linkTemplates * @return list * @throws ReflectionException */ public static function createForChain( ?SuiteInfo $suiteInfo, - array $linkTemplates, + LinkTemplateCollectionInterface $linkTemplates, ): array { $providers = [new self($suiteInfo)]; $suiteClass = $suiteInfo?->getClass(); @@ -62,11 +61,6 @@ public function getParameters(): array return []; } - public function getTitle(): ?string - { - return $this->getDisplayName(); - } - public function getDisplayName(): ?string { return null; @@ -81,4 +75,9 @@ public function getDescriptionHtml(): ?string { return null; } + + public function getFullName(): ?string + { + return null; + } } diff --git a/src/Internal/TestInfo.php b/src/Internal/TestInfo.php index 8ceecfd..2c86a98 100644 --- a/src/Internal/TestInfo.php +++ b/src/Internal/TestInfo.php @@ -6,7 +6,6 @@ final class TestInfo { - public function __construct( private object $originalTest, private string $signature, diff --git a/src/Internal/TestInfoBuilderInterface.php b/src/Internal/TestInfoBuilderInterface.php index cb29ee3..4b714aa 100644 --- a/src/Internal/TestInfoBuilderInterface.php +++ b/src/Internal/TestInfoBuilderInterface.php @@ -6,6 +6,5 @@ interface TestInfoBuilderInterface { - public function build(?string $host, ?string $thread): TestInfo; } diff --git a/src/Internal/TestInfoProvider.php b/src/Internal/TestInfoProvider.php index f112945..b813c10 100644 --- a/src/Internal/TestInfoProvider.php +++ b/src/Internal/TestInfoProvider.php @@ -9,7 +9,6 @@ final class TestInfoProvider implements ModelProviderInterface { - public function __construct( private TestInfo $info, ) { @@ -44,14 +43,6 @@ public function getParameters(): array return []; } - /** - * @deprecated Please use {@see getDisplayName()} method - */ - public function getTitle(): ?string - { - return $this->getDisplayName(); - } - public function getDisplayName(): ?string { return null; @@ -66,4 +57,9 @@ public function getDescriptionHtml(): ?string { return null; } + + public function getFullName(): ?string + { + return null; + } } diff --git a/src/Internal/TestLifecycle.php b/src/Internal/TestLifecycle.php index 32eea66..7ee466c 100644 --- a/src/Internal/TestLifecycle.php +++ b/src/Internal/TestLifecycle.php @@ -11,7 +11,6 @@ use Codeception\TestInterface; use PHPUnit\Framework\TestCase; use Qameta\Allure\AllureLifecycleInterface; -use Qameta\Allure\Attribute\LinkTemplateInterface; use Qameta\Allure\Codeception\Setup\ThreadDetectorInterface; use Qameta\Allure\Io\DataSourceFactory; use Qameta\Allure\Model\ModelProviderChain; @@ -21,6 +20,7 @@ use Qameta\Allure\Model\StatusDetails; use Qameta\Allure\Model\StepResult; use Qameta\Allure\Model\TestResult; +use Qameta\Allure\Setup\LinkTemplateCollectionInterface; use Qameta\Allure\Setup\StatusDetectorInterface; use RuntimeException; use Throwable; @@ -33,7 +33,6 @@ final class TestLifecycle implements TestLifecycleInterface { - private ?SuiteInfo $currentSuite = null; private ?TestInfo $currentTest = null; @@ -48,18 +47,18 @@ final class TestLifecycle implements TestLifecycleInterface private WeakMap $stepStarts; /** - * @param AllureLifecycleInterface $lifecycle - * @param ResultFactoryInterface $resultFactory - * @param StatusDetectorInterface $statusDetector - * @param ThreadDetectorInterface $threadDetector - * @param array $linkTemplates + * @param AllureLifecycleInterface $lifecycle + * @param ResultFactoryInterface $resultFactory + * @param StatusDetectorInterface $statusDetector + * @param ThreadDetectorInterface $threadDetector + * @param LinkTemplateCollectionInterface $linkTemplates */ public function __construct( private AllureLifecycleInterface $lifecycle, private ResultFactoryInterface $resultFactory, private StatusDetectorInterface $statusDetector, private ThreadDetectorInterface $threadDetector, - private array $linkTemplates = [], + private LinkTemplateCollectionInterface $linkTemplates, ) { /** @var WeakMap */ $this->stepStarts = new WeakMap(); @@ -151,7 +150,7 @@ public function updateTest(): self $this->lifecycle->updateTest( fn (TestResult $t) => $t ->setName($provider->getDisplayName()) - ->setFullName($this->getCurrentTest()->getSignature()) + ->setFullName($provider->getFullName()) ->setDescription($provider->getDescription()) ->setDescriptionHtml($provider->getDescriptionHtml()) ->addLinks(...$provider->getLinks()) diff --git a/src/Internal/TestLifecycleInterface.php b/src/Internal/TestLifecycleInterface.php index 9e61ed8..6d4d8f5 100644 --- a/src/Internal/TestLifecycleInterface.php +++ b/src/Internal/TestLifecycleInterface.php @@ -11,7 +11,6 @@ interface TestLifecycleInterface { - public function switchToSuite(SuiteInfo $suiteInfo): TestLifecycleInterface; public function resetSuite(): TestLifecycleInterface; diff --git a/src/Internal/TestStartInfo.php b/src/Internal/TestStartInfo.php index 148a9bc..a5d9cbc 100644 --- a/src/Internal/TestStartInfo.php +++ b/src/Internal/TestStartInfo.php @@ -6,7 +6,6 @@ final class TestStartInfo { - public function __construct( private string $containerUuid, private string $testUuid, diff --git a/src/Internal/UnitInfoBuilder.php b/src/Internal/UnitInfoBuilder.php index e0593c7..0009a4f 100644 --- a/src/Internal/UnitInfoBuilder.php +++ b/src/Internal/UnitInfoBuilder.php @@ -10,7 +10,6 @@ final class UnitInfoBuilder implements TestInfoBuilderInterface { - public function __construct( private TestCase $test, ) { diff --git a/src/Internal/UnitProvider.php b/src/Internal/UnitProvider.php index d43e583..487da5c 100644 --- a/src/Internal/UnitProvider.php +++ b/src/Internal/UnitProvider.php @@ -6,8 +6,7 @@ use PHPUnit\Framework\TestCase; use Qameta\Allure\Attribute\AttributeParser; -use Qameta\Allure\Attribute\LinkTemplateInterface; -use Qameta\Allure\Model\Label; +use Qameta\Allure\Setup\LinkTemplateCollectionInterface; use Qameta\Allure\Model\ModelProviderInterface; use Qameta\Allure\Model\Parameter; use ReflectionException; @@ -21,24 +20,23 @@ */ final class UnitProvider implements ModelProviderInterface { - /** - * @param TestCase $test - * @param array $linkTemplates + * @param TestCase $test + * @param LinkTemplateCollectionInterface $linkTemplates */ public function __construct( private TestCase $test, - array $linkTemplates = [], + LinkTemplateCollectionInterface $linkTemplates, ) { } /** - * @param TestCase $test - * @param array $linkTemplates + * @param TestCase $test + * @param LinkTemplateCollectionInterface $linkTemplates * @throws ReflectionException * @return list */ - public static function createForChain(TestCase $test, array $linkTemplates = []): array + public static function createForChain(TestCase $test, LinkTemplateCollectionInterface $linkTemplates): array { /** * @var callable-string|null $methodOrFunction @@ -100,14 +98,6 @@ public function getParameters(): array return $params; } - /** - * @deprecated Please use {@see getDisplayName()} method - */ - public function getTitle(): ?string - { - return $this->getDisplayName(); - } - public function getDisplayName(): ?string { /** @psalm-suppress InternalMethod */ @@ -123,4 +113,9 @@ public function getDescriptionHtml(): ?string { return null; } + + public function getFullName(): ?string + { + return null; + } } diff --git a/src/Internal/UnknownInfoBuilder.php b/src/Internal/UnknownInfoBuilder.php index 328c7a3..9944612 100644 --- a/src/Internal/UnknownInfoBuilder.php +++ b/src/Internal/UnknownInfoBuilder.php @@ -6,7 +6,6 @@ final class UnknownInfoBuilder implements TestInfoBuilderInterface { - public function __construct( private object $test, ) { diff --git a/src/Setup/ThreadDetectorInterface.php b/src/Setup/ThreadDetectorInterface.php index 8643516..1be27ea 100644 --- a/src/Setup/ThreadDetectorInterface.php +++ b/src/Setup/ThreadDetectorInterface.php @@ -6,7 +6,6 @@ interface ThreadDetectorInterface { - public function getHost(): ?string; public function getThread(): ?string; diff --git a/src/StatusDetector.php b/src/StatusDetector.php index f941128..3456596 100644 --- a/src/StatusDetector.php +++ b/src/StatusDetector.php @@ -14,7 +14,6 @@ final class StatusDetector implements StatusDetectorInterface { - public function __construct( private StatusDetectorInterface $defaultStatusDetector, ) { diff --git a/test/codeception/functional/ClassTitleCest.php b/test/codeception/functional/ClassTitleCest.php index 695d48c..d79b9f6 100644 --- a/test/codeception/functional/ClassTitleCest.php +++ b/test/codeception/functional/ClassTitleCest.php @@ -10,7 +10,6 @@ #[DisplayName('Cest Title')] class ClassTitleCest { - public function makeAction(FunctionalTester $I): void { $I->expect('some condition'); diff --git a/test/codeception/functional/NoClassTitleCest.php b/test/codeception/functional/NoClassTitleCest.php index 660a6aa..ebef295 100644 --- a/test/codeception/functional/NoClassTitleCest.php +++ b/test/codeception/functional/NoClassTitleCest.php @@ -12,7 +12,6 @@ #[Issue('Issue 1')] class NoClassTitleCest { - #[DisplayName('Action title')] public function makeActionWithTitle(FunctionalTester $I): void { diff --git a/test/codeception/unit/AnnotationTest.php b/test/codeception/unit/AnnotationTest.php index 39b6f46..e9b1464 100644 --- a/test/codeception/unit/AnnotationTest.php +++ b/test/codeception/unit/AnnotationTest.php @@ -9,7 +9,6 @@ class AnnotationTest extends Unit { - #[Attribute\DisplayName('Test title')] public function testTitleAnnotation(): void { diff --git a/test/codeception/unit/DataProviderTest.php b/test/codeception/unit/DataProviderTest.php index ca4518e..e816bb7 100644 --- a/test/codeception/unit/DataProviderTest.php +++ b/test/codeception/unit/DataProviderTest.php @@ -9,7 +9,6 @@ class DataProviderTest extends Unit { - /** * @dataProvider providerData */ @@ -27,6 +26,9 @@ public function testDataProviderWithTitle(string $first, string $second): void self::assertSame($first, $second); } + /** + * @return iterable + */ public function providerData(): iterable { return [ diff --git a/test/codeception/unit/StepsTest.php b/test/codeception/unit/StepsTest.php index 7f3dbb7..d466236 100644 --- a/test/codeception/unit/StepsTest.php +++ b/test/codeception/unit/StepsTest.php @@ -14,7 +14,6 @@ class StepsTest extends Unit { - public function testNoStepsSuccess(): void { $this->expectNotToPerformAssertions(); @@ -82,7 +81,6 @@ public function testTwoStepsSecondFails(): void private function createFailingStep(string $name, string $failure): \Codeception\Step { return new class ($failure, $name) extends Meta { - private $failure; public function __construct(string $failure, $action, array $arguments = []) diff --git a/test/report/ReportTest.php b/test/report/ReportTest.php index 15a2c9c..3c16800 100644 --- a/test/report/ReportTest.php +++ b/test/report/ReportTest.php @@ -26,7 +26,6 @@ class ReportTest extends TestCase { - /** * @var array> */ From ddf42f5a8441ca17e2b45eda04c19c7e3fd4bbd1 Mon Sep 17 00:00:00 2001 From: Edward Surov Date: Tue, 15 Feb 2022 19:22:09 +0200 Subject: [PATCH 04/10] Link templates supported --- README.md | 8 +++++++- codeception.yml | 4 ++-- composer.json | 5 ++--- src/AllureCodeception.php | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 51ef322..1b41097 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,10 @@ extensions: enabled: - Qameta\Allure\Codeception\AllureCodeception config: - Qameta\Allure\Codeception\AllureCodeception: + Qameta\Allure\Codeception\AllureCodeception: outputDirectory: allure-results + linkTemplates: + issue: https://example.org/issues/%s ``` `outputDirectory` is used to store Allure results and will be calculated @@ -40,6 +42,10 @@ relatively to Codeception output directory (also known as `paths: log` in codeception.yml) unless you specify an absolute path. You can traverse up using `..` as usual. `outputDirectory` defaults to `allure-results`. +`linkTemplates` is used to process links and generate URLs for them. You can put +here an `sprintf()`-like template or a name of class to be constructed; such class +must implement `Qameta\Allure\Setup\LinkTemplateInterface`. + To generate report from your favourite terminal, [install](https://github.com/allure-framework/allure-cli#installation) allure-cli and run following command (assuming you're in project root and using diff --git a/codeception.yml b/codeception.yml index 72402e6..83d7137 100644 --- a/codeception.yml +++ b/codeception.yml @@ -13,5 +13,5 @@ extensions: config: Qameta\Allure\Codeception\AllureCodeception: outputDirectory: allure-results - - + linkTemplates: + issue: https://example.org/issues/%s diff --git a/composer.json b/composer.json index b836150..939652b 100644 --- a/composer.json +++ b/composer.json @@ -27,13 +27,12 @@ "allure-framework/allure-php-commons": "^2" }, "require-dev": { - "ext-dom": "*", "phpunit/phpunit": "^9", "psalm/plugin-phpunit": "^0.16.1", "remorhaz/php-json-data": "^0.5.3", "remorhaz/php-json-path": "^0.7.7", - "squizlabs/php_codesniffer": "^3.6.1", - "vimeo/psalm": "^4.16.1" + "squizlabs/php_codesniffer": "^3.6.2", + "vimeo/psalm": "^4.20" }, "conflict": { "codeception/phpunit-wrapper": "<9.0.1" diff --git a/src/AllureCodeception.php b/src/AllureCodeception.php index ecbfec4..2ec9961 100644 --- a/src/AllureCodeception.php +++ b/src/AllureCodeception.php @@ -20,12 +20,17 @@ use Qameta\Allure\Codeception\Internal\TestLifecycle; use Qameta\Allure\Codeception\Internal\TestLifecycleInterface; use Qameta\Allure\Codeception\Setup\ThreadDetectorInterface; +use Qameta\Allure\Model\LinkType; use Qameta\Allure\Model\Status; use Qameta\Allure\Model\StatusDetails; use Qameta\Allure\Setup\DefaultStatusDetector; +use Qameta\Allure\Setup\LinkTemplate; +use Qameta\Allure\Setup\LinkTemplateInterface; use Throwable; use function class_exists; +use function is_a; +use function is_array; use function is_callable; use function is_string; use function trim; @@ -36,6 +41,7 @@ final class AllureCodeception extends Extension { private const SETUP_HOOK_PARAMETER = 'setupHook'; private const OUTPUT_DIRECTORY_PARAMETER = 'outputDirectory'; + private const LINK_TEMPLATES_PARAMETER = 'linkTemplates'; private const DEFAULT_RESULTS_DIRECTORY = 'allure-results'; @@ -71,6 +77,9 @@ public function _initialize(): void QametaAllure::getLifecycleConfigurator() ->setStatusDetector(new StatusDetector(new DefaultStatusDetector())) ->setOutputDirectory($this->getOutputDirectory()); + foreach ($this->getLinkTemplates() as $linkType => $linkTemplate) { + QametaAllure::getLifecycleConfigurator()->addLinkTemplate($linkType, $linkTemplate); + } $this->callSetupHook(); } @@ -108,6 +117,31 @@ private function getOutputDirectory(): string return Configuration::outputDir() . ($outputLocal ?? self::DEFAULT_RESULTS_DIRECTORY) . DIRECTORY_SEPARATOR; } + /** + * @psalm-suppress MoreSpecificReturnType + * @return iterable + */ + private function getLinkTemplates(): iterable + { + /** + * @var mixed $templatesConfig + * @psalm-var array $this->config + */ + $templatesConfig = $this->config[self::LINK_TEMPLATES_PARAMETER] ?? []; + if (!is_array($templatesConfig)) { + $templatesConfig = []; + } + foreach ($templatesConfig as $linkTypeName => $linkConfig) { + if (!is_string($linkConfig) || !is_string($linkTypeName)) { + continue; + } + yield LinkType::fromOptionalString($linkTypeName) => + class_exists($linkConfig) && is_a($linkConfig, LinkTemplateInterface::class, true) + ? new $linkConfig() + : new LinkTemplate($linkConfig); + } + } + /** * @psalm-suppress MissingDependency */ From b0ca7b98f3511ca5fbb5c0310614955e9adf2109 Mon Sep 17 00:00:00 2001 From: Edward Surov Date: Tue, 15 Feb 2022 20:58:14 +0200 Subject: [PATCH 05/10] Added info about setup hoos to readme file --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 1b41097..00aa2e2 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ extensions: outputDirectory: allure-results linkTemplates: issue: https://example.org/issues/%s + setipHook: My\SetupHook ``` `outputDirectory` is used to store Allure results and will be calculated @@ -46,6 +47,26 @@ codeception.yml) unless you specify an absolute path. You can traverse up using here an `sprintf()`-like template or a name of class to be constructed; such class must implement `Qameta\Allure\Setup\LinkTemplateInterface`. +`setupHook` allows to execute some bootstrapping code during initialization. You can +put here a name of the class that implements magic `__invoke()` method - and that method +will be called. For example, it can be used to ignore unnecessary docblock annotations: + +```php + Date: Thu, 15 Dec 2022 16:01:04 +0200 Subject: [PATCH 06/10] Developer dependencies updated --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 939652b..434523d 100644 --- a/composer.json +++ b/composer.json @@ -28,11 +28,11 @@ }, "require-dev": { "phpunit/phpunit": "^9", - "psalm/plugin-phpunit": "^0.16.1", + "psalm/plugin-phpunit": "^0.18.4", "remorhaz/php-json-data": "^0.5.3", "remorhaz/php-json-path": "^0.7.7", - "squizlabs/php_codesniffer": "^3.6.2", - "vimeo/psalm": "^4.20" + "squizlabs/php_codesniffer": "^3.7.1", + "vimeo/psalm": "^5.2" }, "conflict": { "codeception/phpunit-wrapper": "<9.0.1" From a5af9bf60dcd54692bbd084e7237cc90969d40b0 Mon Sep 17 00:00:00 2001 From: Edward Surov Date: Fri, 16 Dec 2022 20:32:58 +0200 Subject: [PATCH 07/10] Switched to Codecept 5 (#99) --- composer.json | 5 +-- src/AllureCodeception.php | 33 ++++++++----------- src/Internal/ArgumentAsString.php | 22 +++++++++++-- src/Internal/CeptInfoBuilder.php | 6 ++-- src/Internal/CeptProvider.php | 6 ++-- src/Internal/CestInfoBuilder.php | 12 ++----- src/Internal/CestProvider.php | 11 ++----- src/Internal/GherkinInfoBuilder.php | 11 ++----- src/Internal/GherkinProvider.php | 2 +- src/Internal/TestLifecycle.php | 20 +++--------- src/Internal/UnitInfoBuilder.php | 28 ++++++---------- src/Internal/UnitProvider.php | 41 ++++++++++++------------ test/codeception/_support/UnitTester.php | 29 +++++++++++++++++ test/codeception/unit.suite.yml | 2 ++ test/codeception/unit/StepsTest.php | 2 +- 15 files changed, 119 insertions(+), 111 deletions(-) create mode 100644 test/codeception/_support/UnitTester.php diff --git a/composer.json b/composer.json index 434523d..5faea3d 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "require": { "php": "^8", "ext-json": "*", - "codeception/codeception": "^4.1", + "codeception/codeception": "^5", "allure-framework/allure-php-commons": "^2" }, "require-dev": { @@ -34,9 +34,6 @@ "squizlabs/php_codesniffer": "^3.7.1", "vimeo/psalm": "^5.2" }, - "conflict": { - "codeception/phpunit-wrapper": "<9.0.1" - }, "autoload": { "psr-4": { "Qameta\\Allure\\Codeception\\": "src/" diff --git a/src/AllureCodeception.php b/src/AllureCodeception.php index 2ec9961..4723151 100644 --- a/src/AllureCodeception.php +++ b/src/AllureCodeception.php @@ -12,7 +12,6 @@ use Codeception\Event\TestEvent; use Codeception\Events; use Codeception\Exception\ConfigurationException; -use Codeception\Step; use Qameta\Allure\Allure; use Qameta\Allure\Allure as QametaAllure; use Qameta\Allure\Codeception\Internal\DefaultThreadDetector; @@ -26,7 +25,6 @@ use Qameta\Allure\Setup\DefaultStatusDetector; use Qameta\Allure\Setup\LinkTemplate; use Qameta\Allure\Setup\LinkTemplateInterface; -use Throwable; use function class_exists; use function is_a; @@ -46,6 +44,7 @@ final class AllureCodeception extends Extension private const DEFAULT_RESULTS_DIRECTORY = 'allure-results'; protected static array $events = [ + Events::MODULE_INIT => 'moduleInit', Events::SUITE_BEFORE => 'suiteBefore', Events::SUITE_AFTER => 'suiteAfter', Events::TEST_START => 'testStart', @@ -69,11 +68,13 @@ final class AllureCodeception extends Extension * @throws ConfigurationException * phpcs:disable PSR2.Methods.MethodDeclaration.Underscore */ - public function _initialize(): void + public function moduleInit(): void { // phpcs:enable PSR2.Methods.MethodDeclaration.Underscore - parent::_initialize(); + //parent::_initialize(); QametaAllure::reset(); + $this->testLifecycle = null; + $this->threadDetector = null; QametaAllure::getLifecycleConfigurator() ->setStatusDetector(new StatusDetector(new DefaultStatusDetector())) ->setOutputDirectory($this->getOutputDirectory()); @@ -148,7 +149,11 @@ class_exists($linkConfig) && is_a($linkConfig, LinkTemplateInterface::class, tru public function suiteBefore(SuiteEvent $suiteEvent): void { /** @psalm-suppress InternalMethod */ - $suiteName = $suiteEvent->getSuite()->getName(); + $suiteName = $suiteEvent->getSuite()?->getName(); + if (!isset($suiteName)) { + return; + } + $this ->getTestLifecycle() ->switchToSuite(new SuiteInfo($suiteName)); @@ -185,12 +190,10 @@ private function getThreadDetector(): ThreadDetectorInterface */ public function testError(FailEvent $failEvent): void { - /** @var Throwable $error */ - $error = $failEvent->getFail(); $this ->getTestLifecycle() ->switchToTest($failEvent->getTest()) - ->updateTestFailure($error); + ->updateTestFailure($failEvent->getFail()); } /** @@ -198,12 +201,10 @@ public function testError(FailEvent $failEvent): void */ public function testFail(FailEvent $failEvent): void { - /** @var Throwable $error */ - $error = $failEvent->getFail(); $this ->getTestLifecycle() ->switchToTest($failEvent->getTest()) - ->updateTestFailure($error, Status::failed()); + ->updateTestFailure($failEvent->getFail(), Status::failed()); } /** @@ -211,7 +212,6 @@ public function testFail(FailEvent $failEvent): void */ public function testIncomplete(FailEvent $failEvent): void { - /** @var Throwable $error */ $error = $failEvent->getFail(); $this ->getTestLifecycle() @@ -228,7 +228,6 @@ public function testIncomplete(FailEvent $failEvent): void */ public function testSkipped(FailEvent $failEvent): void { - /** @var Throwable $error */ $error = $failEvent->getFail(); $this ->getTestLifecycle() @@ -269,12 +268,10 @@ public function testEnd(TestEvent $testEvent): void */ public function stepBefore(StepEvent $stepEvent): void { - /** @psalm-var Step $step */ - $step = $stepEvent->getStep(); $this ->getTestLifecycle() ->switchToTest($stepEvent->getTest()) - ->startStep($step) + ->startStep($stepEvent->getStep()) ->updateStep(); } @@ -283,12 +280,10 @@ public function stepBefore(StepEvent $stepEvent): void */ public function stepAfter(StepEvent $stepEvent): void { - /** @psalm-var Step $step */ - $step = $stepEvent->getStep(); $this ->getTestLifecycle() ->switchToTest($stepEvent->getTest()) - ->switchToStep($step) + ->switchToStep($stepEvent->getStep()) ->updateStepResult() ->stopStep(); } diff --git a/src/Internal/ArgumentAsString.php b/src/Internal/ArgumentAsString.php index 4667065..26455fe 100644 --- a/src/Internal/ArgumentAsString.php +++ b/src/Internal/ArgumentAsString.php @@ -4,7 +4,7 @@ namespace Qameta\Allure\Codeception\Internal; -use Codeception\Util\Locator; +use InvalidArgumentException; use Stringable; use function array_map; @@ -15,6 +15,7 @@ use function is_resource; use function is_string; use function json_encode; +use function method_exists; use function strtr; use function trim; @@ -77,7 +78,7 @@ private function prepareObject(object $argument): string $webdriverByClass = '\Facebook\WebDriver\WebDriverBy'; if (class_exists($webdriverByClass) && is_a($argument, $webdriverByClass)) { - return Locator::humanReadableString($argument); + return $this->webDriverByAsString($argument); } return trim($argument::class, "\\"); @@ -90,4 +91,21 @@ public function __toString(): string JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, ); } + + private function webDriverByAsString(object $selector): string + { + $type = method_exists($selector, 'getMechanism') + ? (string) $selector->getMechanism() + : null; + + $locator = method_exists($selector, 'getValue') + ? (string) $selector->getValue() + : null; + + if (!isset($type, $locator)) { + throw new InvalidArgumentException("Unrecognized selector"); + } + + return "$type '$locator'"; + } } diff --git a/src/Internal/CeptInfoBuilder.php b/src/Internal/CeptInfoBuilder.php index bd8876f..559fa6d 100644 --- a/src/Internal/CeptInfoBuilder.php +++ b/src/Internal/CeptInfoBuilder.php @@ -17,9 +17,9 @@ public function build(?string $host, ?string $thread): TestInfo { return new TestInfo( originalTest: $this->test, - signature: (string) $this->test->getSignature(), - class: (string) $this->test->getName(), - method: (string) $this->test->getName(), + signature: $this->test->getSignature(), + class: $this->test->getName(), + method: $this->test->getName(), host: $host, thread: $thread, ); diff --git a/src/Internal/CeptProvider.php b/src/Internal/CeptProvider.php index c33ea46..b98869d 100644 --- a/src/Internal/CeptProvider.php +++ b/src/Internal/CeptProvider.php @@ -100,7 +100,7 @@ public function getDisplayName(): ?string public function getFullName(): ?string { - return (string) $this->test->getSignature(); + return $this->test->getSignature(); } public function getDescription(): ?string @@ -118,7 +118,7 @@ public function getDescriptionHtml(): ?string private function getLegacyAnnotation(string $name): ?string { /** - * @var mixed $annotations + * @psalm-var mixed $annotations * @psalm-suppress InvalidArgument */ $annotations = $this->test->getMetadata()->getParam($name); @@ -140,7 +140,7 @@ private function getLegacyAnnotation(string $name): ?string private function getLegacyAnnotations(string $name): array { /** - * @var mixed $annotations + * @psalm-var mixed $annotations * @psalm-suppress InvalidArgument */ $annotations = $this->test->getMetadata()->getParam($name); diff --git a/src/Internal/CestInfoBuilder.php b/src/Internal/CestInfoBuilder.php index 1187040..1b93087 100644 --- a/src/Internal/CestInfoBuilder.php +++ b/src/Internal/CestInfoBuilder.php @@ -7,7 +7,6 @@ use Codeception\Test\Cest; use function is_int; -use function is_object; use function is_string; final class CestInfoBuilder implements TestInfoBuilderInterface @@ -19,16 +18,11 @@ public function __construct( public function build(?string $host, ?string $thread): TestInfo { - /** @var mixed $testClass */ - $testClass = $this->test->getTestClass(); - /** @var mixed $testMethod */ - $testMethod = $this->test->getTestMethod(); - return new TestInfo( originalTest: $this->test, - signature: (string) $this->test->getSignature(), - class: is_object($testClass) ? $testClass::class : null, - method: is_string($testMethod) ? $testMethod : null, + signature: $this->test->getSignature(), + class: $this->test->getTestInstance()::class, + method: $this->test->getTestMethod(), dataLabel: $this->getDataLabel(), host: $host, thread: $thread, diff --git a/src/Internal/CestProvider.php b/src/Internal/CestProvider.php index 9ff8019..ba54b6d 100644 --- a/src/Internal/CestProvider.php +++ b/src/Internal/CestProvider.php @@ -16,7 +16,6 @@ use function array_values; use function is_array; use function is_int; -use function is_object; use function is_string; /** @@ -37,16 +36,12 @@ public function __construct( */ public static function createForChain(Cest $test, LinkTemplateCollectionInterface $linkTemplates): array { - /** @var mixed $testClass */ - $testClass = $test->getTestClass(); - /** @var mixed $testMethod */ - $testMethod = $test->getTestMethod(); - /** @var callable-string|null $callableTestMethod */ - $callableTestMethod = is_string($testMethod) ? $testMethod : null; + /** @psalm-var callable-string $callableTestMethod */ + $callableTestMethod = $test->getTestMethod(); return [ ...AttributeParser::createForChain( - classOrObject: is_object($testClass) ? $testClass : null, + classOrObject: $test->getTestInstance(), methodOrFunction: $callableTestMethod, linkTemplates: $linkTemplates, ), diff --git a/src/Internal/GherkinInfoBuilder.php b/src/Internal/GherkinInfoBuilder.php index d56d3c6..cc744ca 100644 --- a/src/Internal/GherkinInfoBuilder.php +++ b/src/Internal/GherkinInfoBuilder.php @@ -17,16 +17,11 @@ public function __construct( public function build(?string $host, ?string $thread): TestInfo { - /** @psalm-var mixed $className */ - $className = $this->test->getFeature(); - /** @psalm-var mixed $methodName */ - $methodName = $this->test->getScenarioTitle(); - return new TestInfo( originalTest: $this->test, - signature: (string) $this->test->getSignature(), - class: is_string($className) ? $className : null, - method: is_string($methodName) ? $methodName : null, + signature: $this->test->getSignature(), + class: $this->test->getFeature(), + method: $this->test->getScenarioTitle(), host: $host, thread: $thread, ); diff --git a/src/Internal/GherkinProvider.php b/src/Internal/GherkinProvider.php index 66fdd8d..86a8345 100644 --- a/src/Internal/GherkinProvider.php +++ b/src/Internal/GherkinProvider.php @@ -49,7 +49,7 @@ public function getParameters(): array public function getDisplayName(): ?string { - return (string) $this->test->toString(); + return $this->test->toString(); } public function getDescription(): ?string diff --git a/src/Internal/TestLifecycle.php b/src/Internal/TestLifecycle.php index 7ee466c..da87576 100644 --- a/src/Internal/TestLifecycle.php +++ b/src/Internal/TestLifecycle.php @@ -8,8 +8,8 @@ use Codeception\Test\Cept; use Codeception\Test\Cest; use Codeception\Test\Gherkin; +use Codeception\Test\TestCaseWrapper; use Codeception\TestInterface; -use PHPUnit\Framework\TestCase; use Qameta\Allure\AllureLifecycleInterface; use Qameta\Allure\Codeception\Setup\ThreadDetectorInterface; use Qameta\Allure\Io\DataSourceFactory; @@ -60,7 +60,7 @@ public function __construct( private ThreadDetectorInterface $threadDetector, private LinkTemplateCollectionInterface $linkTemplates, ) { - /** @var WeakMap */ + /** @psalm-var WeakMap $this->stepStarts */ $this->stepStarts = new WeakMap(); } @@ -119,7 +119,7 @@ private function getTestInfoBuilder(object $test): TestInfoBuilderInterface $test instanceof Cest => new CestInfoBuilder($test), $test instanceof Gherkin => new GherkinInfoBuilder($test), $test instanceof Cept => new CeptInfoBuilder($test), - $test instanceof TestCase => new UnitInfoBuilder($test), + $test instanceof TestCaseWrapper => new UnitInfoBuilder($test), default => new UnknownInfoBuilder($test), }; } @@ -168,7 +168,7 @@ private function createModelProvidersForTest(mixed $test): array $test instanceof Cest => CestProvider::createForChain($test, $this->linkTemplates), $test instanceof Gherkin => GherkinProvider::createForChain($test), $test instanceof Cept => CeptProvider::createForChain($test, $this->linkTemplates), - $test instanceof TestCase => UnitProvider::createForChain($test, $this->linkTemplates), + $test instanceof TestCaseWrapper => UnitProvider::createForChain($test, $this->linkTemplates), default => [], }; } @@ -323,10 +323,6 @@ public function stopStep(): self { $stepStart = $this->getCurrentStepStart(); $this->lifecycle->stopStep($stepStart->getUuid()); - /** - * @var Step $step - * @psalm-ignore-var - */ foreach ($this->stepStarts as $step => $storedStart) { if ($storedStart === $stepStart) { unset($this->stepStarts[$step]); @@ -341,15 +337,9 @@ public function updateStep(): self { $stepStart = $this->getCurrentStepStart(); $step = $stepStart->getOriginalStep(); - if (null === $step->getAction()) { - $step = $step->getMetaStep(); - } $params = []; - /** - * @var array-key $name - * @var mixed $value - */ + /** @psalm-var mixed $value */ foreach ($step->getArguments() as $name => $value) { $params[] = new Parameter( is_int($name) ? "#$name" : $name, diff --git a/src/Internal/UnitInfoBuilder.php b/src/Internal/UnitInfoBuilder.php index 0009a4f..61dfae9 100644 --- a/src/Internal/UnitInfoBuilder.php +++ b/src/Internal/UnitInfoBuilder.php @@ -4,40 +4,32 @@ namespace Qameta\Allure\Codeception\Internal; -use PHPUnit\Framework\TestCase; +use Codeception\Test\TestCaseWrapper; +use function is_int; use function preg_match; final class UnitInfoBuilder implements TestInfoBuilderInterface { public function __construct( - private TestCase $test, + private TestCaseWrapper $test, ) { } public function build(?string $host, ?string $thread): TestInfo { - /** @psalm-suppress InternalMethod */ - $methodName = $this->test->getName(false); + $fields = $this->test->getReportFields(); + $index = $this->test->getMetadata()->getIndex(); + $dataLabel = is_int($index) ? "#$index" : $index; return new TestInfo( originalTest: $this->test, - signature: $this->test::class . ':' . $methodName, - class: $this->test::class, - method: $methodName, - dataLabel: $this->getDataLabel(), + signature: $this->test->getSignature(), + class: $fields['class'] ?? null, + method: $this->test->getMetadata()->getName(), + dataLabel: $dataLabel, host: $host, thread: $thread, ); } - - private function getDataLabel(): ?string - { - /** @psalm-suppress InternalMethod */ - $dataSet = $this->test->getDataSetAsString(false); - - return 1 === preg_match('#^ with data set (.+)$#', $dataSet, $matches) - ? $matches[1] - : null; - } } diff --git a/src/Internal/UnitProvider.php b/src/Internal/UnitProvider.php index 487da5c..382bc00 100644 --- a/src/Internal/UnitProvider.php +++ b/src/Internal/UnitProvider.php @@ -4,7 +4,8 @@ namespace Qameta\Allure\Codeception\Internal; -use PHPUnit\Framework\TestCase; +use Closure; +use Codeception\Test\TestCaseWrapper; use Qameta\Allure\Attribute\AttributeParser; use Qameta\Allure\Setup\LinkTemplateCollectionInterface; use Qameta\Allure\Model\ModelProviderInterface; @@ -21,32 +22,32 @@ final class UnitProvider implements ModelProviderInterface { /** - * @param TestCase $test + * @param TestCaseWrapper $test * @param LinkTemplateCollectionInterface $linkTemplates */ public function __construct( - private TestCase $test, + private TestCaseWrapper $test, LinkTemplateCollectionInterface $linkTemplates, ) { } /** - * @param TestCase $test + * @param TestCaseWrapper $test * @param LinkTemplateCollectionInterface $linkTemplates * @throws ReflectionException * @return list */ - public static function createForChain(TestCase $test, LinkTemplateCollectionInterface $linkTemplates): array + public static function createForChain(TestCaseWrapper $test, LinkTemplateCollectionInterface $linkTemplates): array { - /** - * @var callable-string|null $methodOrFunction - * @psalm-suppress InternalMethod - */ - $methodOrFunction = $test->getName(false); + $fields = $test->getReportFields(); + /** @var class-string $class */ + $class = $fields['class'] ?? null; + /** @var Closure|callable-string|null $methodOrFunction */ + $methodOrFunction = $test->getMetadata()->getName(); return [ ...AttributeParser::createForChain( - classOrObject: $test, + classOrObject: $class, methodOrFunction: $methodOrFunction, linkTemplates: $linkTemplates, ), @@ -69,16 +70,17 @@ public function getLabels(): array */ public function getParameters(): array { - /** @psalm-suppress InternalMethod */ - if (!$this->test->usesDataProvider()) { + $testMetadata = $this->test->getMetadata(); + if (null === $testMetadata->getIndex()) { return []; } - $dataMethod = new ReflectionMethod($this->test, 'getProvidedData'); + $testCase = $this->test->getTestCase(); + + $dataMethod = new ReflectionMethod($testCase, 'getProvidedData'); $dataMethod->setAccessible(true); - /** @psalm-suppress InternalMethod */ - $methodName = $this->test->getName(false); - $testMethod = new ReflectionMethod($this->test, $methodName); + $methodName = $testMetadata->getName(); + $testMethod = new ReflectionMethod($testCase, $methodName); $argNames = $testMethod->getParameters(); $params = []; @@ -86,7 +88,7 @@ public function getParameters(): array * @var array-key $key * @var mixed $param */ - foreach ($dataMethod->invoke($this->test) as $key => $param) { + foreach ($dataMethod->invoke($testCase) as $key => $param) { $argName = array_shift($argNames); $name = $argName?->getName() ?? $key; $params[] = new Parameter( @@ -100,8 +102,7 @@ public function getParameters(): array public function getDisplayName(): ?string { - /** @psalm-suppress InternalMethod */ - return $this->test->getName(); + return $this->test->getMetadata()->getName(); } public function getDescription(): ?string diff --git a/test/codeception/_support/UnitTester.php b/test/codeception/_support/UnitTester.php new file mode 100644 index 0000000..a2ff24d --- /dev/null +++ b/test/codeception/_support/UnitTester.php @@ -0,0 +1,29 @@ +failure = $failure; } - public function run(ModuleContainer $container = null) + public function run(ModuleContainer $container = null): void { $this->setFailed(true); Assert::fail($this->failure); From 926864bea0f86de73d492e6fb0830fbfc92437ec Mon Sep 17 00:00:00 2001 From: Edward Surov Date: Tue, 20 Dec 2022 13:03:47 +0200 Subject: [PATCH 08/10] Tune build pipeline --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2a9d69c..f31eb38 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,7 @@ jobs: php-version: - "8.0" - "8.1" + - "8.2" os: - ubuntu-latest - windows-latest @@ -48,10 +49,10 @@ jobs: ${{ matrix.composer-options }} - name: Run tests - if: ${{ matrix.php-version != '8.1' }} + if: ${{ matrix.php-version != '8.2' }} run: composer test - name: Run tests (experimental) - if: ${{ matrix.php-version == '8.1' }} + if: ${{ matrix.php-version == '8.2' }} continue-on-error: true run: composer test From 916e6e17a72b8fc403e1e18701789c0c2aa28de5 Mon Sep 17 00:00:00 2001 From: Edward Surov Date: Tue, 23 May 2023 12:48:09 +0300 Subject: [PATCH 09/10] Static testing tools updated --- composer.json | 6 +++--- src/Internal/TestLifecycle.php | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 5faea3d..5d11539 100644 --- a/composer.json +++ b/composer.json @@ -27,12 +27,12 @@ "allure-framework/allure-php-commons": "^2" }, "require-dev": { - "phpunit/phpunit": "^9", + "phpunit/phpunit": "^9.5", "psalm/plugin-phpunit": "^0.18.4", "remorhaz/php-json-data": "^0.5.3", "remorhaz/php-json-path": "^0.7.7", - "squizlabs/php_codesniffer": "^3.7.1", - "vimeo/psalm": "^5.2" + "squizlabs/php_codesniffer": "^3.7.2", + "vimeo/psalm": "^5.12" }, "autoload": { "psr-4": { diff --git a/src/Internal/TestLifecycle.php b/src/Internal/TestLifecycle.php index 5281234..970cc49 100644 --- a/src/Internal/TestLifecycle.php +++ b/src/Internal/TestLifecycle.php @@ -323,6 +323,10 @@ public function stopStep(): self { $stepStart = $this->getCurrentStepStart(); $this->lifecycle->stopStep($stepStart->getUuid()); + /** + * @psalm-var Step $step + * @psalm-var StepStartInfo $storedStart + */ foreach ($this->stepStarts as $step => $storedStart) { if ($storedStart === $stepStart) { unset($this->stepStarts[$step]); From baf7d66358c6a8ee313efcd7688c15d6199e36b9 Mon Sep 17 00:00:00 2001 From: Edward Surov Date: Tue, 23 May 2023 14:39:10 +0300 Subject: [PATCH 10/10] Codeception steps use incorrect parent (#116) --- src/Internal/TestLifecycle.php | 3 +- .../functional/NestedStepsCest.php | 41 +++++++++++++++++++ test/report/ReportTest.php | 31 ++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 test/codeception/functional/NestedStepsCest.php diff --git a/src/Internal/TestLifecycle.php b/src/Internal/TestLifecycle.php index 970cc49..763b28b 100644 --- a/src/Internal/TestLifecycle.php +++ b/src/Internal/TestLifecycle.php @@ -297,9 +297,8 @@ private function buildHistoryId(string $testCaseId, TestInfo $testInfo, Paramete public function startStep(Step $step): self { - $parent = $this->currentStepStart?->getUuid() ?? $this->currentTestStart?->getTestUuid(); $stepResult = $this->resultFactory->createStep(); - $this->lifecycle->startStep($stepResult, $parent); + $this->lifecycle->startStep($stepResult); $stepStart = new StepStartInfo( $step, diff --git a/test/codeception/functional/NestedStepsCest.php b/test/codeception/functional/NestedStepsCest.php new file mode 100644 index 0000000..618158f --- /dev/null +++ b/test/codeception/functional/NestedStepsCest.php @@ -0,0 +1,41 @@ +expect("condition 1"); + Allure::runStep( + function () use ($I): void { + $I->expect("condition 1.1"); + Allure::runStep( + function () use ($I): void { + $I->expect("condition 1.1.1"); + }, + 'Step 1.1.1', + ); + }, + 'Step 1.1', + ); + Allure::runStep( + function () use ($I): void { + $I->expect("condition 1.2"); + }, + 'Step 1.2', + ); + }, + 'Step 1', + ); + } +} diff --git a/test/report/ReportTest.php b/test/report/ReportTest.php index 3c16800..8be2570 100644 --- a/test/report/ReportTest.php +++ b/test/report/ReportTest.php @@ -5,6 +5,7 @@ namespace Qameta\Allure\Codeception\Test; use PHPUnit\Framework\TestCase; +use Qameta\Allure\Codeception\Test\Functional\NestedStepsCest; use Qameta\Allure\Codeception\Test\Unit\AnnotationTest; use Qameta\Allure\Codeception\Test\Unit\StepsTest; use Remorhaz\JSON\Data\Value\EncodedJson\NodeValueFactory; @@ -323,6 +324,36 @@ public function providerExistingNodeValue(): iterable '$.steps[*].name', ['step 1 name', 'step 2 name'], ], + 'Nested steps: root names' => [ + NestedStepsCest::class, + 'makeNestedSteps', + '$.steps[*].name', + ['Step 1'], + ], + 'Nested steps: level 1 names' => [ + NestedStepsCest::class, + 'makeNestedSteps', + '$.steps[?(@.name=="Step 1")].steps[*].name', + ['i expect condition 1', 'Step 1.1', 'Step 1.2'], + ], + 'Nested steps: level 1.1 names' => [ + NestedStepsCest::class, + 'makeNestedSteps', + '$.steps..steps[?(@.name=="Step 1.1")].steps[*].name', + ['i expect condition 1.1', 'Step 1.1.1'], + ], + 'Nested steps: level 1.1.1 names' => [ + NestedStepsCest::class, + 'makeNestedSteps', + '$.steps..steps[?(@.name=="Step 1.1.1")].steps[*].name', + ['i expect condition 1.1.1'], + ], + 'Nested steps: level 1.2 names' => [ + NestedStepsCest::class, + 'makeNestedSteps', + '$.steps..steps[?(@.name=="Step 1.2")].steps[*].name', + ['i expect condition 1.2'], + ], ]; }