Skip to content

Commit 499d02f

Browse files
committed
refactoring the coverage, metrics and dashboard
1 parent 1677f66 commit 499d02f

5 files changed

Lines changed: 321 additions & 190 deletions

File tree

docs/internals/requirements/implementation_state.rst

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,9 @@ Overview
2020
--------
2121

2222
.. needpie:: Requirements Status
23-
:labels: not implemented, implemented but not tested, implemented and tested
23+
:labels: not implemented, implemented but incomplete docs, fully documented
2424
:colors: red,yellow, green
25-
26-
type == 'tool_req' and implemented == 'NO'
27-
type == 'tool_req' and testlink == '' and (implemented == 'YES' or implemented == 'PARTIAL')
28-
type == 'tool_req' and testlink != '' and (implemented == 'YES' or implemented == 'PARTIAL')
25+
:filter-func: src.extensions.score_metamodel.checks.traceability_dashboard.pie_requirements_status(tool_req)
2926

3027
In Detail
3128
---------
@@ -48,9 +45,7 @@ In Detail
4845
.. needpie:: Requirements with Codelinks
4946
:labels: no codelink, with codelink
5047
:colors: red, green
51-
52-
type == 'tool_req' and source_code_link == ''
53-
type == 'tool_req' and source_code_link != ''
48+
:filter-func: src.extensions.score_metamodel.checks.traceability_dashboard.pie_requirements_with_code_links(tool_req)
5449

5550
.. grid-item-card::
5651

scripts_bazel/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,5 @@ py_binary(
4343
srcs = ["traceability_coverage.py"],
4444
main = "traceability_coverage.py",
4545
visibility = ["//visibility:public"],
46+
deps = all_requirements + ["//src/extensions/score_metamodel:score_metamodel"],
4647
)

scripts_bazel/traceability_coverage.py

Lines changed: 41 additions & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -16,40 +16,27 @@
1616
from __future__ import annotations
1717

1818
import argparse
19+
import importlib.util
1920
import json
2021
import os
22+
import sys
2123
from pathlib import Path
2224
from typing import Any
2325

26+
# Ensure shared metric code under src/ is importable when executed directly.
27+
_REPO_ROOT = Path(__file__).resolve().parent.parent
28+
if str(_REPO_ROOT) not in sys.path:
29+
sys.path.insert(0, str(_REPO_ROOT))
2430

25-
def _is_non_empty(value: Any) -> bool:
26-
if value is None:
27-
return False
28-
if isinstance(value, str):
29-
return bool(value.strip())
30-
if isinstance(value, (list, tuple, set, dict)):
31-
return len(value) > 0
32-
return True
31+
# Import only the metrics module, avoid heavy __init__.py
32+
_metrics_path = _REPO_ROOT / "src/extensions/score_metamodel/traceability_metrics.py"
33+
_spec = importlib.util.spec_from_file_location("traceability_metrics", _metrics_path)
34+
if _spec is None or _spec.loader is None:
35+
raise ImportError(f"Failed to load metrics module from {_metrics_path}")
36+
traceability_metrics = importlib.util.module_from_spec(_spec)
37+
_spec.loader.exec_module(traceability_metrics)
3338

34-
35-
def _parse_need_id_list(value: Any) -> list[str]:
36-
if value is None:
37-
return []
38-
if isinstance(value, str):
39-
return [item.strip() for item in value.split(",") if item.strip()]
40-
if isinstance(value, list):
41-
out: list[str] = []
42-
for item in value:
43-
if isinstance(item, str) and item.strip():
44-
out.append(item.strip())
45-
return out
46-
return []
47-
48-
49-
def _safe_percent(numerator: int, denominator: int) -> float:
50-
if denominator == 0:
51-
return 100.0
52-
return (numerator / denominator) * 100.0
39+
compute_traceability_summary = traceability_metrics.compute_traceability_summary
5340

5441

5542
def _load_needs(needs_json: Path) -> list[dict[str, Any]]:
@@ -121,113 +108,6 @@ def _apply_argument_shortcuts(args: argparse.Namespace) -> None:
121108
args.fail_on_broken_test_refs = True
122109

123110

