reqcov 0.1.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.
- reqcov-0.1.0/LICENSE +21 -0
- reqcov-0.1.0/PKG-INFO +176 -0
- reqcov-0.1.0/README.md +152 -0
- reqcov-0.1.0/pyproject.toml +40 -0
- reqcov-0.1.0/reqcov/__init__.py +3 -0
- reqcov-0.1.0/reqcov/cli.py +122 -0
- reqcov-0.1.0/reqcov/config.py +121 -0
- reqcov-0.1.0/reqcov/coverage.py +149 -0
- reqcov-0.1.0/reqcov/files.py +38 -0
- reqcov-0.1.0/reqcov/junit.py +63 -0
- reqcov-0.1.0/reqcov/links.py +95 -0
- reqcov-0.1.0/reqcov/models.py +153 -0
- reqcov-0.1.0/reqcov/report.py +178 -0
- reqcov-0.1.0/reqcov/requirements.py +310 -0
- reqcov-0.1.0/reqcov/templates/report.html +139 -0
- reqcov-0.1.0/reqcov.egg-info/PKG-INFO +176 -0
- reqcov-0.1.0/reqcov.egg-info/SOURCES.txt +21 -0
- reqcov-0.1.0/reqcov.egg-info/dependency_links.txt +1 -0
- reqcov-0.1.0/reqcov.egg-info/entry_points.txt +2 -0
- reqcov-0.1.0/reqcov.egg-info/requires.txt +6 -0
- reqcov-0.1.0/reqcov.egg-info/top_level.txt +1 -0
- reqcov-0.1.0/setup.cfg +4 -0
- reqcov-0.1.0/tests/test_reqcov.py +170 -0
reqcov-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Antoine005
|
|
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.
|
reqcov-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reqcov
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Automate requirements traceability: trace requirements to tests and code, fail CI on gaps, generate audit-ready traceability matrices.
|
|
5
|
+
Author: reqcov contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Antoine005/reqcov
|
|
8
|
+
Keywords: requirements,traceability,IEC 62304,ISO 26262,DO-178C,EN 50128,CI,github-actions
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
14
|
+
Classifier: Topic :: Software Development :: Testing
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: PyYAML>=6.0
|
|
19
|
+
Requires-Dist: Jinja2>=3.1
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# reqcov — requirements coverage for pull requests
|
|
26
|
+
|
|
27
|
+
**Codecov, but for your requirements.** reqcov reads the requirements you already keep in your
|
|
28
|
+
repository (Markdown, YAML or Doorstop), finds the tests and code that reference them, merges
|
|
29
|
+
the JUnit results of your test run, and tells every pull request which requirements are
|
|
30
|
+
**uncovered**, **covered**, **verified** or **failing** — then writes the traceability matrix an
|
|
31
|
+
auditor asks for (IEC 62304, ISO 26262, EN 50128, DO-178C, IEC 61508, ECSS).
|
|
32
|
+
|
|
33
|
+
No server, no account, no new editor: a CLI + a GitHub Action.
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
## ❌ Requirements coverage: 83.3%
|
|
37
|
+
|
|
38
|
+
| Requirements | With test | Verified | Uncovered | Failing | Unknown ids | Orphan tests |
|
|
39
|
+
|---:|---:|---:|---:|---:|---:|---:|
|
|
40
|
+
| 8 | 5 | 5 | 1 | 0 | 0 | 1 |
|
|
41
|
+
|
|
42
|
+
- SRS: 75% of 4 testable requirements have a test
|
|
43
|
+
- SYS: 100% of 2 testable requirements have a test
|
|
44
|
+
|
|
45
|
+
### ❌ 1 error(s)
|
|
46
|
+
- `COVERAGE` test coverage of requirements 83.3% is below the required 100.0%
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## How it works
|
|
50
|
+
|
|
51
|
+
1. **Requirements** live in your repo, one id per requirement (`SYS-1`, `SRS-12`, `LLR-3`…):
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
## SRS-11 — Over-temperature cut-off
|
|
55
|
+
The controller shall force the heater off when temperature ≥ 35 °C.
|
|
56
|
+
Parent: SYS-2
|
|
57
|
+
Verification: test
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
YAML lists and [Doorstop](https://github.com/doorstop-dev/doorstop) items are also read.
|
|
61
|
+
|
|
62
|
+
2. **Tests and code** reference ids with a marker — any language, in a comment, a decorator, a
|
|
63
|
+
macro:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
@pytest.mark.req("SRS-11", "SYS-2")
|
|
67
|
+
def test_overtemp_cutoff(): ...
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```c
|
|
71
|
+
/* @req LLR-12 */
|
|
72
|
+
void test_frame_bad_crc_is_rejected(void) { ... }
|
|
73
|
+
|
|
74
|
+
/* @implements LLR-11, LLR-12 */
|
|
75
|
+
frame_status_t frame_validate(const uint8_t *frame, size_t len) { ... }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```cpp
|
|
79
|
+
// @verifies SWR-2
|
|
80
|
+
TEST(RingBuffer, PushOnFullFails) { ... }
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
3. **JUnit XML** (pytest, Ceedling, GoogleTest, CTest, Jest, Maven…) turns *covered* into
|
|
84
|
+
*verified* or *failing*.
|
|
85
|
+
|
|
86
|
+
4. **Rules** in `reqcov.yml` decide what fails the build: minimum coverage, unknown ids,
|
|
87
|
+
orphan tests, mandatory parent links per level, mandatory `@implements` per level.
|
|
88
|
+
|
|
89
|
+
## Quick start
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install reqcov
|
|
93
|
+
reqcov init # writes reqcov.yml — edit the globs
|
|
94
|
+
pytest --junitxml=reports/junit.xml
|
|
95
|
+
reqcov check # exit 1 on rule violations, writes reqcov-report/
|
|
96
|
+
open reqcov-report/index.html
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`reqcov-report/` contains `index.html` (interactive matrix), `matrix.csv` (auditor-friendly),
|
|
100
|
+
`coverage.json` (machine readable) and `summary.md` (the PR comment).
|
|
101
|
+
|
|
102
|
+
## GitHub Action
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
name: requirements
|
|
106
|
+
on: [pull_request, push]
|
|
107
|
+
jobs:
|
|
108
|
+
reqcov:
|
|
109
|
+
runs-on: ubuntu-latest
|
|
110
|
+
permissions:
|
|
111
|
+
contents: read
|
|
112
|
+
pull-requests: write
|
|
113
|
+
steps:
|
|
114
|
+
- uses: actions/checkout@v4
|
|
115
|
+
- run: pip install -e . pytest && pytest --junitxml=reports/junit.xml
|
|
116
|
+
- uses: Antoine005/reqcov@v0
|
|
117
|
+
with:
|
|
118
|
+
junit: reports/junit.xml
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The action posts (and keeps updating) one sticky comment on the pull request, writes the
|
|
122
|
+
summary to the job page, emits annotations on the requirement lines that are uncovered or
|
|
123
|
+
failing, and uploads the report directory as an artifact.
|
|
124
|
+
|
|
125
|
+
## Configuration (`reqcov.yml`)
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
id_pattern: "[A-Z][A-Z0-9_]*-\\d+" # level = everything before the last dash
|
|
129
|
+
requirements: [docs/requirements/**/*.md]
|
|
130
|
+
sources: [src/**/*] # scanned for @implements markers
|
|
131
|
+
tests: [tests/**/*] # scanned for @req / @verifies markers
|
|
132
|
+
junit: [reports/*.xml]
|
|
133
|
+
markers: [req, requirement, implements, verifies, satisfies, trace]
|
|
134
|
+
rules:
|
|
135
|
+
min_test_coverage: 100 # % of testable requirements with ≥ 1 test
|
|
136
|
+
min_verified: null # % that must be verified (needs junit)
|
|
137
|
+
fail_on_unknown_ids: true
|
|
138
|
+
fail_on_orphan_tests: false
|
|
139
|
+
fail_on_failing_tests: true
|
|
140
|
+
require_parent_for: [SRS] # levels that must trace up
|
|
141
|
+
require_source_for: [] # levels that must have an @implements
|
|
142
|
+
allow_derived: true # missing parent = warning (false: error)
|
|
143
|
+
report:
|
|
144
|
+
out_dir: reqcov-report
|
|
145
|
+
formats: [html, csv, json, md]
|
|
146
|
+
title: "Software Requirements Traceability"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Requirement metadata
|
|
150
|
+
|
|
151
|
+
| Field | Markdown body line | YAML key | Values |
|
|
152
|
+
|---|---|---|---|
|
|
153
|
+
| parents | `Parent: SYS-1, SYS-2` | `parent` / `parents` / `links` | ids |
|
|
154
|
+
| verification | `Verification: test` | `verification` | `test` (default), `analysis`, `inspection`, `demonstration`, `none` |
|
|
155
|
+
| status | `Status: draft` | `status` (Doorstop: `active: false` → obsolete) | free text; `obsolete` is ignored by rules |
|
|
156
|
+
| tags | `Tags: safety, ui` | `tags` | list |
|
|
157
|
+
|
|
158
|
+
Only requirements with `verification: test` count toward test coverage; the others are shown
|
|
159
|
+
as `n/a` in the matrix so the auditor still sees them.
|
|
160
|
+
|
|
161
|
+
## Examples
|
|
162
|
+
|
|
163
|
+
- [`examples/pytest-project`](examples/pytest-project) — Python, SYS→SRS levels, one deliberate gap and one orphan test.
|
|
164
|
+
- [`examples/ceedling-unity`](examples/ceedling-unity) — C, HLR→LLR, `@implements` in sources, a failing Unity test propagating to two requirements.
|
|
165
|
+
- [`examples/googletest`](examples/googletest) — C++, one-line requirements, GoogleTest `Suite.Name` results.
|
|
166
|
+
|
|
167
|
+
## Status and roadmap
|
|
168
|
+
|
|
169
|
+
`0.1` — CLI, Markdown/YAML/Doorstop input, marker scanning, JUnit merge, HTML/CSV/JSON/MD
|
|
170
|
+
reports, GitHub Action with sticky PR comment. Planned: coverage delta against the base
|
|
171
|
+
branch, StrictDoc and ReqIF input, Jira issue links, GitLab CI template, signed PDF export for
|
|
172
|
+
audit packages, hosted history and badges.
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT.
|
reqcov-0.1.0/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# reqcov — requirements coverage for pull requests
|
|
2
|
+
|
|
3
|
+
**Codecov, but for your requirements.** reqcov reads the requirements you already keep in your
|
|
4
|
+
repository (Markdown, YAML or Doorstop), finds the tests and code that reference them, merges
|
|
5
|
+
the JUnit results of your test run, and tells every pull request which requirements are
|
|
6
|
+
**uncovered**, **covered**, **verified** or **failing** — then writes the traceability matrix an
|
|
7
|
+
auditor asks for (IEC 62304, ISO 26262, EN 50128, DO-178C, IEC 61508, ECSS).
|
|
8
|
+
|
|
9
|
+
No server, no account, no new editor: a CLI + a GitHub Action.
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
## ❌ Requirements coverage: 83.3%
|
|
13
|
+
|
|
14
|
+
| Requirements | With test | Verified | Uncovered | Failing | Unknown ids | Orphan tests |
|
|
15
|
+
|---:|---:|---:|---:|---:|---:|---:|
|
|
16
|
+
| 8 | 5 | 5 | 1 | 0 | 0 | 1 |
|
|
17
|
+
|
|
18
|
+
- SRS: 75% of 4 testable requirements have a test
|
|
19
|
+
- SYS: 100% of 2 testable requirements have a test
|
|
20
|
+
|
|
21
|
+
### ❌ 1 error(s)
|
|
22
|
+
- `COVERAGE` test coverage of requirements 83.3% is below the required 100.0%
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## How it works
|
|
26
|
+
|
|
27
|
+
1. **Requirements** live in your repo, one id per requirement (`SYS-1`, `SRS-12`, `LLR-3`…):
|
|
28
|
+
|
|
29
|
+
```markdown
|
|
30
|
+
## SRS-11 — Over-temperature cut-off
|
|
31
|
+
The controller shall force the heater off when temperature ≥ 35 °C.
|
|
32
|
+
Parent: SYS-2
|
|
33
|
+
Verification: test
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
YAML lists and [Doorstop](https://github.com/doorstop-dev/doorstop) items are also read.
|
|
37
|
+
|
|
38
|
+
2. **Tests and code** reference ids with a marker — any language, in a comment, a decorator, a
|
|
39
|
+
macro:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
@pytest.mark.req("SRS-11", "SYS-2")
|
|
43
|
+
def test_overtemp_cutoff(): ...
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```c
|
|
47
|
+
/* @req LLR-12 */
|
|
48
|
+
void test_frame_bad_crc_is_rejected(void) { ... }
|
|
49
|
+
|
|
50
|
+
/* @implements LLR-11, LLR-12 */
|
|
51
|
+
frame_status_t frame_validate(const uint8_t *frame, size_t len) { ... }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```cpp
|
|
55
|
+
// @verifies SWR-2
|
|
56
|
+
TEST(RingBuffer, PushOnFullFails) { ... }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
3. **JUnit XML** (pytest, Ceedling, GoogleTest, CTest, Jest, Maven…) turns *covered* into
|
|
60
|
+
*verified* or *failing*.
|
|
61
|
+
|
|
62
|
+
4. **Rules** in `reqcov.yml` decide what fails the build: minimum coverage, unknown ids,
|
|
63
|
+
orphan tests, mandatory parent links per level, mandatory `@implements` per level.
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install reqcov
|
|
69
|
+
reqcov init # writes reqcov.yml — edit the globs
|
|
70
|
+
pytest --junitxml=reports/junit.xml
|
|
71
|
+
reqcov check # exit 1 on rule violations, writes reqcov-report/
|
|
72
|
+
open reqcov-report/index.html
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`reqcov-report/` contains `index.html` (interactive matrix), `matrix.csv` (auditor-friendly),
|
|
76
|
+
`coverage.json` (machine readable) and `summary.md` (the PR comment).
|
|
77
|
+
|
|
78
|
+
## GitHub Action
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
name: requirements
|
|
82
|
+
on: [pull_request, push]
|
|
83
|
+
jobs:
|
|
84
|
+
reqcov:
|
|
85
|
+
runs-on: ubuntu-latest
|
|
86
|
+
permissions:
|
|
87
|
+
contents: read
|
|
88
|
+
pull-requests: write
|
|
89
|
+
steps:
|
|
90
|
+
- uses: actions/checkout@v4
|
|
91
|
+
- run: pip install -e . pytest && pytest --junitxml=reports/junit.xml
|
|
92
|
+
- uses: Antoine005/reqcov@v0
|
|
93
|
+
with:
|
|
94
|
+
junit: reports/junit.xml
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The action posts (and keeps updating) one sticky comment on the pull request, writes the
|
|
98
|
+
summary to the job page, emits annotations on the requirement lines that are uncovered or
|
|
99
|
+
failing, and uploads the report directory as an artifact.
|
|
100
|
+
|
|
101
|
+
## Configuration (`reqcov.yml`)
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
id_pattern: "[A-Z][A-Z0-9_]*-\\d+" # level = everything before the last dash
|
|
105
|
+
requirements: [docs/requirements/**/*.md]
|
|
106
|
+
sources: [src/**/*] # scanned for @implements markers
|
|
107
|
+
tests: [tests/**/*] # scanned for @req / @verifies markers
|
|
108
|
+
junit: [reports/*.xml]
|
|
109
|
+
markers: [req, requirement, implements, verifies, satisfies, trace]
|
|
110
|
+
rules:
|
|
111
|
+
min_test_coverage: 100 # % of testable requirements with ≥ 1 test
|
|
112
|
+
min_verified: null # % that must be verified (needs junit)
|
|
113
|
+
fail_on_unknown_ids: true
|
|
114
|
+
fail_on_orphan_tests: false
|
|
115
|
+
fail_on_failing_tests: true
|
|
116
|
+
require_parent_for: [SRS] # levels that must trace up
|
|
117
|
+
require_source_for: [] # levels that must have an @implements
|
|
118
|
+
allow_derived: true # missing parent = warning (false: error)
|
|
119
|
+
report:
|
|
120
|
+
out_dir: reqcov-report
|
|
121
|
+
formats: [html, csv, json, md]
|
|
122
|
+
title: "Software Requirements Traceability"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Requirement metadata
|
|
126
|
+
|
|
127
|
+
| Field | Markdown body line | YAML key | Values |
|
|
128
|
+
|---|---|---|---|
|
|
129
|
+
| parents | `Parent: SYS-1, SYS-2` | `parent` / `parents` / `links` | ids |
|
|
130
|
+
| verification | `Verification: test` | `verification` | `test` (default), `analysis`, `inspection`, `demonstration`, `none` |
|
|
131
|
+
| status | `Status: draft` | `status` (Doorstop: `active: false` → obsolete) | free text; `obsolete` is ignored by rules |
|
|
132
|
+
| tags | `Tags: safety, ui` | `tags` | list |
|
|
133
|
+
|
|
134
|
+
Only requirements with `verification: test` count toward test coverage; the others are shown
|
|
135
|
+
as `n/a` in the matrix so the auditor still sees them.
|
|
136
|
+
|
|
137
|
+
## Examples
|
|
138
|
+
|
|
139
|
+
- [`examples/pytest-project`](examples/pytest-project) — Python, SYS→SRS levels, one deliberate gap and one orphan test.
|
|
140
|
+
- [`examples/ceedling-unity`](examples/ceedling-unity) — C, HLR→LLR, `@implements` in sources, a failing Unity test propagating to two requirements.
|
|
141
|
+
- [`examples/googletest`](examples/googletest) — C++, one-line requirements, GoogleTest `Suite.Name` results.
|
|
142
|
+
|
|
143
|
+
## Status and roadmap
|
|
144
|
+
|
|
145
|
+
`0.1` — CLI, Markdown/YAML/Doorstop input, marker scanning, JUnit merge, HTML/CSV/JSON/MD
|
|
146
|
+
reports, GitHub Action with sticky PR comment. Planned: coverage delta against the base
|
|
147
|
+
branch, StrictDoc and ReqIF input, Jira issue links, GitLab CI template, signed PDF export for
|
|
148
|
+
audit packages, hosted history and badges.
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "reqcov"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Automate requirements traceability: trace requirements to tests and code, fail CI on gaps, generate audit-ready traceability matrices."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "reqcov contributors" }]
|
|
13
|
+
keywords = ["requirements", "traceability", "IEC 62304", "ISO 26262", "DO-178C", "EN 50128", "CI", "github-actions"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
20
|
+
"Topic :: Software Development :: Testing",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["PyYAML>=6.0", "Jinja2>=3.1"]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = ["pytest>=7", "pytest-cov"]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
reqcov = "reqcov.cli:main"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/Antoine005/reqcov"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
include = ["reqcov*"]
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.package-data]
|
|
37
|
+
reqcov = ["templates/*.html"]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Command line interface.
|
|
2
|
+
|
|
3
|
+
reqcov check [--config reqcov.yml] [--root .] [--junit reports/*.xml] [--out dir] [--no-report]
|
|
4
|
+
reqcov report ... same options, never fails the build
|
|
5
|
+
reqcov init write an example reqcov.yml
|
|
6
|
+
reqcov list print requirements found
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from typing import List, Optional
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
from .config import EXAMPLE_CONFIG, Config
|
|
17
|
+
from .coverage import analyze
|
|
18
|
+
from .report import render_markdown, write_reports
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _common(p: argparse.ArgumentParser) -> None:
|
|
22
|
+
p.add_argument("--config", "-c", help="path to reqcov.yml (default: auto-detect in root)")
|
|
23
|
+
p.add_argument("--root", "-r", default=".", help="repository root (default: .)")
|
|
24
|
+
p.add_argument("--junit", action="append", help="JUnit XML glob (repeatable, overrides config)")
|
|
25
|
+
p.add_argument("--out", "-o", help="report output directory (overrides config)")
|
|
26
|
+
p.add_argument("--format", "-f", action="append", choices=["html", "csv", "json", "md"], help="report formats (repeatable)")
|
|
27
|
+
p.add_argument("--no-report", action="store_true", help="do not write report files")
|
|
28
|
+
p.add_argument("--quiet", "-q", action="store_true")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
32
|
+
p = argparse.ArgumentParser(prog="reqcov", description="Requirements coverage for pull requests.")
|
|
33
|
+
p.add_argument("--version", action="version", version=f"reqcov {__version__}")
|
|
34
|
+
sub = p.add_subparsers(dest="cmd")
|
|
35
|
+
c = sub.add_parser("check", help="analyze and fail (exit 1) when rules are violated")
|
|
36
|
+
_common(c)
|
|
37
|
+
r = sub.add_parser("report", help="analyze and write reports, never fails")
|
|
38
|
+
_common(r)
|
|
39
|
+
i = sub.add_parser("init", help="write an example reqcov.yml")
|
|
40
|
+
i.add_argument("--root", "-r", default=".")
|
|
41
|
+
i.add_argument("--force", action="store_true")
|
|
42
|
+
l = sub.add_parser("list", help="list requirements found")
|
|
43
|
+
_common(l)
|
|
44
|
+
return p
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _load(args) -> Config:
|
|
48
|
+
cfg = Config.load(args.config, root=args.root)
|
|
49
|
+
if getattr(args, "junit", None):
|
|
50
|
+
cfg.junit = args.junit
|
|
51
|
+
if getattr(args, "out", None):
|
|
52
|
+
cfg.report.out_dir = args.out
|
|
53
|
+
if getattr(args, "format", None):
|
|
54
|
+
cfg.report.formats = args.format
|
|
55
|
+
return cfg
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
59
|
+
args = build_parser().parse_args(argv)
|
|
60
|
+
if args.cmd is None:
|
|
61
|
+
build_parser().print_help()
|
|
62
|
+
return 2
|
|
63
|
+
|
|
64
|
+
if args.cmd == "init":
|
|
65
|
+
path = os.path.join(args.root, "reqcov.yml")
|
|
66
|
+
if os.path.exists(path) and not args.force:
|
|
67
|
+
print(f"{path} already exists (use --force to overwrite)", file=sys.stderr)
|
|
68
|
+
return 1
|
|
69
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
70
|
+
fh.write(EXAMPLE_CONFIG)
|
|
71
|
+
print(f"wrote {path}")
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
cfg = _load(args)
|
|
75
|
+
report = analyze(cfg)
|
|
76
|
+
|
|
77
|
+
if args.cmd == "list":
|
|
78
|
+
for level, rows in report.by_level().items():
|
|
79
|
+
print(f"[{level}]")
|
|
80
|
+
for rc in rows:
|
|
81
|
+
r = rc.requirement
|
|
82
|
+
print(f" {r.id:<12} {rc.verification_status:<10} {r.title[:70]}")
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
if not args.no_report:
|
|
86
|
+
written = write_reports(report, cfg)
|
|
87
|
+
else:
|
|
88
|
+
written = {}
|
|
89
|
+
|
|
90
|
+
if not args.quiet:
|
|
91
|
+
print(render_markdown(report))
|
|
92
|
+
for fmt, p in written.items():
|
|
93
|
+
print(f"[reqcov] wrote {fmt}: {p}")
|
|
94
|
+
|
|
95
|
+
# GitHub Actions integration: job summary + annotations
|
|
96
|
+
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
|
|
97
|
+
if summary_path:
|
|
98
|
+
with open(summary_path, "a", encoding="utf-8") as fh:
|
|
99
|
+
fh.write(render_markdown(report))
|
|
100
|
+
if os.environ.get("GITHUB_ACTIONS") == "true":
|
|
101
|
+
for f in report.findings:
|
|
102
|
+
if f.severity == "info":
|
|
103
|
+
continue
|
|
104
|
+
level = "error" if f.severity == "error" else "warning"
|
|
105
|
+
loc = f" file={f.file}" + (f",line={f.line}" if f.line else "") if f.file else ""
|
|
106
|
+
print(f"::{level}{loc}::[{f.code}] {f.message}")
|
|
107
|
+
out = os.environ.get("GITHUB_OUTPUT")
|
|
108
|
+
if out:
|
|
109
|
+
with open(out, "a", encoding="utf-8") as fh:
|
|
110
|
+
fh.write(f"coverage={report.test_coverage_pct():.1f}\n")
|
|
111
|
+
fh.write(f"errors={len(report.errors)}\n")
|
|
112
|
+
fh.write(f"report_dir={os.path.join(cfg.root, cfg.report.out_dir)}\n")
|
|
113
|
+
|
|
114
|
+
if args.cmd == "check" and report.errors:
|
|
115
|
+
if not args.quiet:
|
|
116
|
+
print(f"[reqcov] FAILED with {len(report.errors)} error(s)", file=sys.stderr)
|
|
117
|
+
return 1
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__": # pragma: no cover
|
|
122
|
+
sys.exit(main())
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Configuration loading (reqcov.yml)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
DEFAULT_CONFIG_NAMES = ("reqcov.yml", "reqcov.yaml", ".reqcov.yml")
|
|
11
|
+
|
|
12
|
+
DEFAULT_ID_PATTERN = r"[A-Z][A-Z0-9_]*-\d+"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Rules:
|
|
17
|
+
min_test_coverage: float = 100.0 # % of testable requirements with >= 1 linked test
|
|
18
|
+
min_verified: Optional[float] = None # % verified (needs JUnit); None = not enforced
|
|
19
|
+
fail_on_unknown_ids: bool = True
|
|
20
|
+
fail_on_orphan_tests: bool = False
|
|
21
|
+
fail_on_failing_tests: bool = True
|
|
22
|
+
require_parent_for: List[str] = field(default_factory=list) # levels that must have a parent
|
|
23
|
+
require_source_for: List[str] = field(default_factory=list) # levels that must have an implements link
|
|
24
|
+
allow_derived: bool = True # if False, missing parent is an error instead of warning
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ReportConfig:
|
|
29
|
+
out_dir: str = "reqcov-report"
|
|
30
|
+
formats: List[str] = field(default_factory=lambda: ["html", "csv", "json", "md"])
|
|
31
|
+
title: str = "Requirements Traceability"
|
|
32
|
+
project: str = ""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Config:
|
|
37
|
+
root: str = "."
|
|
38
|
+
id_pattern: str = DEFAULT_ID_PATTERN
|
|
39
|
+
requirements: List[str] = field(default_factory=lambda: ["docs/requirements/**/*.md", "docs/requirements/**/*.yml"])
|
|
40
|
+
sources: List[str] = field(default_factory=lambda: ["src/**/*"])
|
|
41
|
+
tests: List[str] = field(default_factory=lambda: ["tests/**/*", "test/**/*"])
|
|
42
|
+
junit: List[str] = field(default_factory=list)
|
|
43
|
+
exclude: List[str] = field(default_factory=lambda: ["**/node_modules/**", "**/.git/**", "**/build/**", "**/.venv/**"])
|
|
44
|
+
markers: List[str] = field(default_factory=lambda: ["req", "requirement", "requirements", "implements", "verifies", "satisfies", "trace", "traces"])
|
|
45
|
+
rules: Rules = field(default_factory=Rules)
|
|
46
|
+
report: ReportConfig = field(default_factory=ReportConfig)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def load(path: Optional[str] = None, root: Optional[str] = None) -> "Config":
|
|
50
|
+
root = root or "."
|
|
51
|
+
data: Dict[str, Any] = {}
|
|
52
|
+
cfg_path = path
|
|
53
|
+
if cfg_path is None:
|
|
54
|
+
for name in DEFAULT_CONFIG_NAMES:
|
|
55
|
+
candidate = os.path.join(root, name)
|
|
56
|
+
if os.path.exists(candidate):
|
|
57
|
+
cfg_path = candidate
|
|
58
|
+
break
|
|
59
|
+
if cfg_path and os.path.exists(cfg_path):
|
|
60
|
+
with open(cfg_path, "r", encoding="utf-8") as fh:
|
|
61
|
+
data = yaml.safe_load(fh) or {}
|
|
62
|
+
return Config.from_dict(data, root=root)
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def from_dict(data: Dict[str, Any], root: str = ".") -> "Config":
|
|
66
|
+
cfg = Config(root=root)
|
|
67
|
+
if "id_pattern" in data:
|
|
68
|
+
cfg.id_pattern = str(data["id_pattern"])
|
|
69
|
+
for key in ("requirements", "sources", "tests", "junit", "exclude", "markers"):
|
|
70
|
+
if key in data and data[key] is not None:
|
|
71
|
+
val = data[key]
|
|
72
|
+
if isinstance(val, str):
|
|
73
|
+
val = [val]
|
|
74
|
+
# accept list of {path: ...} too
|
|
75
|
+
cfg.__dict__[key] = [v["path"] if isinstance(v, dict) else str(v) for v in val]
|
|
76
|
+
rules = data.get("rules") or {}
|
|
77
|
+
for k, v in rules.items():
|
|
78
|
+
if hasattr(cfg.rules, k):
|
|
79
|
+
setattr(cfg.rules, k, v)
|
|
80
|
+
rep = data.get("report") or {}
|
|
81
|
+
for k, v in rep.items():
|
|
82
|
+
if hasattr(cfg.report, k):
|
|
83
|
+
setattr(cfg.report, k, v)
|
|
84
|
+
return cfg
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
EXAMPLE_CONFIG = """# reqcov configuration — see https://github.com/Antoine005/reqcov
|
|
88
|
+
version: 1
|
|
89
|
+
|
|
90
|
+
# Regex for requirement identifiers. Level = everything before the last dash (SYS, SRS, HLR, LLR...).
|
|
91
|
+
id_pattern: "[A-Z][A-Z0-9_]*-\\\\d+"
|
|
92
|
+
|
|
93
|
+
# Where requirements live (Markdown headings, YAML lists, or Doorstop items).
|
|
94
|
+
requirements:
|
|
95
|
+
- docs/requirements/**/*.md
|
|
96
|
+
- docs/requirements/**/*.yml
|
|
97
|
+
|
|
98
|
+
# Files scanned for `@implements REQ-1` style markers (traces to code).
|
|
99
|
+
sources:
|
|
100
|
+
- src/**/*
|
|
101
|
+
|
|
102
|
+
# Files scanned for `@req REQ-1` / `@verifies REQ-1` markers (traces to tests).
|
|
103
|
+
tests:
|
|
104
|
+
- tests/**/*
|
|
105
|
+
|
|
106
|
+
# Optional JUnit XML results: turns "covered" into "verified" / "failing".
|
|
107
|
+
junit:
|
|
108
|
+
- reports/**/*.xml
|
|
109
|
+
|
|
110
|
+
rules:
|
|
111
|
+
min_test_coverage: 100 # % of testable requirements that must have at least one test
|
|
112
|
+
fail_on_unknown_ids: true # a marker references an id that does not exist
|
|
113
|
+
fail_on_orphan_tests: false # a test has no requirement marker
|
|
114
|
+
fail_on_failing_tests: true # a linked test failed (needs junit)
|
|
115
|
+
require_parent_for: [SRS] # these levels must trace up to a parent requirement
|
|
116
|
+
|
|
117
|
+
report:
|
|
118
|
+
out_dir: reqcov-report
|
|
119
|
+
formats: [html, csv, json, md]
|
|
120
|
+
title: "Software Requirements Traceability"
|
|
121
|
+
"""
|