Skip to content

Commit 6287c69

Browse files
committed
changed to new json structure
1 parent 313ff9b commit 6287c69

8 files changed

Lines changed: 1315 additions & 18 deletions

File tree

docs/internals/requirements/score_1782_open_summary.md

Lines changed: 629 additions & 0 deletions
Large diffs are not rendered by default.

scripts_bazel/BUILD

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,11 @@ py_binary(
4949
visibility = ["//visibility:public"],
5050
deps = all_requirements + ["//src/extensions/score_metamodel:score_metamodel"],
5151
)
52+
53+
py_binary(
54+
name = "traceability_gate",
55+
srcs = ["traceability_gate.py"],
56+
main = "traceability_gate.py",
57+
visibility = ["//visibility:public"],
58+
deps = all_requirements,
59+
)

scripts_bazel/tests/BUILD

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,12 @@ score_pytest(
4545
] + all_requirements,
4646
pytest_config = "//:pyproject.toml",
4747
)
48+
49+
score_pytest(
50+
name = "traceability_gate_test",
51+
srcs = ["traceability_gate_test.py"],
52+
deps = [
53+
"//scripts_bazel:traceability_gate",
54+
] + all_requirements,
55+
pytest_config = "//:pyproject.toml",
56+
)

scripts_bazel/tests/traceability_coverage_test.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,17 @@ def test_traceability_coverage_thresholds_pass(tmp_path: Path) -> None:
110110
assert output_json.exists()
111111

112112
summary = json.loads(output_json.read_text(encoding="utf-8"))
113-
assert summary["requirements"]["total"] == 2
114-
assert summary["requirements"]["with_code_link"] == 1
115-
assert summary["requirements"]["with_test_link"] == 1
116-
assert summary["requirements"]["fully_linked"] == 0
117-
assert summary["tests"]["total"] == 3
118-
assert summary["tests"]["linked_to_requirements"] == 2
119-
assert len(summary["tests"]["broken_references"]) == 1
113+
assert summary["schema_version"] == "1"
114+
assert summary["generated_by"] == "traceability_coverage"
115+
assert "tool_req" in summary["metrics_by_type"]
116+
type_metrics = summary["metrics_by_type"]["tool_req"]
117+
assert type_metrics["requirements"]["total"] == 2
118+
assert type_metrics["requirements"]["with_code_link"] == 1
119+
assert type_metrics["requirements"]["with_test_link"] == 1
120+
assert type_metrics["requirements"]["fully_linked"] == 0
121+
assert type_metrics["tests"]["total"] == 3
122+
assert type_metrics["tests"]["linked_to_requirements"] == 2
123+
assert len(type_metrics["tests"]["broken_references"]) == 1
120124

121125