124-
def _filter_requirements(
125-
all_needs: list[dict[str, Any]],
126-
requirement_types: set[str],
127-
include_not_implemented: bool,
128-
) -> list[dict[str, Any]]:
129-
"""Extract and filter requirements from needs."""
130-
requirements: list[dict[str, Any]] = []
131-
for need in all_needs:
132-
need_type = str(need.get("type", "")).strip()
133-
if need_type not in requirement_types:
134-
continue
135-
if not include_not_implemented:
136-
implemented = str(need.get("implemented", "")).upper().strip()
137-
if implemented not in {"YES", "PARTIAL"}:
138-
continue
139-
requirements.append(need)
140-
return requirements
141-
142-
143-
def _calculate_requirement_metrics(
144-
requirements: list[dict[str, Any]],
145-
) -> tuple[int, int, int, int, list[str], list[str], list[str]]:
146-
"""Calculate traceability metrics for requirements."""
147-
req_total = len(requirements)
148-
req_with_code = sum(
149-
1 for need in requirements if _is_non_empty(need.get("source_code_link"))
150-
)
151-
req_with_test = sum(
152-
1 for need in requirements if _is_non_empty(need.get("testlink"))
153-
)
154-
req_fully_linked = sum(
155-
1
156-
for need in requirements
157-
if _is_non_empty(need.get("source_code_link"))
158-
and _is_non_empty(need.get("testlink"))
159-
)
160-
req_missing_code = [
161-
str(need.get("id", ""))
162-
for need in requirements
163-
if not _is_non_empty(need.get("source_code_link")) and need.get("id")
164-
]
165-
req_missing_test = [
166-
str(need.get("id", ""))
167-
for need in requirements
168-
if not _is_non_empty(need.get("testlink")) and need.get("id")
169-
]
170-
req_not_fully_linked = [
171-
str(need.get("id", ""))
172-
for need in requirements
173-
if (
174-
(
175-
not _is_non_empty(need.get("source_code_link"))
176-
or not _is_non_empty(need.get("testlink"))
177-
)
178-
and need.get("id")
179-
)
180-
]
181-
return (
182-
req_total,
183-
req_with_code,
184-
req_with_test,
185-
req_fully_linked,
186-
req_missing_code,
187-
req_missing_test,
188-
req_not_fully_linked,
189-
)
190-
191-
192-
def _calculate_test_metrics(
193-
all_needs: list[dict[str, Any]],
194-
requirement_ids: set[str],
195-
filtered_test_types: set[str],
196-
) -> tuple[int, int, list[dict[str, str]]]:
197-
"""Calculate test linkage metrics and find broken references."""
198-
testcases = [
199-
need for need in all_needs if str(need.get("type", "")).strip() == "testcase"
200-
]
201-
if filtered_test_types:
202-
testcases = [
203-
need
204-
for need in testcases
205-
if str(need.get("test_type", need.get("TestType", ""))).strip()
206-
in filtered_test_types
207-
]
208-
tests_total = len(testcases)
209-
210-
tests_linked = 0
211-
broken_test_references: list[dict[str, str]] = []
212-
for test_need in testcases:
213-
test_id = str(test_need.get("id", "<unknown_testcase>"))
214-
partially = _parse_need_id_list(
215-
test_need.get("partially_verifies", test_need.get("PartiallyVerifies"))
216-
)
217-
fully = _parse_need_id_list(
218-
test_need.get("fully_verifies", test_need.get("FullyVerifies"))
219-
)
220-
refs = partially + fully
221-
if refs:
222-
tests_linked += 1
223-
for ref in refs:
224-
if ref not in requirement_ids:
225-
broken_test_references.append(
226-
{"testcase": test_id, "missing_need": ref}
227-
)
228-
return tests_total, tests_linked, broken_test_references
229-
230-
231111
def _print_summary(
232112
needs_json: Path,
233113
req_total: int,
@@ -432,56 +312,35 @@ def main() -> int:
432312
needs_json = _find_needs_json(args.needs_json)
433313
all_needs = _load_needs(needs_json)
434314

435-
requirements = _filter_requirements(
436-
all_needs, requirement_types, args.include_not_implemented
437-
)
438-
439-
requirement_ids = {
440-
str(need.get("id", "")).strip() for need in requirements if need.get("id")
441-
}
442-
443-
(
444-
req_total,
445-
req_with_code,
446-
req_with_test,
447-
req_fully_linked,
448-
req_missing_code,
449-
req_missing_test,
450-
req_not_fully_linked,
451-
) = _calculate_requirement_metrics(requirements)
452-
453-
tests_total, tests_linked, broken_test_references = _calculate_test_metrics(
454-
all_needs, requirement_ids, filtered_test_types
315+
summary = compute_traceability_summary(
316+
all_needs=all_needs,
317+
requirement_types=requirement_types,
318+
include_not_implemented=args.include_not_implemented,
319+
filtered_test_types=filtered_test_types,
455320
)
456321

457-
req_code_pct = _safe_percent(req_with_code, req_total)
458-
req_test_pct = _safe_percent(req_with_test, req_total)
459-
req_fully_linked_pct = _safe_percent(req_fully_linked, req_total)
460-
tests_linked_pct = _safe_percent(tests_linked, tests_total)
461-
462-
summary = {
322+
req_total = int(summary["requirements"]["total"])
323+
req_with_code = int(summary["requirements"]["with_code_link"])
324+
req_with_test = int(summary["requirements"]["with_test_link"])
325+
req_fully_linked = int(summary["requirements"]["fully_linked"])
326+
req_code_pct = float(summary["requirements"]["with_code_link_pct"])
327+
req_test_pct = float(summary["requirements"]["with_test_link_pct"])
328+
req_fully_linked_pct = float(summary["requirements"]["fully_linked_pct"])
329+
req_missing_code = list(summary["requirements"]["missing_code_link_ids"])
330+
req_missing_test = list(summary["requirements"]["missing_test_link_ids"])
331+
req_not_fully_linked = list(summary["requirements"]["not_fully_linked_ids"])
332+
333+
tests_total = int(summary["tests"]["total"])
334+
tests_linked = int(summary["tests"]["linked_to_requirements"])
335+
tests_linked_pct = float(summary["tests"]["linked_to_requirements_pct"])
336+
broken_test_references = list(summary["tests"]["broken_references"])
337+
338+
summary_output = {
463339
"needs_json": str(needs_json),
464-
"requirement_types": sorted(requirement_types),
465-
"include_not_implemented": bool(args.include_not_implemented),
466-
"requirements": {
467-
"total": req_total,
468-
"with_code_link": req_with_code,
469-
"with_test_link": req_with_test,
470-
"fully_linked": req_fully_linked,
471-
"with_code_link_pct": req_code_pct,
472-
"with_test_link_pct": req_test_pct,
473-
"fully_linked_pct": req_fully_linked_pct,
474-
"missing_code_link_ids": sorted(req_missing_code),
475-
"missing_test_link_ids": sorted(req_missing_test),
476-
"not_fully_linked_ids": sorted(req_not_fully_linked),
477-
},
478-
"tests": {
479-
"total": tests_total,
480-
"filtered_test_types": sorted(filtered_test_types),
481-
"linked_to_requirements": tests_linked,
482-
"linked_to_requirements_pct": tests_linked_pct,
483-
"broken_references": broken_test_references,
484-
},
340+
"requirement_types": summary["requirement_types"],
341+
"include_not_implemented": summary["include_not_implemented"],
342+
"requirements": summary["requirements"],
343+
"tests": summary["tests"],
485344
"thresholds": {
486345
"min_req_code": float(args.min_req_code),
487346
"min_req_test": float(args.min_req_test),
@@ -512,7 +371,7 @@ def main() -> int:
512371

513372
if args.json_output:
514373
out_file = Path(args.json_output)
515-
out_file.write_text(json.dumps(summary, indent=2), encoding="utf-8")
374+
out_file.write_text(json.dumps(summary_output, indent=2), encoding="utf-8")
516375
print(f"JSON summary written to: {out_file}")
517376

518377
failures = _check_thresholds(
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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+
"""Needpie filter functions backed by shared traceability metric calculations."""
15+
16+
from __future__ import annotations
17+
18+
from sphinx_needs.need_item import NeedItem
19+
20+
from ..traceability_metrics import compute_traceability_summary, filter_requirements
21+
22+
23+
def _requirement_types(kwargs: dict[str, str | int | float]) -> set[str]:
24+
raw = str(kwargs.get("arg1", "tool_req")).strip()
25+
values = {value.strip() for value in raw.split(",") if value.strip()}
26+
return values or {"tool_req"}
27+
28+
29+
def pie_requirements_status(
30+
needs: list[NeedItem], results: list[int], **kwargs: str | int | float
31+
) -> None:
32+
"""Dashboard status split: not implemented, implemented/incomplete, fully linked."""
33+
req_types = _requirement_types(kwargs)
34+
35+
all_requirements = filter_requirements(
36+
needs,
37+
requirement_types=req_types,
38+
include_not_implemented=True,
39+
)
40+
implemented_requirements = filter_requirements(
41+
needs,
42+
requirement_types=req_types,
43+
include_not_implemented=False,
44+
)
45+
summary = compute_traceability_summary(
46+
all_needs=needs,
47+
requirement_types=req_types,
48+
include_not_implemented=False,
49+
filtered_test_types=set(),
50+
)
51+
52+
not_implemented = len(all_requirements) - len(implemented_requirements)
53+
fully_linked = int(summary["requirements"]["fully_linked"])
54+
implemented_incomplete = len(implemented_requirements) - fully_linked
55+
56+
results.append(not_implemented)
57+
results.append(implemented_incomplete)
58+
results.append(fully_linked)
59+
60+
61+
def pie_requirements_with_code_links(
62+
needs: list[NeedItem], results: list[int], **kwargs: str | int | float
63+
) -> None:
64+
"""Dashboard split: requirements with and without source code links."""
65+
req_types = _requirement_types(kwargs)
66+
summary = compute_traceability_summary(
67+
all_needs=needs,
68+
requirement_types=req_types,
69+
include_not_implemented=True,
70+
filtered_test_types=set(),
71+
)
72+
73+
total = int(summary["requirements"]["total"])
74+
with_code = int(summary["requirements"]["with_code_link"])
75+
76+
results.append(total - with_code)
77+
results.append(with_code)

0 commit comments

Comments
 (0)