├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── release.yml └── workflows │ ├── build.yml │ └── labels-verify.yml ├── .gitignore ├── LICENSE ├── README.md ├── codeception-report.yml ├── codeception.yml ├── composer.json ├── phpcs.xml.dist ├── psalm.xml.dist ├── src ├── AllureCodeception.php ├── Internal │ ├── ArgumentAsString.php │ ├── CeptInfoBuilder.php │ ├── CeptProvider.php │ ├── CestInfoBuilder.php │ ├── CestProvider.php │ ├── DefaultThreadDetector.php │ ├── GherkinInfoBuilder.php │ ├── GherkinProvider.php │ ├── StepStartInfo.php │ ├── SuiteInfo.php │ ├── SuiteProvider.php │ ├── TestInfo.php │ ├── TestInfoBuilderInterface.php │ ├── TestInfoProvider.php │ ├── TestLifecycle.php │ ├── TestLifecycleInterface.php │ ├── TestStartInfo.php │ ├── UnitInfoBuilder.php │ ├── UnitProvider.php │ └── UnknownInfoBuilder.php ├── Setup │ └── ThreadDetectorInterface.php └── StatusDetector.php └── test ├── codeception-report ├── _support │ ├── AcceptanceTester.php │ ├── FunctionalTester.php │ └── UnitTester.php ├── acceptance.suite.yml ├── acceptance │ └── sample.feature ├── functional.suite.yml ├── functional │ ├── BasicScenarioCept.php │ ├── ClassTitleCest.php │ ├── CustomizedScenarioCept.php │ ├── NestedStepsCest.php │ ├── NoClassTitleCest.php │ └── ScenarioWithLegacyAnnotationsCept.php ├── unit.suite.yml └── unit │ ├── AnnotationTest.php │ ├── DataProviderTest.php │ └── StepsTest.php └── codeception ├── _support └── UnitTester.php ├── report-check.suite.yml ├── report-check └── ReportTest.php ├── unit.suite.yml └── unit └── Internal ├── ArgumentAsStringTest.php └── CestProviderTest.php /.gitattributes: -------------------------------------------------------------------------------- 1 | # Define the line ending behavior of the different file extensions 2 | # Set default behavior, in case users don't have core.autocrlf set. 3 | * text text=auto eol=lf 4 | 5 | .php diff=php 6 | 7 | # Declare files that will always have CRLF line endings on checkout. 8 | *.bat eol=crlf 9 | 10 | # Declare files that will always have LF line endings on checkout. 11 | *.pem eol=lf 12 | 13 | # Denote all files that are truly binary and should not be modified. 14 | *.png binary 15 | *.jpg binary 16 | *.gif binary 17 | *.ico binary 18 | *.mo binary 19 | *.pdf binary 20 | *.phar binary 21 | *.woff binary 22 | *.woff2 binary 23 | *.ttf binary 24 | *.otf binary 25 | *.eot binary 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | labels: 8 | - "type:dependencies" 9 | 10 | - package-ecosystem: "composer" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | labels: 15 | - "type:dependencies" 16 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # release.yml 2 | 3 | changelog: 4 | categories: 5 | - title: '🚀 New Features' 6 | labels: 7 | - 'type:new feature' 8 | - title: '🔬 Improvements' 9 | labels: 10 | - 'type:improvement' 11 | - title: '🐞 Bug Fixes' 12 | labels: 13 | - 'type:bug' 14 | - title: '⬆️ Dependency Updates' 15 | labels: 16 | - 'type:dependencies' 17 | - title: '⛔️ Security' 18 | labels: 19 | - 'type:security' 20 | - title: '👻 Internal changes' 21 | labels: 22 | - 'type:internal' 23 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | push: 8 | branches: 9 | - 'main' 10 | - 'hotfix-*' 11 | 12 | jobs: 13 | tests: 14 | name: PHP ${{ matrix.php-version }} on ${{ matrix.os }} (${{ matrix.composer-options }}) 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | php-version: 20 | - "8.0" 21 | - "8.1" 22 | - "8.2" 23 | - "8.3" 24 | os: 25 | - ubuntu-latest 26 | - windows-latest 27 | - macOS-latest 28 | composer-options: 29 | - "" 30 | - "--prefer-lowest" 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | 35 | - name: Set up PHP ${{ matrix.php-version }} 36 | uses: shivammathur/setup-php@v2 37 | with: 38 | php-version: ${{ matrix.php-version }} 39 | extensions: pcntl, posix, intl 40 | coverage: xdebug 41 | ini-values: error_reporting=E_ALL 42 | 43 | - name: Validate composer.json and composer.lock 44 | run: composer validate 45 | 46 | - name: Install dependencies 47 | run: composer update 48 | --prefer-dist 49 | --no-progress 50 | ${{ matrix.composer-options }} 51 | 52 | - name: Run tests 53 | run: composer test 54 | -------------------------------------------------------------------------------- /.github/workflows/labels-verify.yml: -------------------------------------------------------------------------------- 1 | name: "Verify type labels" 2 | 3 | on: 4 | pull_request: 5 | types: [opened, labeled, unlabeled, synchronize] 6 | 7 | jobs: 8 | triage: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: baev/match-label-action@master 12 | with: 13 | allowed: 'type:bug,type:new feature,type:improvement,type:dependencies,type:internal,type:invalid' 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | vendor/* 4 | composer.phar 5 | composer.lock 6 | /build/ 7 | /test/codeception*/_support/_generated/ 8 | .phpunit.result.cache 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 Qameta Software OÜ 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Allure Codeception Adapter 2 | 3 | [![Latest Stable Version](http://poser.pugx.org/allure-framework/allure-codeception/v)](https://packagist.org/packages/allure-framework/allure-codeception) 4 | [![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) 5 | [![Type Coverage](https://shepherd.dev/github/allure-framework/allure-codeception/coverage.svg)](https://shepherd.dev/github/allure-framework/allure-codeception) 6 | [![Psalm Level](https://shepherd.dev/github/allure-framework/allure-codeception/level.svg)](https://shepherd.dev/github/allure-framework/allure-codeception) 7 | [![Total Downloads](http://poser.pugx.org/allure-framework/allure-codeception/downloads)](https://packagist.org/packages/allure-framework/allure-codeception) 8 | [![License](http://poser.pugx.org/allure-framework/allure-codeception/license)](https://packagist.org/packages/allure-framework/allure-codeception) 9 | 10 | This is an official [Codeception](http://codeception.com) adapter for Allure Framework. 11 | 12 | ## What is this for? 13 | The main purpose of this adapter is to accumulate information about your tests and write it out to a set of XML files: one for each test class. This adapter only generates XML files containing information about tests. See [wiki section](https://github.com/allure-framework/allure-core/wiki#generating-report) on how to generate report. 14 | 15 | ## Example project 16 | Example project is located at: https://github.com/allure-examples/allure-codeception-example 17 | 18 | ## Installation and Usage 19 | In order to use this adapter you need to add a new dependency to your **composer.json** file: 20 | ``` 21 | { 22 | "require": { 23 | "php": "^8", 24 | "allure-framework/allure-codeception": "^2" 25 | } 26 | } 27 | ``` 28 | To enable this adapter in Codeception tests simply put it in "enabled" extensions section of **codeception.yml**: 29 | ```yaml 30 | extensions: 31 | enabled: 32 | - Qameta\Allure\Codeception\AllureCodeception 33 | config: 34 | Qameta\Allure\Codeception\AllureCodeception: 35 | outputDirectory: allure-results 36 | linkTemplates: 37 | issue: https://example.org/issues/%s 38 | setupHook: My\SetupHook 39 | ``` 40 | 41 | `outputDirectory` is used to store Allure results and will be calculated 42 | relatively to Codeception output directory (also known as `paths: log` in 43 | codeception.yml) unless you specify an absolute path. You can traverse up using 44 | `..` as usual. `outputDirectory` defaults to `allure-results`. 45 | 46 | `linkTemplates` is used to process links and generate URLs for them. You can put 47 | here an `sprintf()`-like template or a name of class to be constructed; such class 48 | must implement `Qameta\Allure\Setup\LinkTemplateInterface`. 49 | 50 | `setupHook` allows to execute some bootstrapping code during initialization. You can 51 | put here a name of the class that implements magic `__invoke()` method - and that method 52 | will be called. For example, it can be used to ignore unnecessary docblock annotations: 53 | 54 | ```php 55 | 2 | 3 | Qameta Coding Standards 4 | 5 | src 6 | test 7 | 8 | 9 | 10 | 11 | 12 | 13 | */test/*Test.php 14 | 15 | 16 | -------------------------------------------------------------------------------- /psalm.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/AllureCodeception.php: -------------------------------------------------------------------------------- 1 | 'moduleInit', 48 | Events::SUITE_BEFORE => 'suiteBefore', 49 | Events::SUITE_AFTER => 'suiteAfter', 50 | Events::TEST_START => 'testStart', 51 | Events::TEST_FAIL => 'testFail', 52 | Events::TEST_ERROR => 'testError', 53 | Events::TEST_INCOMPLETE => 'testIncomplete', 54 | Events::TEST_SKIPPED => 'testSkipped', 55 | Events::TEST_SUCCESS => 'testSuccess', 56 | Events::TEST_END => 'testEnd', 57 | Events::STEP_BEFORE => 'stepBefore', 58 | Events::STEP_AFTER => 'stepAfter' 59 | ]; 60 | 61 | private ?ThreadDetectorInterface $threadDetector = null; 62 | 63 | private ?TestLifecycleInterface $testLifecycle = null; 64 | 65 | /** 66 | * {@inheritDoc} 67 | * 68 | * @throws ConfigurationException 69 | * phpcs:disable PSR2.Methods.MethodDeclaration.Underscore 70 | */ 71 | public function _initialize(): void 72 | { 73 | parent::_initialize(); 74 | $this->reconfigure(); 75 | } 76 | 77 | /** 78 | * @throws ConfigurationException 79 | */ 80 | public function moduleInit(): void 81 | { 82 | $this->reconfigure(); 83 | } 84 | 85 | private function reconfigure(): void 86 | { 87 | QametaAllure::reset(); 88 | $this->testLifecycle = null; 89 | $this->threadDetector = null; 90 | QametaAllure::getLifecycleConfigurator() 91 | ->setOutputDirectory($this->getOutputDirectory()); 92 | foreach ($this->getLinkTemplates() as $linkType => $linkTemplate) { 93 | QametaAllure::getLifecycleConfigurator()->addLinkTemplate($linkType, $linkTemplate); 94 | } 95 | $this->callSetupHook(); 96 | } 97 | 98 | private function callSetupHook(): void 99 | { 100 | /** 101 | * @var mixed $hookClass 102 | * @psalm-var array $this->config 103 | */ 104 | $hookClass = $this->config[self::SETUP_HOOK_PARAMETER] ?? ''; 105 | /** @psalm-suppress MixedMethodCall */ 106 | $hook = is_string($hookClass) && class_exists($hookClass) 107 | ? new $hookClass() 108 | : null; 109 | 110 | if (is_callable($hook)) { 111 | $hook(); 112 | } 113 | } 114 | 115 | /** 116 | * @throws ConfigurationException 117 | */ 118 | private function getOutputDirectory(): string 119 | { 120 | /** 121 | * @var mixed $outputCfg 122 | * @psalm-var array $this->config 123 | */ 124 | $outputCfg = $this->config[self::OUTPUT_DIRECTORY_PARAMETER] ?? null; 125 | $outputLocal = is_string($outputCfg) 126 | ? trim($outputCfg, '\\/') 127 | : null; 128 | 129 | return Configuration::outputDir() . ($outputLocal ?? self::DEFAULT_RESULTS_DIRECTORY) . DIRECTORY_SEPARATOR; 130 | } 131 | 132 | /** 133 | * @psalm-suppress MoreSpecificReturnType 134 | * @return iterable 135 | */ 136 | private function getLinkTemplates(): iterable 137 | { 138 | /** 139 | * @var mixed $templatesConfig 140 | * @psalm-var array $this->config 141 | */ 142 | $templatesConfig = $this->config[self::LINK_TEMPLATES_PARAMETER] ?? []; 143 | if (!is_array($templatesConfig)) { 144 | $templatesConfig = []; 145 | } 146 | foreach ($templatesConfig as $linkTypeName => $linkConfig) { 147 | if (!is_string($linkConfig) || !is_string($linkTypeName)) { 148 | continue; 149 | } 150 | yield LinkType::fromOptionalString($linkTypeName) => 151 | class_exists($linkConfig) && is_a($linkConfig, LinkTemplateInterface::class, true) 152 | ? new $linkConfig() 153 | : new LinkTemplate($linkConfig); 154 | } 155 | } 156 | 157 | /** 158 | * @psalm-suppress MissingDependency 159 | */ 160 | public function suiteBefore(SuiteEvent $suiteEvent): void 161 | { 162 | /** @psalm-suppress InternalMethod */ 163 | $suiteName = $suiteEvent->getSuite()?->getName(); 164 | if (!isset($suiteName)) { 165 | return; 166 | } 167 | 168 | $this 169 | ->getTestLifecycle() 170 | ->switchToSuite(new SuiteInfo($suiteName)); 171 | } 172 | 173 | public function suiteAfter(): void 174 | { 175 | $this 176 | ->getTestLifecycle() 177 | ->resetSuite(); 178 | } 179 | 180 | /** 181 | * @psalm-suppress MissingDependency 182 | */ 183 | public function testStart(TestEvent $testEvent): void 184 | { 185 | $test = $testEvent->getTest(); 186 | $this 187 | ->getTestLifecycle() 188 | ->switchToTest($test) 189 | ->create() 190 | ->updateTest() 191 | ->startTest(); 192 | } 193 | 194 | private function getThreadDetector(): ThreadDetectorInterface 195 | { 196 | return $this->threadDetector ??= new DefaultThreadDetector(); 197 | } 198 | 199 | /** 200 | * @psalm-suppress MissingDependency 201 | */ 202 | public function testError(FailEvent $failEvent): void 203 | { 204 | $this 205 | ->getTestLifecycle() 206 | ->switchToTest($failEvent->getTest()) 207 | ->updateTestFailure( 208 | $failEvent->getFail(), 209 | Status::broken(), 210 | ); 211 | } 212 | 213 | /** 214 | * @psalm-suppress MissingDependency 215 | */ 216 | public function testFail(FailEvent $failEvent): void 217 | { 218 | $error = $failEvent->getFail(); 219 | $this 220 | ->getTestLifecycle() 221 | ->switchToTest($failEvent->getTest()) 222 | ->updateTestFailure( 223 | $failEvent->getFail(), 224 | Status::failed(), 225 | new StatusDetails(message: $error->getMessage(), trace: $error->getTraceAsString()), 226 | ); 227 | } 228 | 229 | /** 230 | * @psalm-suppress MissingDependency 231 | */ 232 | public function testIncomplete(FailEvent $failEvent): void 233 | { 234 | $error = $failEvent->getFail(); 235 | $this 236 | ->getTestLifecycle() 237 | ->switchToTest($failEvent->getTest()) 238 | ->updateTestFailure( 239 | $error, 240 | Status::broken(), 241 | new StatusDetails(message: $error->getMessage(), trace: $error->getTraceAsString()), 242 | ); 243 | } 244 | 245 | /** 246 | * @psalm-suppress MissingDependency 247 | */ 248 | public function testSkipped(FailEvent $failEvent): void 249 | { 250 | $error = $failEvent->getFail(); 251 | $this 252 | ->getTestLifecycle() 253 | ->switchToTest($failEvent->getTest()) 254 | ->updateTestFailure( 255 | $error, 256 | Status::skipped(), 257 | new StatusDetails(message: $error->getMessage(), trace: $error->getTraceAsString()), 258 | ); 259 | } 260 | 261 | /** 262 | * @psalm-suppress MissingDependency 263 | */ 264 | public function testSuccess(TestEvent $testEvent): void 265 | { 266 | $this 267 | ->getTestLifecycle() 268 | ->switchToTest($testEvent->getTest()) 269 | ->updateTestSuccess(); 270 | } 271 | 272 | /** 273 | * @psalm-suppress MissingDependency 274 | */ 275 | public function testEnd(TestEvent $testEvent): void 276 | { 277 | $this 278 | ->getTestLifecycle() 279 | ->switchToTest($testEvent->getTest()) 280 | ->updateTestResult() 281 | ->attachReports() 282 | ->stopTest(); 283 | } 284 | 285 | /** 286 | * @psalm-suppress MissingDependency 287 | */ 288 | public function stepBefore(StepEvent $stepEvent): void 289 | { 290 | $this 291 | ->getTestLifecycle() 292 | ->switchToTest($stepEvent->getTest()) 293 | ->startStep($stepEvent->getStep()) 294 | ->updateStep(); 295 | } 296 | 297 | /** 298 | * @psalm-suppress MissingDependency 299 | */ 300 | public function stepAfter(StepEvent $stepEvent): void 301 | { 302 | $this 303 | ->getTestLifecycle() 304 | ->switchToTest($stepEvent->getTest()) 305 | ->switchToStep($stepEvent->getStep()) 306 | ->updateStepResult() 307 | ->stopStep(); 308 | } 309 | 310 | private function getTestLifecycle(): TestLifecycleInterface 311 | { 312 | return $this->testLifecycle ??= new TestLifecycle( 313 | Allure::getLifecycle(), 314 | Allure::getConfig()->getResultFactory(), 315 | Allure::getConfig()->getStatusDetector(), 316 | $this->getThreadDetector(), 317 | Allure::getConfig()->getLinkTemplates(), 318 | $_ENV, 319 | ); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/Internal/ArgumentAsString.php: -------------------------------------------------------------------------------- 1 | $this->prepareString($argument), 41 | is_resource($argument) => $this->prepareResource($argument), 42 | is_array($argument) => $this->prepareArray($argument), 43 | is_object($argument) => $this->prepareObject($argument), 44 | default => $argument, 45 | }; 46 | } 47 | 48 | private function prepareString(string $argument): string 49 | { 50 | return strtr($argument, ["\n" => '\n', "\r" => '\r', "\t" => ' ']); 51 | } 52 | 53 | /** 54 | * @param resource $argument 55 | * @return string 56 | */ 57 | private function prepareResource($argument): string 58 | { 59 | return (string) $argument; 60 | } 61 | 62 | private function prepareArray(array $argument): array 63 | { 64 | return array_map( 65 | fn(mixed $element): mixed => $this->prepareArgument($element), 66 | $argument, 67 | ); 68 | } 69 | 70 | private function isClosure(object $argument): bool 71 | { 72 | return $argument instanceof \Closure; 73 | } 74 | 75 | private function prepareObject(object $argument): string 76 | { 77 | if (!$this->isClosure($argument) && isset($argument->__mocked) && is_object($argument->__mocked)) { 78 | $argument = $argument->__mocked; 79 | } 80 | if ($argument instanceof Stringable) { 81 | return (string) $argument; 82 | } 83 | $webdriverByClass = '\Facebook\WebDriver\WebDriverBy'; 84 | if (class_exists($webdriverByClass) && is_a($argument, $webdriverByClass)) { 85 | return $this->webDriverByAsString($argument); 86 | } 87 | 88 | return trim($argument::class, "\\"); 89 | } 90 | 91 | public function __toString(): string 92 | { 93 | return json_encode( 94 | $this->prepareArgument($this->argument), 95 | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, 96 | ); 97 | } 98 | 99 | private function webDriverByAsString(object $selector): string 100 | { 101 | $type = method_exists($selector, 'getMechanism') 102 | ? (string) $selector->getMechanism() 103 | : null; 104 | 105 | $locator = method_exists($selector, 'getValue') 106 | ? (string) $selector->getValue() 107 | : null; 108 | 109 | if (!isset($type, $locator)) { 110 | throw new InvalidArgumentException("Unrecognized selector"); 111 | } 112 | 113 | return "$type '$locator'"; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Internal/CeptInfoBuilder.php: -------------------------------------------------------------------------------- 1 | test, 20 | signature: $this->test->getSignature(), 21 | class: $this->test->getName(), 22 | method: $this->test->getName(), 23 | host: $host, 24 | thread: $thread, 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Internal/CeptProvider.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | private array $legacyLabels = []; 36 | 37 | /** 38 | * @var list 39 | */ 40 | private array $legacyLinks = []; 41 | 42 | private ?string $legacyTitle = null; 43 | 44 | private ?string $legacyDescription = null; 45 | 46 | /** 47 | * @param Cept $test 48 | * @param LinkTemplateCollectionInterface $linkTemplates 49 | */ 50 | public function __construct( 51 | private Cept $test, 52 | private LinkTemplateCollectionInterface $linkTemplates, 53 | ) { 54 | } 55 | 56 | /** 57 | * @param Cept $test 58 | * @param LinkTemplateCollectionInterface $linkTemplates 59 | * @return list 60 | */ 61 | public static function createForChain(Cept $test, LinkTemplateCollectionInterface $linkTemplates): array 62 | { 63 | return [new self($test, $linkTemplates)]; 64 | } 65 | 66 | public function getLinks(): array 67 | { 68 | $this->loadLegacyModels(); 69 | 70 | return $this->legacyLinks; 71 | } 72 | 73 | public function getLabels(): array 74 | { 75 | $this->loadLegacyModels(); 76 | 77 | return $this->legacyLabels; 78 | } 79 | 80 | public function getParameters(): array 81 | { 82 | return []; 83 | } 84 | 85 | public function getDisplayName(): ?string 86 | { 87 | $this->loadLegacyModels(); 88 | 89 | if (isset($this->legacyTitle)) { 90 | return $this->legacyTitle; 91 | } 92 | 93 | /** @psalm-var mixed $testName */ 94 | $testName = $this->test->getName(); 95 | 96 | return is_string($testName) 97 | ? $testName 98 | : null; 99 | } 100 | 101 | public function getFullName(): ?string 102 | { 103 | return $this->test->getSignature(); 104 | } 105 | 106 | public function getDescription(): ?string 107 | { 108 | $this->loadLegacyModels(); 109 | 110 | return $this->legacyDescription; 111 | } 112 | 113 | public function getDescriptionHtml(): ?string 114 | { 115 | return null; 116 | } 117 | 118 | private function getLegacyAnnotation(string $name): ?string 119 | { 120 | /** 121 | * @psalm-var mixed $annotations 122 | * @psalm-suppress InvalidArgument 123 | */ 124 | $annotations = $this->test->getMetadata()->getParam($name); 125 | if (!is_array($annotations)) { 126 | return null; 127 | } 128 | /** @var mixed $lastAnnotation */ 129 | $lastAnnotation = array_pop($annotations); 130 | 131 | return is_string($lastAnnotation) 132 | ? $this->getStringFromTagContent(trim($lastAnnotation, '()')) 133 | : null; 134 | } 135 | 136 | /** 137 | * @param string $name 138 | * @return list 139 | */ 140 | private function getLegacyAnnotations(string $name): array 141 | { 142 | /** 143 | * @psalm-var mixed $annotations 144 | * @psalm-suppress InvalidArgument 145 | */ 146 | $annotations = $this->test->getMetadata()->getParam($name); 147 | $stringAnnotations = is_array($annotations) 148 | ? array_values(array_filter($annotations, 'is_string')) 149 | : []; 150 | 151 | return array_merge( 152 | ...array_map( 153 | fn (string $annotation) => $this->getStringsFromTagContent(trim($annotation, '()')), 154 | $stringAnnotations, 155 | ), 156 | ); 157 | } 158 | 159 | private function loadLegacyModels(): void 160 | { 161 | if ($this->isLoaded) { 162 | return; 163 | } 164 | $this->isLoaded = true; 165 | 166 | $this->legacyTitle = $this->getLegacyAnnotation('Title'); 167 | $this->legacyDescription = $this->getLegacyAnnotation('Description'); 168 | $this->legacyLabels = [ 169 | ...array_map( 170 | fn (string $value): Label => Label::feature($value), 171 | $this->getLegacyAnnotations('Features'), 172 | ), 173 | ...array_map( 174 | fn (string $value): Label => Label::story($value), 175 | $this->getLegacyAnnotations('Stories'), 176 | ), 177 | ]; 178 | $linkTemplate = $this->linkTemplates->get(LinkType::issue()) ?? null; 179 | $this->legacyLinks = array_map( 180 | fn (string $value): Link => Link::issue($value, $linkTemplate?->buildUrl($value)), 181 | $this->getLegacyAnnotations('Issues'), 182 | ); 183 | } 184 | 185 | private function getStringFromTagContent(string $tagContent): string 186 | { 187 | return str_replace('"', '', $tagContent); 188 | } 189 | 190 | /** 191 | * @param string $string 192 | * @return list 193 | */ 194 | private function getStringsFromTagContent(string $string): array 195 | { 196 | $detected = str_replace(['{', '}', '"'], '', $string); 197 | 198 | return explode(',', $detected); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Internal/CestInfoBuilder.php: -------------------------------------------------------------------------------- 1 | test, 23 | signature: $this->test->getSignature(), 24 | class: $this->test->getTestInstance()::class, 25 | method: $this->test->getTestMethod(), 26 | dataLabel: $this->getDataLabel(), 27 | host: $host, 28 | thread: $thread, 29 | ); 30 | } 31 | 32 | private function getDataLabel(): ?string 33 | { 34 | /** @psalm-var mixed $index */ 35 | $index = $this->test->getMetadata()->getIndex(); 36 | 37 | if (is_string($index)) { 38 | return $index; 39 | } 40 | if (is_int($index)) { 41 | return "#$index"; 42 | } 43 | 44 | return null; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Internal/CestProvider.php: -------------------------------------------------------------------------------- 1 | 35 | * @throws ReflectionException 36 | */ 37 | public static function createForChain(Cest $test, LinkTemplateCollectionInterface $linkTemplates): array 38 | { 39 | /** @psalm-var callable-string $callableTestMethod */ 40 | $callableTestMethod = $test->getTestMethod(); 41 | 42 | return [ 43 | ...AttributeParser::createForChain( 44 | classOrObject: $test->getTestInstance(), 45 | methodOrFunction: $callableTestMethod, 46 | linkTemplates: $linkTemplates, 47 | ), 48 | new self($test), 49 | ]; 50 | } 51 | 52 | public function getLinks(): array 53 | { 54 | return []; 55 | } 56 | 57 | public function getLabels(): array 58 | { 59 | return []; 60 | } 61 | 62 | public function getParameters(): array 63 | { 64 | /** @var mixed $currentExample */ 65 | $currentExample = $this 66 | ->test 67 | ->getMetadata() 68 | ->getCurrent('example') ?? []; 69 | if (!is_array($currentExample)) { 70 | return []; 71 | } 72 | 73 | return array_map( 74 | fn (mixed $value, int|string $name) => new Parameter( 75 | is_int($name) ? "#$name" : $name, 76 | ArgumentAsString::get($value), 77 | ), 78 | array_values($currentExample), 79 | array_keys($currentExample), 80 | ); 81 | } 82 | 83 | public function getDisplayName(): ?string 84 | { 85 | /** @psalm-var mixed $displayName */ 86 | $displayName = $this->test->getName(); 87 | 88 | return is_string($displayName) 89 | ? $displayName 90 | : null; 91 | } 92 | 93 | public function getDescription(): ?string 94 | { 95 | return null; 96 | } 97 | 98 | public function getDescriptionHtml(): ?string 99 | { 100 | return null; 101 | } 102 | 103 | /** 104 | * @psalm-suppress MixedOperand 105 | * @psalm-suppress MixedArgument 106 | */ 107 | public function getFullName(): ?string 108 | { 109 | return $this->test->getTestInstance()::class . "::" . $this->test->getTestMethod(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Internal/DefaultThreadDetector.php: -------------------------------------------------------------------------------- 1 | host ??= gethostname(); 18 | 19 | return $this->host === false 20 | ? null 21 | : $this->host; 22 | } 23 | 24 | public function getThread(): ?string 25 | { 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Internal/GherkinInfoBuilder.php: -------------------------------------------------------------------------------- 1 | test, 22 | signature: $this->test->getSignature(), 23 | class: $this->test->getFeature(), 24 | method: $this->test->getScenarioTitle(), 25 | host: $host, 26 | thread: $thread, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Internal/GherkinProvider.php: -------------------------------------------------------------------------------- 1 | Label::feature($value), 38 | [ 39 | ...array_values($this->test->getFeatureNode()->getTags()), 40 | ...array_values($this->test->getScenarioNode()->getTags()), 41 | ], 42 | ); 43 | } 44 | 45 | public function getParameters(): array 46 | { 47 | return []; 48 | } 49 | 50 | public function getDisplayName(): ?string 51 | { 52 | return $this->test->toString(); 53 | } 54 | 55 | public function getDescription(): ?string 56 | { 57 | return null; 58 | } 59 | 60 | public function getDescriptionHtml(): ?string 61 | { 62 | return null; 63 | } 64 | 65 | public function getFullName(): ?string 66 | { 67 | return null; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Internal/StepStartInfo.php: -------------------------------------------------------------------------------- 1 | originalStep; 20 | } 21 | 22 | public function getUuid(): string 23 | { 24 | return $this->uuid; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Internal/SuiteInfo.php: -------------------------------------------------------------------------------- 1 | name; 22 | } 23 | 24 | /** 25 | * @return class-string|null 26 | */ 27 | public function getClass(): ?string 28 | { 29 | return class_exists($this->name, false) 30 | ? $this->name 31 | : null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Internal/SuiteProvider.php: -------------------------------------------------------------------------------- 1 | 27 | * @throws ReflectionException 28 | */ 29 | public static function createForChain( 30 | ?SuiteInfo $suiteInfo, 31 | LinkTemplateCollectionInterface $linkTemplates, 32 | ): array { 33 | $providers = [new self($suiteInfo)]; 34 | $suiteClass = $suiteInfo?->getClass(); 35 | 36 | return isset($suiteClass) 37 | ? [ 38 | ...$providers, 39 | ...AttributeParser::createForChain(classOrObject: $suiteClass, linkTemplates: $linkTemplates), 40 | ] 41 | : $providers; 42 | } 43 | 44 | public function getLinks(): array 45 | { 46 | return []; 47 | } 48 | 49 | public function getLabels(): array 50 | { 51 | return [ 52 | Label::language(null), 53 | Label::framework('codeception'), 54 | Label::parentSuite($this->suiteInfo?->getName()), 55 | Label::package($this->suiteInfo?->getName()), 56 | ]; 57 | } 58 | 59 | public function getParameters(): array 60 | { 61 | return []; 62 | } 63 | 64 | public function getDisplayName(): ?string 65 | { 66 | return null; 67 | } 68 | 69 | public function getDescription(): ?string 70 | { 71 | return null; 72 | } 73 | 74 | public function getDescriptionHtml(): ?string 75 | { 76 | return null; 77 | } 78 | 79 | public function getFullName(): ?string 80 | { 81 | return null; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Internal/TestInfo.php: -------------------------------------------------------------------------------- 1 | originalTest; 23 | } 24 | 25 | public function getSignature(): string 26 | { 27 | return $this->signature; 28 | } 29 | 30 | public function getClass(): ?string 31 | { 32 | return $this->class; 33 | } 34 | 35 | public function getMethod(): ?string 36 | { 37 | return $this->method; 38 | } 39 | 40 | public function getDataLabel(): ?string 41 | { 42 | return $this->dataLabel; 43 | } 44 | 45 | public function getHost(): ?string 46 | { 47 | return $this->host; 48 | } 49 | 50 | public function getThread(): ?string 51 | { 52 | return $this->thread; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Internal/TestInfoBuilderInterface.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public static function createForChain(TestInfo $info): array 22 | { 23 | return [new self($info)]; 24 | } 25 | 26 | public function getLinks(): array 27 | { 28 | return []; 29 | } 30 | 31 | public function getLabels(): array 32 | { 33 | return [ 34 | Label::testClass($this->info->getClass()), 35 | Label::testMethod($this->info->getMethod()), 36 | Label::host($this->info->getHost()), 37 | Label::thread($this->info->getThread()), 38 | ]; 39 | } 40 | 41 | public function getParameters(): array 42 | { 43 | return []; 44 | } 45 | 46 | public function getDisplayName(): ?string 47 | { 48 | return null; 49 | } 50 | 51 | public function getDescription(): ?string 52 | { 53 | return null; 54 | } 55 | 56 | public function getDescriptionHtml(): ?string 57 | { 58 | return null; 59 | } 60 | 61 | public function getFullName(): ?string 62 | { 63 | return null; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Internal/TestLifecycle.php: -------------------------------------------------------------------------------- 1 | 47 | */ 48 | private WeakMap $stepStarts; 49 | 50 | public function __construct( 51 | private AllureLifecycleInterface $lifecycle, 52 | private ResultFactoryInterface $resultFactory, 53 | private StatusDetectorInterface $statusDetector, 54 | private ThreadDetectorInterface $threadDetector, 55 | private LinkTemplateCollectionInterface $linkTemplates, 56 | private array $env, 57 | ) { 58 | /** @psalm-var WeakMap $this->stepStarts */ 59 | $this->stepStarts = new WeakMap(); 60 | } 61 | 62 | public function getCurrentSuite(): SuiteInfo 63 | { 64 | return $this->currentSuite ?? throw new RuntimeException("Current suite not found"); 65 | } 66 | 67 | public function getCurrentTest(): TestInfo 68 | { 69 | return $this->currentTest ?? throw new RuntimeException("Current test not found"); 70 | } 71 | 72 | public function getCurrentTestStart(): TestStartInfo 73 | { 74 | return $this->currentTestStart ?? throw new RuntimeException("Current test start not found"); 75 | } 76 | 77 | public function getCurrentStepStart(): StepStartInfo 78 | { 79 | return $this->currentStepStart ?? throw new RuntimeException("Current step start not found"); 80 | } 81 | 82 | public function switchToSuite(SuiteInfo $suiteInfo): self 83 | { 84 | $this->currentSuite = $suiteInfo; 85 | 86 | return $this; 87 | } 88 | 89 | public function resetSuite(): self 90 | { 91 | $this->currentSuite = null; 92 | 93 | return $this; 94 | } 95 | 96 | public function switchToTest(object $test): self 97 | { 98 | $thread = $this->threadDetector->getThread(); 99 | $this->lifecycle->switchThread($thread); 100 | 101 | $this->currentTest = $this 102 | ->getTestInfoBuilder($test) 103 | ->build( 104 | $this->threadDetector->getHost(), 105 | $thread, 106 | ); 107 | 108 | return $this; 109 | } 110 | 111 | private function getTestInfoBuilder(object $test): TestInfoBuilderInterface 112 | { 113 | return match (true) { 114 | $test instanceof Cest => new CestInfoBuilder($test), 115 | $test instanceof Gherkin => new GherkinInfoBuilder($test), 116 | $test instanceof Cept => new CeptInfoBuilder($test), 117 | $test instanceof TestCaseWrapper => new UnitInfoBuilder($test), 118 | default => new UnknownInfoBuilder($test), 119 | }; 120 | } 121 | 122 | public function create(): self 123 | { 124 | $containerResult = $this->resultFactory->createContainer(); 125 | $this->lifecycle->startContainer($containerResult); 126 | 127 | $testResult = $this->resultFactory->createTest(); 128 | $this->lifecycle->scheduleTest($testResult, $containerResult->getUuid()); 129 | 130 | $this->currentTestStart = new TestStartInfo( 131 | containerUuid: $containerResult->getUuid(), 132 | testUuid: $testResult->getUuid(), 133 | ); 134 | 135 | return $this; 136 | } 137 | 138 | public function updateTest(): self 139 | { 140 | $provider = new ModelProviderChain( 141 | new EnvProvider($this->env), 142 | ...SuiteProvider::createForChain($this->getCurrentSuite(), $this->linkTemplates), 143 | ...TestInfoProvider::createForChain($this->getCurrentTest()), 144 | ...$this->createModelProvidersForTest($this->getCurrentTest()->getOriginalTest()), 145 | ); 146 | $this->lifecycle->updateTest( 147 | fn (TestResult $t) => $t 148 | ->setName($provider->getDisplayName()) 149 | ->setFullName($provider->getFullName()) 150 | ->setDescription($provider->getDescription()) 151 | ->setDescriptionHtml($provider->getDescriptionHtml()) 152 | ->addLinks(...$provider->getLinks()) 153 | ->addLabels(...$provider->getLabels()) 154 | ->addParameters(...$provider->getParameters()), 155 | $this->getCurrentTestStart()->getTestUuid(), 156 | ); 157 | 158 | return $this; 159 | } 160 | 161 | private function createModelProvidersForTest(mixed $test): array 162 | { 163 | return match (true) { 164 | $test instanceof Cest => CestProvider::createForChain($test, $this->linkTemplates), 165 | $test instanceof Gherkin => GherkinProvider::createForChain($test), 166 | $test instanceof Cept => CeptProvider::createForChain($test, $this->linkTemplates), 167 | $test instanceof TestCaseWrapper => UnitProvider::createForChain($test, $this->linkTemplates), 168 | default => [], 169 | }; 170 | } 171 | 172 | public function startTest(): self 173 | { 174 | $this->lifecycle->startTest($this->getCurrentTestStart()->getTestUuid()); 175 | 176 | return $this; 177 | } 178 | 179 | public function stopTest(): self 180 | { 181 | $testUuid = $this->getCurrentTestStart()->getTestUuid(); 182 | $this 183 | ->lifecycle 184 | ->stopTest($testUuid); 185 | $this->lifecycle->writeTest($testUuid); 186 | 187 | $containerUuid = $this->getCurrentTestStart()->getContainerUuid(); 188 | $this 189 | ->lifecycle 190 | ->stopContainer($containerUuid); 191 | $this->lifecycle->writeContainer($containerUuid); 192 | 193 | $this->currentTest = null; 194 | $this->currentTestStart = null; 195 | 196 | return $this; 197 | } 198 | 199 | public function updateTestFailure( 200 | Throwable $error, 201 | ?Status $status = null, 202 | ?StatusDetails $statusDetails = null, 203 | ): self { 204 | $this->lifecycle->updateTest( 205 | fn (TestResult $t) => $t 206 | ->setStatus($status ?? $this->statusDetector->getStatus($error)) 207 | ->setStatusDetails($statusDetails ?? $this->statusDetector->getStatusDetails($error)), 208 | ); 209 | 210 | return $this; 211 | } 212 | 213 | public function updateTestSuccess(): self 214 | { 215 | $this->lifecycle->updateTest( 216 | fn (TestResult $t) => $t->setStatus(Status::passed()), 217 | ); 218 | 219 | return $this; 220 | } 221 | 222 | public function attachReports(): self 223 | { 224 | $originalTest = $this->getCurrentTest()->getOriginalTest(); 225 | if ($originalTest instanceof TestInterface) { 226 | $artifacts = $originalTest->getMetadata()->getReports(); 227 | /** 228 | * @psalm-var mixed $artifact 229 | */ 230 | foreach ($artifacts as $name => $artifact) { 231 | $attachment = $this 232 | ->resultFactory 233 | ->createAttachment() 234 | ->setName((string) $name); 235 | if (!is_string($artifact)) { 236 | continue; 237 | } 238 | $dataSource = @file_exists($artifact) && is_file($artifact) 239 | ? DataSourceFactory::fromFile($artifact) 240 | : DataSourceFactory::fromString($artifact); 241 | $this 242 | ->lifecycle 243 | ->addAttachment($attachment, $dataSource); 244 | } 245 | } 246 | 247 | return $this; 248 | } 249 | 250 | public function updateTestResult(): self 251 | { 252 | $this->lifecycle->updateTest( 253 | fn (TestResult $t) => $t 254 | ->setTestCaseId($testCaseId = $this->buildTestCaseId($this->getCurrentTest(), ...$t->getParameters())) 255 | ->setHistoryId($this->buildHistoryId($testCaseId, $this->getCurrentTest(), ...$t->getParameters())), 256 | $this->getCurrentTestStart()->getTestUuid(), 257 | ); 258 | 259 | return $this; 260 | } 261 | 262 | private function buildTestCaseId(TestInfo $testInfo, Parameter ...$parameters): string 263 | { 264 | $parameterNames = implode( 265 | '::', 266 | array_map( 267 | fn (Parameter $parameter): string => $parameter->getName(), 268 | array_filter( 269 | $parameters, 270 | fn (Parameter $parameter): bool => !$parameter->getExcluded(), 271 | ), 272 | ), 273 | ); 274 | 275 | return md5("{$testInfo->getSignature()}::$parameterNames"); 276 | } 277 | 278 | private function buildHistoryId(string $testCaseId, TestInfo $testInfo, Parameter ...$parameters): string 279 | { 280 | $parameterNames = implode( 281 | '::', 282 | array_map( 283 | fn (Parameter $parameter): string => $parameter->getValue() ?? '', 284 | array_filter( 285 | $parameters, 286 | fn (Parameter $parameter): bool => !$parameter->getExcluded(), 287 | ), 288 | ), 289 | ); 290 | 291 | return md5("$testCaseId::{$testInfo->getSignature()}::$parameterNames"); 292 | } 293 | 294 | public function startStep(Step $step): self 295 | { 296 | $stepResult = $this->resultFactory->createStep(); 297 | $this->lifecycle->startStep($stepResult); 298 | 299 | $stepStart = new StepStartInfo( 300 | $step, 301 | $stepResult->getUuid(), 302 | ); 303 | $this->stepStarts[$step] = $stepStart; 304 | $this->currentStepStart = $stepStart; 305 | 306 | return $this; 307 | } 308 | 309 | public function switchToStep(Step $step): self 310 | { 311 | $this->currentStepStart = 312 | $this->stepStarts[$step] ?? throw new RuntimeException("Step start info not found"); 313 | 314 | return $this; 315 | } 316 | 317 | public function stopStep(): self 318 | { 319 | $stepStart = $this->getCurrentStepStart(); 320 | $this->lifecycle->stopStep($stepStart->getUuid()); 321 | /** 322 | * @psalm-var Step $step 323 | * @psalm-var StepStartInfo $storedStart 324 | */ 325 | foreach ($this->stepStarts as $step => $storedStart) { 326 | if ($storedStart === $stepStart) { 327 | unset($this->stepStarts[$step]); 328 | } 329 | } 330 | $this->currentStepStart = null; 331 | 332 | return $this; 333 | } 334 | 335 | public function updateStep(): self 336 | { 337 | $stepStart = $this->getCurrentStepStart(); 338 | $step = $stepStart->getOriginalStep(); 339 | 340 | $params = []; 341 | /** @psalm-var mixed $value */ 342 | foreach ($step->getArguments() as $name => $value) { 343 | $params[] = new Parameter( 344 | is_int($name) ? "#$name" : $name, 345 | ArgumentAsString::get($value), 346 | ); 347 | } 348 | /** @var mixed $humanizedAction */ 349 | $humanizedAction = $step->getHumanizedActionWithoutArguments(); 350 | $this->lifecycle->updateStep( 351 | fn (StepResult $s) => $s 352 | ->setName(is_string($humanizedAction) ? $humanizedAction : null) 353 | ->setParameters(...$params), 354 | $stepStart->getUuid(), 355 | ); 356 | 357 | return $this; 358 | } 359 | 360 | public function updateStepResult(): self 361 | { 362 | $this->lifecycle->updateStep( 363 | fn (StepResult $s) => $s 364 | ->setStatus( 365 | $this->getCurrentStepStart()->getOriginalStep()->hasFailed() 366 | ? Status::failed() 367 | : Status::passed(), 368 | ), 369 | ); 370 | 371 | return $this; 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /src/Internal/TestLifecycleInterface.php: -------------------------------------------------------------------------------- 1 | containerUuid; 18 | } 19 | 20 | public function getTestUuid(): string 21 | { 22 | return $this->testUuid; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Internal/UnitInfoBuilder.php: -------------------------------------------------------------------------------- 1 | test->getReportFields(); 22 | $index = $this->test->getMetadata()->getIndex(); 23 | $dataLabel = is_int($index) ? "#$index" : $index; 24 | 25 | return new TestInfo( 26 | originalTest: $this->test, 27 | signature: $this->test->getSignature(), 28 | class: $fields['class'] ?? null, 29 | method: $this->test->getMetadata()->getName(), 30 | dataLabel: $dataLabel, 31 | host: $host, 32 | thread: $thread, 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Internal/UnitProvider.php: -------------------------------------------------------------------------------- 1 | 39 | */ 40 | public static function createForChain(TestCaseWrapper $test, LinkTemplateCollectionInterface $linkTemplates): array 41 | { 42 | $fields = $test->getReportFields(); 43 | /** @var class-string $class */ 44 | $class = $fields['class'] ?? null; 45 | /** @var Closure|callable-string|null $methodOrFunction */ 46 | $methodOrFunction = $test->getMetadata()->getName(); 47 | 48 | return [ 49 | ...AttributeParser::createForChain( 50 | classOrObject: $class, 51 | methodOrFunction: $methodOrFunction, 52 | linkTemplates: $linkTemplates, 53 | ), 54 | new self($test, $linkTemplates), 55 | ]; 56 | } 57 | 58 | public function getLinks(): array 59 | { 60 | return []; 61 | } 62 | 63 | public function getLabels(): array 64 | { 65 | return []; 66 | } 67 | 68 | /** 69 | * @throws ReflectionException 70 | */ 71 | public function getParameters(): array 72 | { 73 | $testMetadata = $this->test->getMetadata(); 74 | if (null === $testMetadata->getIndex()) { 75 | return []; 76 | } 77 | 78 | $testCase = $this->test->getTestCase(); 79 | 80 | $dataMethod = new ReflectionMethod($testCase, 'getProvidedData'); 81 | $dataMethod->setAccessible(true); 82 | $methodName = $testMetadata->getName(); 83 | $testMethod = new ReflectionMethod($testCase, $methodName); 84 | $argNames = $testMethod->getParameters(); 85 | 86 | $params = []; 87 | /** 88 | * @var array-key $key 89 | * @var mixed $param 90 | */ 91 | foreach ($dataMethod->invoke($testCase) as $key => $param) { 92 | $argName = array_shift($argNames); 93 | $name = $argName?->getName() ?? $key; 94 | $params[] = new Parameter( 95 | is_int($name) ? "#$name" : $name, 96 | ArgumentAsString::get($param), 97 | ); 98 | } 99 | 100 | return $params; 101 | } 102 | 103 | public function getDisplayName(): ?string 104 | { 105 | return $this->test->getMetadata()->getName(); 106 | } 107 | 108 | public function getDescription(): ?string 109 | { 110 | return null; 111 | } 112 | 113 | public function getDescriptionHtml(): ?string 114 | { 115 | return null; 116 | } 117 | 118 | public function getFullName(): ?string 119 | { 120 | return $this->test->getTestCase()::class . '::' . $this->test->getMetadata()->getName(); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Internal/UnknownInfoBuilder.php: -------------------------------------------------------------------------------- 1 | test, 18 | signature: 'Unknown test: ' . $this->test::class, 19 | host: $host, 20 | thread: $thread, 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Setup/ThreadDetectorInterface.php: -------------------------------------------------------------------------------- 1 | getUnwrappedStatus( 29 | $this->unwrapError($error), 30 | ); 31 | } 32 | 33 | private function getUnwrappedStatus(Throwable $error): Status 34 | { 35 | return match (true) { 36 | $error instanceof SkippedTestError => Status::skipped(), 37 | $error instanceof AssertionFailedError => Status::failed(), 38 | default => Status::broken(), 39 | }; 40 | } 41 | 42 | public function getStatusDetails(Throwable $error): ?StatusDetails 43 | { 44 | $unwrappedError = $this->unwrapError($error); 45 | $unwrappedStatus = $this->getUnwrappedStatus($unwrappedError); 46 | 47 | return match (true) { 48 | Status::skipped() === $unwrappedStatus, 49 | Status::failed() === $unwrappedStatus => new StatusDetails( 50 | message: $error->getMessage(), 51 | trace: $error->getTraceAsString(), 52 | ), 53 | default => $this->defaultStatusDetector->getStatusDetails($unwrappedError), 54 | }; 55 | } 56 | 57 | private function unwrapError(Throwable $error): Throwable 58 | { 59 | /** @psalm-suppress InternalMethod */ 60 | return $error instanceof ExceptionWrapper 61 | ? $error->getOriginalException() ?? $error 62 | : $error; 63 | } 64 | 65 | private function buildMessage(Throwable $error): string 66 | { 67 | /** @psalm-suppress InternalMethod */ 68 | return $error instanceof AssertionFailedError 69 | ? $error->toString() 70 | : $error->getMessage(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/codeception-report/_support/AcceptanceTester.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | private array $inputs = []; 37 | 38 | /** 39 | * @var list 40 | */ 41 | private array $outputs = []; 42 | 43 | /** 44 | * @Given I have input as :num 45 | */ 46 | public function iHaveInputAs($num) 47 | { 48 | $this->inputs = [$num]; 49 | $this->calculate(); 50 | } 51 | 52 | private function calculate(): void 53 | { 54 | $this->outputs = array_map( 55 | fn (int $num): int => abs($num), 56 | $this->inputs, 57 | ); 58 | } 59 | 60 | /** 61 | * @Then I should get output as :num 62 | */ 63 | public function iShouldGetOutputAs($num) 64 | { 65 | Unit::assertSame([(int) $num], $this->outputs); 66 | } 67 | 68 | /** 69 | * @Given I have no input 70 | */ 71 | public function iHaveNoInput() 72 | { 73 | $this->inputs = []; 74 | $this->calculate(); 75 | } 76 | 77 | /** 78 | * @Given I have inputs 79 | */ 80 | public function iHaveInputs(TableNode $table) 81 | { 82 | $this->inputs = array_map( 83 | fn (array $row): int => (int) $row['num'], 84 | iterator_to_array($table), 85 | ); 86 | $this->calculate(); 87 | } 88 | 89 | /** 90 | * @Then I should get non-negative outputs 91 | */ 92 | public function iShouldGetNonNegativeOutputs() 93 | { 94 | foreach ($this->outputs as $num) { 95 | Unit::assertGreaterThanOrEqual(0, $num); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /test/codeception-report/_support/FunctionalTester.php: -------------------------------------------------------------------------------- 1 | 19 | Then I should get output as 20 | 21 | Examples: 22 | | in | out | 23 | | 1 | 1 | 24 | | 2 | 2 | 25 | 26 | Scenario: various numbers 27 | Given I have inputs 28 | | num | 29 | | -1 | 30 | | 0 | 31 | | 1 | 32 | Then I should get non-negative outputs -------------------------------------------------------------------------------- /test/codeception-report/functional.suite.yml: -------------------------------------------------------------------------------- 1 | 2 | actor: FunctionalTester -------------------------------------------------------------------------------- /test/codeception-report/functional/BasicScenarioCept.php: -------------------------------------------------------------------------------- 1 | expect('some condition'); 13 | -------------------------------------------------------------------------------- /test/codeception-report/functional/ClassTitleCest.php: -------------------------------------------------------------------------------- 1 | expect('some condition'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/codeception-report/functional/CustomizedScenarioCept.php: -------------------------------------------------------------------------------- 1 | expect('some condition'); 20 | -------------------------------------------------------------------------------- /test/codeception-report/functional/NestedStepsCest.php: -------------------------------------------------------------------------------- 1 | expect("condition 1"); 19 | Allure::runStep( 20 | function () use ($I): void { 21 | $I->expect("condition 1.1"); 22 | Allure::runStep( 23 | function () use ($I): void { 24 | $I->expect("condition 1.1.1"); 25 | }, 26 | 'Step 1.1.1', 27 | ); 28 | }, 29 | 'Step 1.1', 30 | ); 31 | Allure::runStep( 32 | function () use ($I): void { 33 | $I->expect("condition 1.2"); 34 | }, 35 | 'Step 1.2', 36 | ); 37 | }, 38 | 'Step 1', 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/codeception-report/functional/NoClassTitleCest.php: -------------------------------------------------------------------------------- 1 | expect("some condition"); 19 | } 20 | 21 | /** 22 | * @example ["condition 1"] 23 | * @example {"condition":"condition 2"} 24 | */ 25 | public function makeActionWithExamples(FunctionalTester $I, Example $example): void 26 | { 27 | $I->expect($example[0] ?? $example['condition']); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/codeception-report/functional/ScenarioWithLegacyAnnotationsCept.php: -------------------------------------------------------------------------------- 1 | expect('some condition'); 21 | -------------------------------------------------------------------------------- /test/codeception-report/unit.suite.yml: -------------------------------------------------------------------------------- 1 | 2 | actor: UnitTester 3 | -------------------------------------------------------------------------------- /test/codeception-report/unit/AnnotationTest.php: -------------------------------------------------------------------------------- 1 | expectNotToPerformAssertions(); 16 | } 17 | 18 | #[Attribute\Description('Test description with `markdown`')] 19 | public function testDescriptionAnnotation(): void 20 | { 21 | $this->expectNotToPerformAssertions(); 22 | } 23 | 24 | #[Attribute\Severity(Attribute\Severity::MINOR)] 25 | public function testSeverityAnnotation(): void 26 | { 27 | $this->expectNotToPerformAssertions(); 28 | } 29 | 30 | #[Attribute\Parameter('foo', 'bar')] 31 | public function testParameterAnnotation(): void 32 | { 33 | $this->expectNotToPerformAssertions(); 34 | } 35 | 36 | #[Attribute\Story('Story 1')] 37 | #[Attribute\Story('Story 2')] 38 | public function testStoriesAnnotation(): void 39 | { 40 | $this->expectNotToPerformAssertions(); 41 | } 42 | 43 | #[Attribute\Feature('Feature 1')] 44 | #[Attribute\Feature('Feature 2')] 45 | public function testFeaturesAnnotation(): void 46 | { 47 | $this->expectNotToPerformAssertions(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/codeception-report/unit/DataProviderTest.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | public static function providerData(): iterable 33 | { 34 | return [ 35 | 0 => ['foo', 'foo'], 36 | 'a' => ['bar', 'bar'], 37 | 'b' => ['foo', 'bar'], 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/codeception-report/unit/StepsTest.php: -------------------------------------------------------------------------------- 1 | expectNotToPerformAssertions(); 20 | } 21 | 22 | /** 23 | * @throws Exception 24 | */ 25 | public function testNoStepsError(): void 26 | { 27 | throw new Exception('Error'); 28 | } 29 | 30 | public function testNoStepsFailure(): void 31 | { 32 | /** @psalm-suppress UndefinedClass */ 33 | self::fail('Failure'); 34 | } 35 | 36 | public function testNoStepsSkipped(): void 37 | { 38 | /** @psalm-suppress UndefinedClass */ 39 | self::markTestSkipped('Skipped'); 40 | } 41 | 42 | public function testSingleSuccessfulStepWithTitle(): void 43 | { 44 | $this->expectNotToPerformAssertions(); 45 | $scenario = new Scenario($this); 46 | $scenario->runStep(new Comment('Step 1 name')); 47 | } 48 | 49 | public function testSingleSuccessfulStepWithArguments(): void 50 | { 51 | $this->expectNotToPerformAssertions(); 52 | $scenario = new Scenario($this); 53 | $scenario->runStep(new Comment('Step 1 name', ['foo' => 'bar'])); 54 | } 55 | 56 | public function testTwoSuccessfulSteps(): void 57 | { 58 | $this->expectNotToPerformAssertions(); 59 | 60 | $scenario = new Scenario($this); 61 | $scenario->runStep(new Comment('Step 1 name')); 62 | $scenario->runStep(new Comment('Step 2 name')); 63 | } 64 | 65 | public function testTwoStepsFirstFails(): void 66 | { 67 | $this->expectNotToPerformAssertions(); 68 | 69 | $scenario = new Scenario($this); 70 | $scenario->runStep($this->createFailingStep('Step 1 name', 'Failure')); 71 | $scenario->runStep(new Comment('Step 2 name')); 72 | } 73 | 74 | public function testTwoStepsSecondFails(): void 75 | { 76 | $this->expectNotToPerformAssertions(); 77 | 78 | $scenario = new Scenario($this); 79 | $scenario->runStep(new Comment('Step 1 name')); 80 | $scenario->runStep($this->createFailingStep('Step 2 name', 'Failure')); 81 | } 82 | 83 | private function createFailingStep(string $name, string $failure): Step 84 | { 85 | return new class ($failure, $name) extends Meta { 86 | private string $failure; 87 | 88 | public function __construct(string $failure, string $action, array $arguments = []) 89 | { 90 | parent::__construct($action, $arguments); 91 | $this->failure = $failure; 92 | } 93 | 94 | public function run(ModuleContainer $container = null): void 95 | { 96 | $this->setFailed(true); 97 | Unit::fail($this->failure); 98 | } 99 | }; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/codeception/_support/UnitTester.php: -------------------------------------------------------------------------------- 1 | > 32 | */ 33 | private static array $testResults = []; 34 | 35 | private ?ProcessorInterface $jsonPathProcessor = null; 36 | 37 | private ?QueryFactoryInterface $jsonPathQueryFactory = null; 38 | 39 | public static function setUpBeforeClass(): void 40 | { 41 | $buildPath = __DIR__ . '/../../../build/allure-results'; 42 | $files = scandir($buildPath); 43 | 44 | $jsonValueFactory = NodeValueFactory::create(); 45 | $jsonPathProcessor = Processor::create(); 46 | $jsonPathQueryFactory = QueryFactory::create(); 47 | $testMethodsQuery = $jsonPathQueryFactory 48 | ->createQuery('$.labels[?(@.name=="testMethod")].value'); 49 | $testClassesQuery = $jsonPathQueryFactory 50 | ->createQuery('$.labels[?(@.name=="testClass")].value'); 51 | 52 | foreach ($files as $fileName) { 53 | $file = $buildPath . DIRECTORY_SEPARATOR . $fileName; 54 | if (!is_file($file)) { 55 | continue; 56 | } 57 | $extension = pathinfo($file, PATHINFO_EXTENSION); 58 | if ('json' == $extension) { 59 | $fileName = pathinfo($file, PATHINFO_FILENAME); 60 | if (!str_ends_with($fileName, '-result')) { 61 | continue; 62 | } 63 | $fileContent = file_get_contents($file); 64 | $data = $jsonValueFactory->createValue($fileContent); 65 | /** @var mixed $class */ 66 | $class = $jsonPathProcessor 67 | ->select($testClassesQuery, $data) 68 | ->decode()[0] ?? null; 69 | /** @var mixed $method */ 70 | $method = $jsonPathProcessor 71 | ->select($testMethodsQuery, $data) 72 | ->decode()[0] ?? null; 73 | if (!isset($class, $method)) { 74 | throw new RuntimeException("Test not found in file $file"); 75 | } 76 | self::assertIsString($class); 77 | self::assertIsString($method); 78 | self::$testResults[$class][$method] = $data; 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * @param string $class 85 | * @param string $method 86 | * @param string $jsonPath 87 | * @param non-empty-string $expectedValue 88 | * @dataProvider providerSingleNodeValueStartsFromString 89 | */ 90 | public function testSingleNodeValueStartsFromString( 91 | string $class, 92 | string $method, 93 | string $jsonPath, 94 | string $expectedValue 95 | ): void { 96 | /** @psalm-var mixed $nodes */ 97 | $nodes = $this 98 | ->getJsonPathProcessor() 99 | ->select( 100 | $this->getJsonPathQueryFactory()->createQuery($jsonPath), 101 | self::$testResults[$class][$method] 102 | ?? throw new RuntimeException("Result not found for $class::$method"), 103 | ) 104 | ->decode(); 105 | self::assertIsArray($nodes); 106 | self::assertCount(1, $nodes); 107 | $value = $nodes[0] ?? null; 108 | self::assertIsString($value); 109 | self::assertStringStartsWith($expectedValue, $value); 110 | } 111 | 112 | /** 113 | * @return iterable 114 | */ 115 | public static function providerSingleNodeValueStartsFromString(): iterable 116 | { 117 | return [ 118 | 'Error message in test case without steps' => [ 119 | StepsTest::class, 120 | 'testNoStepsError', 121 | '$.statusDetails.message', 122 | "Error\nException(0)", 123 | ], 124 | ]; 125 | } 126 | 127 | /** 128 | * @dataProvider providerExistingNodeValue 129 | */ 130 | public function testExistingNodeValue( 131 | string $class, 132 | string $method, 133 | string $jsonPath, 134 | array $expected 135 | ): void { 136 | $nodes = $this 137 | ->getJsonPathProcessor() 138 | ->select( 139 | $this->getJsonPathQueryFactory()->createQuery($jsonPath), 140 | self::$testResults[$class][$method] 141 | ?? throw new RuntimeException("Result not found for $class::$method"), 142 | ) 143 | ->decode(); 144 | self::assertSame($expected, $nodes); 145 | } 146 | 147 | /** 148 | * @return iterable}> 149 | */ 150 | public static function providerExistingNodeValue(): iterable 151 | { 152 | return [ 153 | 'Test case title annotation' => [ 154 | AnnotationTest::class, 155 | 'testTitleAnnotation', 156 | '$.name', 157 | ['Test title'], 158 | ], 159 | 'Test case severity annotation' => [ 160 | AnnotationTest::class, 161 | 'testSeverityAnnotation', 162 | '$.labels[?(@.name=="severity")].value', 163 | ['minor'], 164 | ], 165 | 'Test case parameter annotation' => [ 166 | AnnotationTest::class, 167 | 'testParameterAnnotation', 168 | '$.parameters[?(@.name=="foo")].value', 169 | ['bar'], 170 | ], 171 | 'Test case stories annotation' => [ 172 | AnnotationTest::class, 173 | 'testStoriesAnnotation', 174 | '$.labels[?(@.name=="story")].value', 175 | ['Story 2', 'Story 1'], 176 | ], 177 | 'Test case features annotation' => [ 178 | AnnotationTest::class, 179 | 'testFeaturesAnnotation', 180 | '$.labels[?(@.name=="feature")].value', 181 | ['Feature 2', 'Feature 1'], 182 | ], 183 | 'Successful test case without steps' => [ 184 | StepsTest::class, 185 | 'testNoStepsSuccess', 186 | '$.status', 187 | ['passed'], 188 | ], 189 | 'Successful test case without steps: no steps' => [ 190 | StepsTest::class, 191 | 'testNoStepsSuccess', 192 | '$.steps[*]', 193 | [], 194 | ], 195 | 'Error in test case without steps' => [ 196 | StepsTest::class, 197 | 'testNoStepsError', 198 | '$.status', 199 | ['broken'], 200 | ], 201 | 'Failure message in test case without steps' => [ 202 | StepsTest::class, 203 | 'testNoStepsFailure', 204 | '$.statusDetails.message', 205 | ['Failure'], 206 | ], 207 | 'Test case without steps skipped' => [ 208 | StepsTest::class, 209 | 'testNoStepsSkipped', 210 | '$.status', 211 | ['skipped'], 212 | ], 213 | 'Skipped message in test case without steps' => [ 214 | StepsTest::class, 215 | 'testNoStepsSkipped', 216 | '$.statusDetails.message', 217 | ['Skipped'], 218 | ], 219 | 'Successful test case with single step: status' => [ 220 | StepsTest::class, 221 | 'testSingleSuccessfulStepWithTitle', 222 | '$.status', 223 | ['passed'], 224 | ], 225 | 'Successful test case with single step: step status' => [ 226 | StepsTest::class, 227 | 'testSingleSuccessfulStepWithTitle', 228 | '$.steps[*].status', 229 | ['passed'], 230 | ], 231 | 'Successful test case with single step: step name' => [ 232 | StepsTest::class, 233 | 'testSingleSuccessfulStepWithTitle', 234 | '$.steps[*].name', 235 | ['step 1 name'], 236 | ], 237 | 'Successful test case with arguments in step: status' => [ 238 | StepsTest::class, 239 | 'testSingleSuccessfulStepWithArguments', 240 | '$.status', 241 | ['passed'], 242 | ], 243 | 'Successful test case with arguments in step: step status' => [ 244 | StepsTest::class, 245 | 'testSingleSuccessfulStepWithArguments', 246 | '$.steps[*].status', 247 | ['passed'], 248 | ], 249 | 'Successful test case with arguments in step: step name' => [ 250 | StepsTest::class, 251 | 'testSingleSuccessfulStepWithArguments', 252 | '$.steps[*].name', 253 | ['step 1 name'], 254 | ], 255 | 'Successful test case with arguments in step: step parameter' => [ 256 | StepsTest::class, 257 | 'testSingleSuccessfulStepWithArguments', 258 | '$.steps[*].parameters[?(@.name=="foo")].value', 259 | ['"bar"'], 260 | ], 261 | 'Successful test case with two successful steps: status' => [ 262 | StepsTest::class, 263 | 'testTwoSuccessfulSteps', 264 | '$.status', 265 | ['passed'], 266 | ], 267 | 'Successful test case with two successful steps: step status' => [ 268 | StepsTest::class, 269 | 'testTwoSuccessfulSteps', 270 | '$.steps[*].status', 271 | ['passed', 'passed'], 272 | ], 273 | 'Successful test case with two successful steps: step name' => [ 274 | StepsTest::class, 275 | 'testTwoSuccessfulSteps', 276 | '$.steps[*].name', 277 | ['step 1 name', 'step 2 name'], 278 | ], 279 | 'First step in test case with two steps fails: status' => [ 280 | StepsTest::class, 281 | 'testTwoStepsFirstFails', 282 | '$.status', 283 | ['failed'], 284 | ], 285 | 'First step in test case with two steps fails: message' => [ 286 | StepsTest::class, 287 | 'testTwoStepsFirstFails', 288 | '$.statusDetails.message', 289 | ['Failure'], 290 | ], 291 | 'First step in test case with two steps fails: step status' => [ 292 | StepsTest::class, 293 | 'testTwoStepsFirstFails', 294 | '$.steps[*].status', 295 | ['failed'], 296 | ], 297 | 'First step in test case with two steps fails: step name' => [ 298 | StepsTest::class, 299 | 'testTwoStepsFirstFails', 300 | '$.steps[*].name', 301 | ['step 1 name'], 302 | ], 303 | 'Second step in test case with two steps fails: status' => [ 304 | StepsTest::class, 305 | 'testTwoStepsSecondFails', 306 | '$.status', 307 | ['failed'], 308 | ], 309 | 'Second step in test case with two steps fails: message' => [ 310 | StepsTest::class, 311 | 'testTwoStepsSecondFails', 312 | '$.statusDetails.message', 313 | ['Failure'], 314 | ], 315 | 'Second step in test case with two steps fails: step status' => [ 316 | StepsTest::class, 317 | 'testTwoStepsSecondFails', 318 | '$.steps[*].status', 319 | ['passed', 'failed'], 320 | ], 321 | 'Second step in test case with two steps fails: step name' => [ 322 | StepsTest::class, 323 | 'testTwoStepsSecondFails', 324 | '$.steps[*].name', 325 | ['step 1 name', 'step 2 name'], 326 | ], 327 | 'Nested steps: root names' => [ 328 | NestedStepsCest::class, 329 | 'makeNestedSteps', 330 | '$.steps[*].name', 331 | ['Step 1'], 332 | ], 333 | 'Nested steps: level 1 names' => [ 334 | NestedStepsCest::class, 335 | 'makeNestedSteps', 336 | '$.steps[?(@.name=="Step 1")].steps[*].name', 337 | ['i expect condition 1', 'Step 1.1', 'Step 1.2'], 338 | ], 339 | 'Nested steps: level 1.1 names' => [ 340 | NestedStepsCest::class, 341 | 'makeNestedSteps', 342 | '$.steps..steps[?(@.name=="Step 1.1")].steps[*].name', 343 | ['i expect condition 1.1', 'Step 1.1.1'], 344 | ], 345 | 'Nested steps: level 1.1.1 names' => [ 346 | NestedStepsCest::class, 347 | 'makeNestedSteps', 348 | '$.steps..steps[?(@.name=="Step 1.1.1")].steps[*].name', 349 | ['i expect condition 1.1.1'], 350 | ], 351 | 'Nested steps: level 1.2 names' => [ 352 | NestedStepsCest::class, 353 | 'makeNestedSteps', 354 | '$.steps..steps[?(@.name=="Step 1.2")].steps[*].name', 355 | ['i expect condition 1.2'], 356 | ], 357 | ]; 358 | } 359 | 360 | private function getJsonPathProcessor(): ProcessorInterface 361 | { 362 | return $this->jsonPathProcessor ??= Processor::create(); 363 | } 364 | 365 | private function getJsonPathQueryFactory(): QueryFactoryInterface 366 | { 367 | return $this->jsonPathQueryFactory ??= QueryFactory::create(); 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /test/codeception/unit.suite.yml: -------------------------------------------------------------------------------- 1 | 2 | actor: UnitTester 3 | coverage: 4 | enabled: true 5 | include: 6 | - src/* -------------------------------------------------------------------------------- /test/codeception/unit/Internal/ArgumentAsStringTest.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | public static function providerString(): iterable 24 | { 25 | return [ 26 | 'Simple string' => ['a', '"a"'], 27 | 'String with tabulation' => ["a\tb", '"a b"'], 28 | 'String with line feed' => ["a\nb", '"a\\\\nb"'], 29 | 'String with carriage return' => ["a\rb", '"a\\\\rb"'], 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/codeception/unit/Internal/CestProviderTest.php: -------------------------------------------------------------------------------- 1 | assertSame(__CLASS__ . '::' . 'a', $cestProvider->getFullName()); 19 | } 20 | 21 | /** 22 | * @psalm-suppress PossiblyUnusedMethod 23 | */ 24 | public function a(): void 25 | { 26 | } 27 | } 28 | --------------------------------------------------------------------------------