122126
def test_traceability_coverage_thresholds_fail(tmp_path: Path) -> None:
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
# *******************************************************************************
2+
# Copyright (c) 2026 Contributors to the Eclipse Foundation
3+
#
4+
# See the NOTICE file(s) distributed with this work for additional
5+
# information regarding copyright ownership.
6+
#
7+
# This program and the accompanying materials are made available under the
8+
# terms of the Apache License Version 2.0 which is available at
9+
# https://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# SPDX-License-Identifier: Apache-2.0
12+
# *******************************************************************************
13+
14+
# ╓ ╖
15+
# ║ Some portions generated by Github Copilot ║
16+
# ╙ ╜
17+
18+
"""Tests for traceability_gate.py."""
19+
20+
import json
21+
import subprocess
22+
import sys
23+
from pathlib import Path
24+
25+
_MY_PATH = Path(__file__).parent
26+
27+
_GATE_SCRIPT = _MY_PATH.parent / "traceability_gate.py"
28+
29+
30+
def _write_metrics_json(tmp_path: Path, metrics_by_type: dict | None = None) -> Path:
31+
"""Write a schema-v1 metrics JSON and return its path."""
32+
if metrics_by_type is None:
33+
metrics_by_type = {
34+
"tool_req": {
35+
"include_not_implemented": False,
36+
"requirements": {
37+
"total": 4,
38+
"with_code_link": 3,
39+
"with_test_link": 2,
40+
"fully_linked": 2,
41+
"with_code_link_pct": 75.0,
42+
"with_test_link_pct": 50.0,
43+
"fully_linked_pct": 50.0,
44+
"missing_code_link_ids": ["REQ_4"],
45+
"missing_test_link_ids": ["REQ_3", "REQ_4"],
46+
"not_fully_linked_ids": ["REQ_3", "REQ_4"],
47+
},
48+
"tests": {
49+
"total": 3,
50+
"filtered_test_types": [],
51+
"linked_to_requirements": 2,
52+
"linked_to_requirements_pct": 66.67,
53+
"broken_references": [],
54+
},
55+
}
56+
}
57+
payload = {
58+
"schema_version": "1",
59+
"generated_by": "traceability_coverage",
60+
"needs_json": "fake/needs.json",
61+
"metrics_by_type": metrics_by_type,
62+
}
63+
out = tmp_path / "metrics.json"
64+
out.write_text(json.dumps(payload), encoding="utf-8")
65+
return out
66+
67+
68+
def _run_gate(metrics_json: Path, extra_args: list[str]) -> subprocess.CompletedProcess:
69+
return subprocess.run(
70+
[sys.executable, _GATE_SCRIPT, "--metrics-json", str(metrics_json)]
71+
+ extra_args,
72+
capture_output=True,
73+
text=True,
74+
)
75+
76+
77+
def test_gate_passes_when_thresholds_met(tmp_path: Path) -> None:
78+
metrics_json = _write_metrics_json(tmp_path)
79+
80+
result = _run_gate(
81+
metrics_json,
82+
["--min-req-code", "70", "--min-req-test", "50", "--min-tests-linked", "60"],
83+
)
84+
85+
assert result.returncode == 0
86+
assert "Threshold check passed." in result.stdout
87+
88+
89+
def test_gate_fails_when_threshold_not_met(tmp_path: Path) -> None:
90+
metrics_json = _write_metrics_json(tmp_path)
91+
92+
result = _run_gate(
93+
metrics_json,
94+
["--min-req-code", "100"],
95+
)
96+
97+
assert result.returncode == 2
98+
assert "Threshold check failed:" in result.stdout
99+
assert "[tool_req] requirements with code links" in result.stdout
100+
101+
102+
def test_gate_require_all_links_fails(tmp_path: Path) -> None:
103+
metrics_json = _write_metrics_json(tmp_path)
104+
105+
result = _run_gate(metrics_json, ["--require-all-links"])
106+
107+
assert result.returncode == 2
108+
assert "Threshold check failed:" in result.stdout
109+
110+
111+
def test_gate_fail_on_broken_refs(tmp_path: Path) -> None:
112+
metrics_by_type = {
113+
"tool_req": {
114+
"include_not_implemented": False,
115+
"requirements": {
116+
"total": 1,
117+
"with_code_link": 1,
118+
"with_test_link": 1,
119+
"fully_linked": 1,
120+
"with_code_link_pct": 100.0,
121+
"with_test_link_pct": 100.0,
122+
"fully_linked_pct": 100.0,
123+
"missing_code_link_ids": [],
124+
"missing_test_link_ids": [],
125+
"not_fully_linked_ids": [],
126+
},
127+
"tests": {
128+
"total": 2,
129+
"filtered_test_types": [],
130+
"linked_to_requirements": 2,
131+
"linked_to_requirements_pct": 100.0,
132+
"broken_references": [
133+
{"testcase": "TC_X", "missing_need": "REQ_UNKNOWN"}
134+
],
135+
},
136+
}
137+
}
138+
metrics_json = _write_metrics_json(tmp_path, metrics_by_type)
139+
140+
result = _run_gate(metrics_json, ["--fail-on-broken-test-refs"])
141+
142+
assert result.returncode == 2
143+
assert "broken testcase references found:" in result.stdout
144+
145+
146+
def test_gate_specific_need_type(tmp_path: Path) -> None:
147+
metrics_by_type = {
148+
"tool_req": {
149+
"include_not_implemented": False,
150+
"requirements": {
151+
"total": 2,
152+
"with_code_link": 2,
153+
"with_test_link": 2,
154+
"fully_linked": 2,
155+
"with_code_link_pct": 100.0,
156+
"with_test_link_pct": 100.0,
157+
"fully_linked_pct": 100.0,
158+
"missing_code_link_ids": [],
159+
"missing_test_link_ids": [],
160+
"not_fully_linked_ids": [],
161+
},
162+
"tests": {
163+
"total": 1,
164+
"filtered_test_types": [],
165+
"linked_to_requirements": 1,
166+
"linked_to_requirements_pct": 100.0,
167+
"broken_references": [],
168+
},
169+
},
170+
"comp_req": {
171+
"include_not_implemented": False,
172+
"requirements": {
173+
"total": 5,
174+
"with_code_link": 0,
175+
"with_test_link": 0,
176+
"fully_linked": 0,
177+
"with_code_link_pct": 0.0,
178+
"with_test_link_pct": 0.0,
179+
"fully_linked_pct": 0.0,
180+
"missing_code_link_ids": ["C1", "C2", "C3", "C4", "C5"],
181+
"missing_test_link_ids": ["C1", "C2", "C3", "C4", "C5"],
182+
"not_fully_linked_ids": ["C1", "C2", "C3", "C4", "C5"],
183+
},
184+
"tests": {
185+
"total": 0,
186+
"filtered_test_types": [],
187+
"linked_to_requirements": 0,
188+
"linked_to_requirements_pct": 100.0,
189+
"broken_references": [],
190+
},
191+
},
192+
}
193+
metrics_json = _write_metrics_json(tmp_path, metrics_by_type)
194+
195+
# Gate only on tool_req (which is fully linked) — comp_req failures are ignored
196+
result = _run_gate(
197+
metrics_json,
198+
["--need-type", "tool_req", "--require-all-links"],
199+
)
200+
201+
assert result.returncode == 0
202+
assert "[tool_req]" in result.stdout
203+
assert "[comp_req]" not in result.stdout
204+
205+
206+
def test_gate_unknown_need_type_fails(tmp_path: Path) -> None:
207+
metrics_json = _write_metrics_json(tmp_path)
208+
209+
result = _run_gate(metrics_json, ["--need-type", "nonexistent_req"])
210+
211+
assert result.returncode == 2
212+
assert "not found in metrics JSON" in result.stdout
213+
214+
215+
def test_gate_unsupported_schema_version(tmp_path: Path) -> None:
216+
bad = tmp_path / "bad.json"
217+
bad.write_text(
218+
json.dumps({"schema_version": "99", "metrics_by_type": {}}), encoding="utf-8"
219+
)
220+
221+
result = _run_gate(bad, [])
222+
223+
assert result.returncode == 1
224+
assert "unsupported schema_version" in result.stderr
225+
226+
227+
def test_gate_missing_file_returns_error(tmp_path: Path) -> None:
228+
result = _run_gate(tmp_path / "does_not_exist.json", [])
229+
230+
assert result.returncode == 1
231+
assert "not found" in result.stderr

