test-report-vibes 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- test_report_vibes-0.2.0/LICENSE +22 -0
- test_report_vibes-0.2.0/PKG-INFO +167 -0
- test_report_vibes-0.2.0/README.md +136 -0
- test_report_vibes-0.2.0/pyproject.toml +67 -0
- test_report_vibes-0.2.0/setup.cfg +4 -0
- test_report_vibes-0.2.0/src/test_report_vibes/__init__.py +26 -0
- test_report_vibes-0.2.0/src/test_report_vibes/__main__.py +6 -0
- test_report_vibes-0.2.0/src/test_report_vibes/classifier.py +187 -0
- test_report_vibes-0.2.0/src/test_report_vibes/cli.py +131 -0
- test_report_vibes-0.2.0/src/test_report_vibes/filter.py +469 -0
- test_report_vibes-0.2.0/src/test_report_vibes/html_generator.py +1154 -0
- test_report_vibes-0.2.0/src/test_report_vibes/models.py +129 -0
- test_report_vibes-0.2.0/src/test_report_vibes/parser.py +80 -0
- test_report_vibes-0.2.0/src/test_report_vibes.egg-info/PKG-INFO +167 -0
- test_report_vibes-0.2.0/src/test_report_vibes.egg-info/SOURCES.txt +19 -0
- test_report_vibes-0.2.0/src/test_report_vibes.egg-info/dependency_links.txt +1 -0
- test_report_vibes-0.2.0/src/test_report_vibes.egg-info/entry_points.txt +2 -0
- test_report_vibes-0.2.0/src/test_report_vibes.egg-info/requires.txt +10 -0
- test_report_vibes-0.2.0/src/test_report_vibes.egg-info/top_level.txt +1 -0
- test_report_vibes-0.2.0/tests/test_classifier.py +41 -0
- test_report_vibes-0.2.0/tests/test_report.py +316 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oscar Barrios Torrero
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: test-report-vibes
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Deterministic HTML summaries of Cucumber test reports
|
|
5
|
+
Author-email: Oscar Barrios Torrero <srbarrios@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/srbarrios/test-report-vibes
|
|
8
|
+
Project-URL: Issues, https://github.com/srbarrios/test-report-vibes/issues
|
|
9
|
+
Keywords: cucumber,testing,report,html
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Software Development :: Testing
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: click>=8.1.0
|
|
22
|
+
Requires-Dist: pydantic>=2.0.0
|
|
23
|
+
Requires-Dist: jinja2>=3.1.0
|
|
24
|
+
Requires-Dist: rich>=13.0.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
28
|
+
Requires-Dist: black>=24.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff>=0.3.0; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# Test Report Vibes
|
|
33
|
+
|
|
34
|
+
<img width="150" src="https://github.com/user-attachments/assets/2d347d22-b88e-472f-8416-595caaed4524" />
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
**Deterministic HTML summaries of Cucumber test reports.**
|
|
38
|
+
|
|
39
|
+
`test-report-vibes` turns Cucumber JSON output into a focused, self-contained HTML report so you can quickly review what failed and why.
|
|
40
|
+
|
|
41
|
+
## What it does
|
|
42
|
+
|
|
43
|
+
- Filters report data to failing, undefined, and pending steps
|
|
44
|
+
- Keeps scenario context (all steps), plus optional screenshots from embeddings/hooks
|
|
45
|
+
- Groups issues by feature and includes pass/fail stats across the full run
|
|
46
|
+
- Builds a deterministic executive summary (no external AI/LLM calls)
|
|
47
|
+
- Optionally classifies failing scenarios by tags such as `@new_issue` and `@flaky`
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
From source:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git clone https://github.com/srbarrios/test-report-vibes.git
|
|
55
|
+
cd test-report-vibes
|
|
56
|
+
pip install -e .
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
test-report-vibes examples/sample_report_with_classifiers.json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
This creates `examples/sample_report_with_classifiers.html`.
|
|
66
|
+
|
|
67
|
+
## CLI usage
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
test-report-vibes [OPTIONS] INPUT_FILE
|
|
71
|
+
|
|
72
|
+
Arguments:
|
|
73
|
+
INPUT_FILE Path to Cucumber JSON report [required]
|
|
74
|
+
|
|
75
|
+
Options:
|
|
76
|
+
-o, --output PATH Output HTML file path (default: INPUT_FILE.html)
|
|
77
|
+
-v, --verbose Verbose output with detailed exception trace on errors
|
|
78
|
+
--no-classify Skip the tag-based classification section
|
|
79
|
+
--help Show this message and exit
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Default output path: INPUT_FILE.html
|
|
86
|
+
test-report-vibes cucumber-report.json
|
|
87
|
+
|
|
88
|
+
# Custom output file
|
|
89
|
+
test-report-vibes cucumber-report.json -o report-summary.html
|
|
90
|
+
|
|
91
|
+
# Disable tag-based classification section
|
|
92
|
+
test-report-vibes cucumber-report.json --no-classify
|
|
93
|
+
|
|
94
|
+
# Run as module
|
|
95
|
+
python -m test_report_vibes cucumber-report.json
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Input format
|
|
99
|
+
|
|
100
|
+
The tool expects standard Cucumber JSON (root array of features). At minimum, each feature should include:
|
|
101
|
+
|
|
102
|
+
- `uri`, `id`, `name`, `keyword`, `elements`
|
|
103
|
+
|
|
104
|
+
Steps support these statuses:
|
|
105
|
+
|
|
106
|
+
- `passed`, `failed`, `skipped`, `pending`, `undefined`
|
|
107
|
+
|
|
108
|
+
Screenshots are supported through base64 embeddings with image mime types (for example `image/png`) on steps and hooks.
|
|
109
|
+
|
|
110
|
+
## Output report highlights
|
|
111
|
+
|
|
112
|
+
Generated HTML includes:
|
|
113
|
+
|
|
114
|
+
- Overall run dashboard (passed/failed/skipped feature percentages)
|
|
115
|
+
- Executive summary cards such as:
|
|
116
|
+
- Most impacted features
|
|
117
|
+
- Recurring normalized error patterns
|
|
118
|
+
- Top error types
|
|
119
|
+
- Framework gaps (undefined/pending steps)
|
|
120
|
+
- Slowest failing scenarios and steps
|
|
121
|
+
- Failing features with collapsible scenario details
|
|
122
|
+
- Full step context including status, duration, location, and error text
|
|
123
|
+
- Embedded screenshots when present
|
|
124
|
+
- Optional "Classified features" section from tag-based mapping
|
|
125
|
+
|
|
126
|
+
## Tag-based classification
|
|
127
|
+
|
|
128
|
+
When classification is enabled (default), statuses are derived from tags in this priority order:
|
|
129
|
+
|
|
130
|
+
- `@new_issue` -> `New and reported`
|
|
131
|
+
- `@under_debugging` -> `Debugging`
|
|
132
|
+
- `@bug_reported` -> `Bug reported`
|
|
133
|
+
- `@test_issue` -> `Test Framework issue`
|
|
134
|
+
- `@flaky` -> `Flaky Test`
|
|
135
|
+
- no match -> `Not reported`
|
|
136
|
+
|
|
137
|
+
## Sample Report
|
|
138
|
+
|
|
139
|
+
Check out a live example of the generated report:
|
|
140
|
+
|
|
141
|
+
[View Sample Report](https://srbarrios.github.io/test-report-vibes/examples/sample_report_with_classifiers.html)
|
|
142
|
+
|
|
143
|
+
## Development
|
|
144
|
+
|
|
145
|
+
Install dev dependencies:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
pip install -e ".[dev]"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Run tests:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
pytest
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Format and lint:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
black src/
|
|
161
|
+
ruff check src/
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
This project is licensed under the MIT License. See `LICENSE` for details.
|
|
167
|
+
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Test Report Vibes
|
|
2
|
+
|
|
3
|
+
<img width="150" src="https://github.com/user-attachments/assets/2d347d22-b88e-472f-8416-595caaed4524" />
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
**Deterministic HTML summaries of Cucumber test reports.**
|
|
7
|
+
|
|
8
|
+
`test-report-vibes` turns Cucumber JSON output into a focused, self-contained HTML report so you can quickly review what failed and why.
|
|
9
|
+
|
|
10
|
+
## What it does
|
|
11
|
+
|
|
12
|
+
- Filters report data to failing, undefined, and pending steps
|
|
13
|
+
- Keeps scenario context (all steps), plus optional screenshots from embeddings/hooks
|
|
14
|
+
- Groups issues by feature and includes pass/fail stats across the full run
|
|
15
|
+
- Builds a deterministic executive summary (no external AI/LLM calls)
|
|
16
|
+
- Optionally classifies failing scenarios by tags such as `@new_issue` and `@flaky`
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
From source:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git clone https://github.com/srbarrios/test-report-vibes.git
|
|
24
|
+
cd test-report-vibes
|
|
25
|
+
pip install -e .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
test-report-vibes examples/sample_report_with_classifiers.json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This creates `examples/sample_report_with_classifiers.html`.
|
|
35
|
+
|
|
36
|
+
## CLI usage
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
test-report-vibes [OPTIONS] INPUT_FILE
|
|
40
|
+
|
|
41
|
+
Arguments:
|
|
42
|
+
INPUT_FILE Path to Cucumber JSON report [required]
|
|
43
|
+
|
|
44
|
+
Options:
|
|
45
|
+
-o, --output PATH Output HTML file path (default: INPUT_FILE.html)
|
|
46
|
+
-v, --verbose Verbose output with detailed exception trace on errors
|
|
47
|
+
--no-classify Skip the tag-based classification section
|
|
48
|
+
--help Show this message and exit
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Default output path: INPUT_FILE.html
|
|
55
|
+
test-report-vibes cucumber-report.json
|
|
56
|
+
|
|
57
|
+
# Custom output file
|
|
58
|
+
test-report-vibes cucumber-report.json -o report-summary.html
|
|
59
|
+
|
|
60
|
+
# Disable tag-based classification section
|
|
61
|
+
test-report-vibes cucumber-report.json --no-classify
|
|
62
|
+
|
|
63
|
+
# Run as module
|
|
64
|
+
python -m test_report_vibes cucumber-report.json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Input format
|
|
68
|
+
|
|
69
|
+
The tool expects standard Cucumber JSON (root array of features). At minimum, each feature should include:
|
|
70
|
+
|
|
71
|
+
- `uri`, `id`, `name`, `keyword`, `elements`
|
|
72
|
+
|
|
73
|
+
Steps support these statuses:
|
|
74
|
+
|
|
75
|
+
- `passed`, `failed`, `skipped`, `pending`, `undefined`
|
|
76
|
+
|
|
77
|
+
Screenshots are supported through base64 embeddings with image mime types (for example `image/png`) on steps and hooks.
|
|
78
|
+
|
|
79
|
+
## Output report highlights
|
|
80
|
+
|
|
81
|
+
Generated HTML includes:
|
|
82
|
+
|
|
83
|
+
- Overall run dashboard (passed/failed/skipped feature percentages)
|
|
84
|
+
- Executive summary cards such as:
|
|
85
|
+
- Most impacted features
|
|
86
|
+
- Recurring normalized error patterns
|
|
87
|
+
- Top error types
|
|
88
|
+
- Framework gaps (undefined/pending steps)
|
|
89
|
+
- Slowest failing scenarios and steps
|
|
90
|
+
- Failing features with collapsible scenario details
|
|
91
|
+
- Full step context including status, duration, location, and error text
|
|
92
|
+
- Embedded screenshots when present
|
|
93
|
+
- Optional "Classified features" section from tag-based mapping
|
|
94
|
+
|
|
95
|
+
## Tag-based classification
|
|
96
|
+
|
|
97
|
+
When classification is enabled (default), statuses are derived from tags in this priority order:
|
|
98
|
+
|
|
99
|
+
- `@new_issue` -> `New and reported`
|
|
100
|
+
- `@under_debugging` -> `Debugging`
|
|
101
|
+
- `@bug_reported` -> `Bug reported`
|
|
102
|
+
- `@test_issue` -> `Test Framework issue`
|
|
103
|
+
- `@flaky` -> `Flaky Test`
|
|
104
|
+
- no match -> `Not reported`
|
|
105
|
+
|
|
106
|
+
## Sample Report
|
|
107
|
+
|
|
108
|
+
Check out a live example of the generated report:
|
|
109
|
+
|
|
110
|
+
[View Sample Report](https://srbarrios.github.io/test-report-vibes/examples/sample_report_with_classifiers.html)
|
|
111
|
+
|
|
112
|
+
## Development
|
|
113
|
+
|
|
114
|
+
Install dev dependencies:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pip install -e ".[dev]"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Run tests:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pytest
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Format and lint:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
black src/
|
|
130
|
+
ruff check src/
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
This project is licensed under the MIT License. See `LICENSE` for details.
|
|
136
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "test-report-vibes"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Deterministic HTML summaries of Cucumber test reports"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Oscar Barrios Torrero", email = "srbarrios@gmail.com"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["cucumber", "testing", "report", "html"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Topic :: Software Development :: Testing",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
"click>=8.1.0",
|
|
29
|
+
"pydantic>=2.0.0",
|
|
30
|
+
"jinja2>=3.1.0",
|
|
31
|
+
"rich>=13.0.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=8.0.0",
|
|
37
|
+
"pytest-cov>=4.1.0",
|
|
38
|
+
"black>=24.0.0",
|
|
39
|
+
"ruff>=0.3.0",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
test-report-vibes = "test_report_vibes.cli:main"
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://github.com/srbarrios/test-report-vibes"
|
|
47
|
+
Issues = "https://github.com/srbarrios/test-report-vibes/issues"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["src"]
|
|
51
|
+
|
|
52
|
+
[tool.setuptools]
|
|
53
|
+
license-files = ["LICENSE"]
|
|
54
|
+
|
|
55
|
+
[tool.black]
|
|
56
|
+
line-length = 100
|
|
57
|
+
target-version = ["py310", "py311", "py312"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
line-length = 100
|
|
61
|
+
target-version = "py310"
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
testpaths = ["tests"]
|
|
65
|
+
python_files = ["test_*.py"]
|
|
66
|
+
python_classes = ["Test*"]
|
|
67
|
+
python_functions = ["test_*"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Test Report Vibes - deterministic HTML summaries of Cucumber test reports."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.2.0"
|
|
4
|
+
__author__ = "Test Report Vibes Contributors"
|
|
5
|
+
__description__ = "Deterministic HTML summaries of Cucumber test reports"
|
|
6
|
+
|
|
7
|
+
from .parser import parse_cucumber_json
|
|
8
|
+
from .filter import filter_issues, calculate_summary_stats, group_issues_by_feature
|
|
9
|
+
from .html_generator import generate_html_report, build_default_executive_summary
|
|
10
|
+
from .classifier import classify_features, build_classification_summary_html
|
|
11
|
+
from .models import Feature, Scenario, Step, FilteredIssue
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"parse_cucumber_json",
|
|
15
|
+
"filter_issues",
|
|
16
|
+
"calculate_summary_stats",
|
|
17
|
+
"group_issues_by_feature",
|
|
18
|
+
"generate_html_report",
|
|
19
|
+
"build_default_executive_summary",
|
|
20
|
+
"classify_features",
|
|
21
|
+
"build_classification_summary_html",
|
|
22
|
+
"Feature",
|
|
23
|
+
"Scenario",
|
|
24
|
+
"Step",
|
|
25
|
+
"FilteredIssue",
|
|
26
|
+
]
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Tag-based issue classification for Cucumber test results.
|
|
2
|
+
|
|
3
|
+
Classifies failing scenarios by their tags and highlights the first failing
|
|
4
|
+
scenario per feature as the most important one to review.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import html
|
|
8
|
+
from typing import Dict, List, Any, Optional
|
|
9
|
+
|
|
10
|
+
# Default mapping from Cucumber tags to human-readable status labels.
|
|
11
|
+
# Priority is determined by order: the first matching tag wins.
|
|
12
|
+
DEFAULT_TAG_MAPPING: Dict[str, str] = {
|
|
13
|
+
"@new_issue": "New and reported",
|
|
14
|
+
"@under_debugging": "Debugging",
|
|
15
|
+
"@bug_reported": "Bug reported",
|
|
16
|
+
"@test_issue": "Test Framework issue",
|
|
17
|
+
"@flaky": "Flaky Test",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
NOT_REPORTED = "Not reported"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def classify_status(tags: List[str], tag_mapping: Optional[Dict[str, str]] = None) -> str:
|
|
24
|
+
"""Return a human-readable status for a set of tags.
|
|
25
|
+
|
|
26
|
+
Iterates through *tags* in order and returns the label of the first tag
|
|
27
|
+
present in *tag_mapping*. Falls back to ``"Not reported"``.
|
|
28
|
+
"""
|
|
29
|
+
mapping = tag_mapping or DEFAULT_TAG_MAPPING
|
|
30
|
+
for tag in tags:
|
|
31
|
+
if tag in mapping:
|
|
32
|
+
return mapping[tag]
|
|
33
|
+
return NOT_REPORTED
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def classify_features(
|
|
37
|
+
feature_groups: List[Dict[str, Any]],
|
|
38
|
+
tag_mapping: Optional[Dict[str, str]] = None,
|
|
39
|
+
) -> Dict[str, Any]:
|
|
40
|
+
"""Classify every failing feature group.
|
|
41
|
+
|
|
42
|
+
For each feature group (as produced by ``filter.group_issues_by_feature``),
|
|
43
|
+
determines:
|
|
44
|
+
|
|
45
|
+
* The *first* failing scenario (by position / line number) — this is the
|
|
46
|
+
most important one to review because later scenarios may cascade.
|
|
47
|
+
* A classification status derived from the combined feature + scenario tags
|
|
48
|
+
using ``classify_status``.
|
|
49
|
+
|
|
50
|
+
Returns a dict with:
|
|
51
|
+
``classified_features`` – list of per-feature classification dicts
|
|
52
|
+
``status_counts`` – breakdown ``{status_label: count}``
|
|
53
|
+
``total_failed_features`` – number of features with failures
|
|
54
|
+
``total_failed_scenarios`` – total failing scenarios across all features
|
|
55
|
+
"""
|
|
56
|
+
mapping = tag_mapping or DEFAULT_TAG_MAPPING
|
|
57
|
+
|
|
58
|
+
classified: List[Dict[str, Any]] = []
|
|
59
|
+
status_counts: Dict[str, int] = {}
|
|
60
|
+
total_failed_scenarios = 0
|
|
61
|
+
|
|
62
|
+
for group in feature_groups:
|
|
63
|
+
feature_name: str = group.get("feature_name", "Unknown Feature")
|
|
64
|
+
feature_tags: List[str] = group.get("feature_tags", [])
|
|
65
|
+
scenarios: List[Dict[str, Any]] = group.get("scenarios", [])
|
|
66
|
+
|
|
67
|
+
first_failing: Optional[Dict[str, Any]] = None
|
|
68
|
+
|
|
69
|
+
for scenario in scenarios:
|
|
70
|
+
# Check if this scenario actually has failures (not just a passing scenario added for context)
|
|
71
|
+
has_failures = (
|
|
72
|
+
scenario.get("failed_steps", 0) > 0 or
|
|
73
|
+
scenario.get("undefined_steps", 0) > 0 or
|
|
74
|
+
scenario.get("pending_steps", 0) > 0
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if not has_failures:
|
|
78
|
+
continue # Skip passing scenarios
|
|
79
|
+
|
|
80
|
+
total_failed_scenarios += 1
|
|
81
|
+
|
|
82
|
+
# Merge feature + scenario tags for classification
|
|
83
|
+
all_tags = feature_tags + scenario.get("tags", [])
|
|
84
|
+
status = classify_status(all_tags, mapping)
|
|
85
|
+
|
|
86
|
+
# Count every failing scenario by status
|
|
87
|
+
status_counts[status] = status_counts.get(status, 0) + 1
|
|
88
|
+
|
|
89
|
+
# The first scenario in the list is by definition the earliest
|
|
90
|
+
# (group_issues_by_feature sorts by line number).
|
|
91
|
+
if first_failing is None:
|
|
92
|
+
first_failing = {
|
|
93
|
+
"scenario_name": scenario.get("name", "Unnamed Scenario"),
|
|
94
|
+
"status": status,
|
|
95
|
+
"tags": all_tags,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Count only scenarios with actual failures
|
|
99
|
+
failing_count = sum(
|
|
100
|
+
1 for s in scenarios
|
|
101
|
+
if s.get("failed_steps", 0) > 0 or s.get("undefined_steps", 0) > 0 or s.get("pending_steps", 0) > 0
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
classified.append({
|
|
105
|
+
"feature_name": feature_name,
|
|
106
|
+
"first_failing": first_failing,
|
|
107
|
+
"failing_scenario_count": failing_count,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
"classified_features": classified,
|
|
112
|
+
"status_counts": status_counts,
|
|
113
|
+
"total_failed_features": len(classified),
|
|
114
|
+
"total_failed_scenarios": total_failed_scenarios,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# HTML rendering
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
_STATUS_COLORS: Dict[str, str] = {
|
|
123
|
+
"New and reported": "#9cf3af",
|
|
124
|
+
"Debugging": "#3b89f6",
|
|
125
|
+
"Bug reported": "#ef0000",
|
|
126
|
+
"Test Framework issue": "#8989ff",
|
|
127
|
+
"Flaky Test": "#f59e0b",
|
|
128
|
+
NOT_REPORTED: "#efbb85",
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _status_color(status: str) -> str:
|
|
133
|
+
return _STATUS_COLORS.get(status, "#6b7280")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def build_classification_summary_html(classification: Dict[str, Any]) -> str:
|
|
137
|
+
"""Render an HTML ``<section>`` with the tag-based classification summary.
|
|
138
|
+
|
|
139
|
+
The section has CSS class ``classification-summary`` and is designed to sit
|
|
140
|
+
alongside (but independently of) the executive summary.
|
|
141
|
+
"""
|
|
142
|
+
classified = classification["classified_features"]
|
|
143
|
+
status_counts = classification["status_counts"]
|
|
144
|
+
total_features = classification["total_failed_features"]
|
|
145
|
+
total_scenarios = classification["total_failed_scenarios"]
|
|
146
|
+
|
|
147
|
+
if total_features == 0:
|
|
148
|
+
return (
|
|
149
|
+
'<section class="classification-summary">'
|
|
150
|
+
"<h2>Classified features</h2>"
|
|
151
|
+
"<p>No failing features to classify.</p>"
|
|
152
|
+
"</section>"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# --- Feature list (first failing scenario only) ---
|
|
156
|
+
list_items = []
|
|
157
|
+
for entry in classified:
|
|
158
|
+
fname = html.escape(entry["feature_name"])
|
|
159
|
+
first = entry.get("first_failing")
|
|
160
|
+
if first:
|
|
161
|
+
sname = html.escape(first["scenario_name"])
|
|
162
|
+
sstatus = html.escape(first["status"])
|
|
163
|
+
color = _status_color(first["status"])
|
|
164
|
+
list_items.append(
|
|
165
|
+
f"<li>"
|
|
166
|
+
f"<strong>{fname}</strong>"
|
|
167
|
+
f'<ul><li><span class="cls-pill" style="background:{color};">'
|
|
168
|
+
f"{sstatus}</span> {sname}</li></ul>"
|
|
169
|
+
f"</li>"
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
list_items.append(f"<li><strong>{fname}</strong></li>")
|
|
173
|
+
|
|
174
|
+
features_html = "\n".join(list_items)
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
'<section class="classification-summary">\n'
|
|
178
|
+
"<h2>Classified features</h2>\n"
|
|
179
|
+
f"<p><strong>Failed Features:</strong> {total_features}</p>\n"
|
|
180
|
+
f"<p><strong>Failed Scenarios:</strong> {total_scenarios}</p>\n"
|
|
181
|
+
'<p style="margin-bottom:0.25rem;color:var(--color-text-light);font-size:0.875rem;">'
|
|
182
|
+
"The first failing scenario in each feature is usually the most important to review, "
|
|
183
|
+
"later failures may cascade from it.</p>\n"
|
|
184
|
+
f"<ul>{features_html}</ul>\n"
|
|
185
|
+
"</section>"
|
|
186
|
+
)
|
|
187
|
+
|