├── .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 | [](https://packagist.org/packages/allure-framework/allure-codeception)
4 | [](https://github.com/allure-framework/allure-codeception/actions/workflows/build.yml)
5 | [](https://shepherd.dev/github/allure-framework/allure-codeception)
6 | [](https://shepherd.dev/github/allure-framework/allure-codeception)
7 | [](https://packagist.org/packages/allure-framework/allure-codeception)
8 | [](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 |
--------------------------------------------------------------------------------