scripts_bazel/traceability_coverage.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -339,19 +339,28 @@ def main() -> int:
339339
tests_linked_pct = float(summary["tests"]["linked_to_requirements_pct"])
340340
broken_test_references = list(summary["tests"]["broken_references"])
341341

342+
# Build per-type metrics for the JSON output (schema v1).
343+
# Each requirement type is computed independently so downstream tools
344+
# (e.g. traceability_gate) can apply per-type thresholds.
345+
metrics_by_type: dict[str, Any] = {}
346+
for req_type in sorted(requirement_types):
347+
type_summary = compute_traceability_summary(
348+
all_needs=all_needs,
349+
requirement_types={req_type},
350+
include_not_implemented=args.include_not_implemented,
351+
filtered_test_types=filtered_test_types,
352+
)
353+
metrics_by_type[req_type] = {
354+
"include_not_implemented": type_summary["include_not_implemented"],
355+
"requirements": type_summary["requirements"],
356+
"tests": type_summary["tests"],
357+
}
358+
342359
summary_output = {
360+
"schema_version": "1",
361+
"generated_by": "traceability_coverage",
343362
"needs_json": str(needs_json),
344-
"requirement_types": summary["requirement_types"],
345-
"include_not_implemented": summary["include_not_implemented"],
346-
"requirements": summary["requirements"],
347-
"tests": summary["tests"],
348-
"thresholds": {
349-
"min_req_code": float(args.min_req_code),
350-
"min_req_test": float(args.min_req_test),
351-
"min_req_fully_linked": float(args.min_req_fully_linked),
352-
"min_tests_linked": float(args.min_tests_linked),
353-
"fail_on_broken_test_refs": bool(args.fail_on_broken_test_refs),
354-
},
363+
"metrics_by_type": metrics_by_type,
355364
}
356365

357366
_print_summary(

0 commit comments

Comments
 (0)