Skip to content

Commit 75d91a3

Browse files
committed
refactor: tidy up results generation with direct model creation
Now we have Pydantic modes, instantiate them directly and remove the unnecessary intermediate dictionary creation steps. Signed-off-by: James McCorrie <[email protected]>
1 parent 14406e4 commit 75d91a3

File tree

1 file changed

+103
-233
lines changed

1 file changed

+103
-233
lines changed

src/dvsim/flow/sim.py

Lines changed: 103 additions & 233 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from dvsim.logging import log
2727
from dvsim.modes import BuildMode, Mode, RunMode, find_mode
2828
from dvsim.regression import Regression
29-
from dvsim.report.data import FlowResults, IPMeta, ToolMeta
29+
from dvsim.report.data import FlowResults, IPMeta, Testpoint, TestResult, TestStage, ToolMeta
3030
from dvsim.sim_results import SimResults
3131
from dvsim.test import Test
3232
from dvsim.testplan import Testplan
@@ -575,257 +575,127 @@ def cov_unr(self) -> None:
575575
item._cov_unr()
576576

577577
def _gen_json_results(self, run_results: Sequence[CompletedJobStatus]) -> FlowResults:
578-
"""Return the run flow results."""
578+
"""Generate structured FlowResults from simulation run data.
579579
580-
def _pct_str_to_float(s: str) -> float | None:
581-
"""Extract percent or None.
580+
Args:
581+
run_results: completed job status.
582582
583-
Map a percentage value stored in a string with ` %` suffix to a
584-
float or to None if the conversion to Float fails.
585-
"""
586-
try:
587-
return float(s[:-2])
588-
except ValueError:
589-
return None
590-
591-
def _test_result_to_dict(tr) -> dict:
592-
"""Map a test result entry to a dict."""
593-
job_time_s = tr.job_runtime
594-
sim_time_us = tr.simulated_time
595-
pass_rate = tr.passing * 100.0 / tr.total if tr.total > 0 else 0
596-
return {
597-
"name": tr.name,
598-
"max_runtime_s": job_time_s,
599-
"simulated_time_us": sim_time_us,
600-
"passing_runs": tr.passing,
601-
"total_runs": tr.total,
602-
"pass_rate": pass_rate,
603-
}
583+
Returns:
584+
Flow results object.
604585
605-
results = {}
606-
607-
# Describe name of hardware block targeted by this run and optionally
608-
# the variant of the hardware block.
609-
results["block_name"] = self.name.lower()
610-
results["block_variant"] = self.variant.lower() or None
611-
612-
# The timestamp for this run has been taken with `utcnow()` and is
613-
# stored in a custom format. Store it in standard ISO format with
614-
# explicit timezone annotation.
615-
timestamp = datetime.strptime(self.timestamp, TS_FORMAT)
616-
timestamp = timestamp.replace(tzinfo=timezone.utc)
617-
results["report_timestamp"] = timestamp.isoformat()
618-
619-
# Extract Git properties.
620-
m = re.search(r"https://github.com/.+?/tree/([0-9a-fA-F]+)", self.revision)
621-
results["git_revision"] = m.group(1) if m else None
622-
results["git_branch_name"] = self.branch or None
623-
624-
# Describe type of report and tool used.
625-
results["report_type"] = "simulation"
626-
results["tool"] = self.tool.lower()
627-
628-
if self.build_seed and not self.run_only:
629-
results["build_seed"] = str(self.build_seed)
630-
631-
# Create dictionary to store results.
632-
results["results"] = {
633-
"testpoints": [],
634-
"unmapped_tests": [],
635-
"testplan_stage_summary": [],
636-
"coverage": {},
637-
"failure_buckets": [],
638-
}
639-
640-
# If the testplan does not yet have test results mapped to testpoints,
641-
# map them now.
586+
"""
642587
sim_results = SimResults(results=run_results)
643588
if not self.testplan.test_results_mapped:
644-
self.testplan.map_test_results(test_results=sim_results.table)
589+
self.testplan.map_test_results(sim_results.table)
590+
591+
# --- Metadata ---
592+
timestamp = datetime.strptime(self.timestamp, TS_FORMAT).replace(tzinfo=timezone.utc)
593+
594+
commit_match = re.search(r"github.com/.+?/tree/([0-9a-f]{7,40})", self.revision)
595+
commit = commit_match.group(1) if commit_match else None
596+
597+
block = IPMeta(
598+
name=self.name.lower(),
599+
variant=(self.variant or "").lower() or None,
600+
commit=commit or "",
601+
branch=self.branch or "",
602+
url=f"https://github.com/lowrisc/opentitan/tree/{commit}" if commit else "",
603+
)
604+
tool = ToolMeta(name=self.tool.lower(), version="unknown")
605+
606+
# --- Build stages only from testpoints that have at least one executed test ---
607+
stage_to_tps: defaultdict[str, dict[str, Testpoint]] = defaultdict(dict)
608+
609+
def make_test_result(tr) -> TestResult | None:
610+
if tr.total == 0 and not self.map_full_testplan:
611+
return None
645612

646-
# Extract results of testpoints and tests into the `testpoints` field.
613+
return TestResult(
614+
max_time=tr.job_runtime,
615+
sim_time=tr.simulated_time,
616+
passed=tr.passing,
617+
total=tr.total,
618+
percent=100.0 * tr.passing / (tr.total or 1),
619+
)
620+
621+
# 1. Mapped testpoints — only include if at least one test ran
647622
for tp in self.testplan.testpoints:
648-
# Ignore testpoints that contain unmapped tests, because those will
649-
# be handled separately.
650-
if tp.name in ["Unmapped tests", "N.A."]:
623+
if tp.name in {"Unmapped tests", "N.A."}:
651624
continue
652625

653-
# Extract test results for this testpoint.
654-
tests = []
626+
test_results: dict[str, TestResult] = {}
655627
for tr in tp.test_results:
656-
# Ignore test results with zero total runs unless we are told
657-
# to "map the full testplan".
658-
if tr.total == 0 and not self.map_full_testplan:
659-
continue
660-
661-
# Map test result metrics and append it to the collecting list.
662-
tests.append(_test_result_to_dict(tr))
628+
if test := make_test_result(tr):
629+
test_results[tr.name] = test
663630

664-
# Ignore testpoints for which no tests have been run unless we are
665-
# told to "map the full testplan".
666-
if len(tests) == 0 and not self.map_full_testplan:
631+
# Critical: skip entire testpoint if no tests actually ran
632+
if not test_results and not self.map_full_testplan:
667633
continue
668634

669-
# Append testpoint to results.
670-
results["results"]["testpoints"].append(
671-
{
672-
"name": tp.name,
673-
"stage": tp.stage,
674-
"tests": tests,
675-
},
676-
)
635+
# Aggregate testpoint stats
636+
tp_passed = sum(t.passed for t in test_results.values())
637+
tp_total = sum(t.total for t in test_results.values())
677638

678-
# Extract unmapped tests.
679-
unmapped_trs = [tr for tr in sim_results.table if not tr.mapped]
680-
for tr in unmapped_trs:
681-
results["results"]["unmapped_tests"].append(_test_result_to_dict(tr))
682-
683-
# Extract summary of testplan stages.
684-
if self.map_full_testplan:
685-
for k, d in self.testplan.progress.items():
686-
results["results"]["testplan_stage_summary"].append(
687-
{
688-
"name": k,
689-
"total_tests": d["total"],
690-
"written_tests": d["written"],
691-
"passing_tests": d["passing"],
692-
"pass_rate": _pct_str_to_float(d["progress"]),
693-
},
694-
)
639+
stage_to_tps[tp.stage][tp.name] = Testpoint(
640+
tests=test_results,
641+
passed=tp_passed,
642+
total=tp_total,
643+
percent=100.0 * tp_passed / tp_total if tp_total else 0.0,
644+
)
695645

696-
# Extract coverage results if coverage has been collected in this run.
697-
if self.cov_report_deploy is not None:
698-
cov = self.cov_report_deploy.cov_results_dict
699-
for k, v in cov.items():
700-
results["results"]["coverage"][k.lower()] = _pct_str_to_float(v)
701-
702-
# Extract failure buckets.
703-
if sim_results.buckets:
704-
by_tests = sorted(sim_results.buckets.items(), key=lambda i: len(i[1]), reverse=True)
705-
706-
for bucket, tests in by_tests:
707-
unique_tests = defaultdict(list)
708-
for test, line, context in tests:
709-
if test.job_type != "RunTest":
710-
continue
711-
unique_tests[test.name].append((test, line, context))
712-
713-
fts = []
714-
for test_name, test_runs in unique_tests.items():
715-
frs = []
716-
for test, line, context in test_runs:
717-
frs.append(
718-
{
719-
"seed": str(test.seed),
720-
"failure_message": {
721-
"log_file_path": str(test.log_path),
722-
"log_file_line_num": line,
723-
"text": "".join(context),
724-
},
725-
},
726-
)
727-
728-
fts.append(
729-
{
730-
"name": test_name,
731-
"failing_runs": frs,
732-
},
733-
)
734-
735-
results["results"]["failure_buckets"].append(
736-
{
737-
"identifier": bucket,
738-
"failing_tests": fts,
739-
},
740-
)
646+
# 2. Unmapped tests — only if they actually ran
647+
unmapped_tests: dict[str, TestResult] = {}
648+
for tr in sim_results.table:
649+
if not tr.mapped and (test := make_test_result(tr)):
650+
unmapped_tests[tr.name] = test
651+
652+
if unmapped_tests:
653+
tp_passed = sum(t.passed for t in unmapped_tests.values())
654+
tp_total = sum(t.total for t in unmapped_tests.values())
655+
stage_to_tps["unmapped"]["Unmapped"] = Testpoint(
656+
tests=unmapped_tests,
657+
passed=tp_passed,
658+
total=tp_total,
659+
percent=100.0 * tp_passed / tp_total if tp_total else 0.0,
660+
)
741661

742-
# Pull out the test results per stage
743-
stages = {}
744-
for testpoint_data in results["results"]["testpoints"]:
745-
stage = testpoint_data["stage"]
746-
testpoint = testpoint_data["name"]
747-
tests = testpoint_data["tests"]
748-
749-
if stage not in stages:
750-
stages[stage] = {"testpoints": {}}
751-
752-
stages[stage]["testpoints"][testpoint] = {
753-
"tests": {
754-
test["name"]: {
755-
"max_time": test["max_runtime_s"],
756-
"sim_time": test["simulated_time_us"],
757-
"passed": test["passing_runs"],
758-
"total": test["total_runs"],
759-
"percent": 100 * test["passing_runs"] / test["total_runs"],
760-
}
761-
for test in tests
762-
},
763-
}
662+
# --- Final stage aggregation ---
663+
stages: dict[str, TestStage] = {}
664+
total_passed = total_runs = 0
665+
666+
for stage_name, testpoints in stage_to_tps.items():
667+
stage_passed = stage_total = 0
668+
for tp in testpoints.values():
669+
stage_passed += tp.passed
670+
stage_total += tp.total
671+
672+
stages[stage_name] = TestStage(
673+
testpoints=testpoints,
674+
passed=stage_passed,
675+
total=stage_total,
676+
percent=100.0 * stage_passed / stage_total if stage_total else 0.0,
677+
)
764678

765-
# unmapped tests that are not part of the test plan?
766-
# Why are they not part of a test plan?
767-
if results["results"]["unmapped_tests"]:
768-
stages["unmapped"] = {
769-
"testpoints": {
770-
"None": {
771-
"tests": {
772-
test["name"]: {
773-
"max_time": test["max_runtime_s"],
774-
"sim_time": test["simulated_time_us"],
775-
"passed": test["passing_runs"],
776-
"total": test["total_runs"],
777-
"percent": 100 * test["passing_runs"] / test["total_runs"],
778-
}
779-
for test in results["results"]["unmapped_tests"]
780-
},
781-
},
782-
},
783-
}
679+
total_passed += stage_passed
680+
total_runs += stage_total
784681

785-
# Gather stats
786-
f_total = 0
787-
f_passed = 0
788-
for stage in stages: # noqa: PLC0206
789-
s_total = 0
790-
s_passed = 0
791-
792-
for testpoint in stages[stage]["testpoints"]:
793-
tp_total = 0
794-
tp_passed = 0
795-
tp_data = stages[stage]["testpoints"][testpoint]
796-
797-
for test in tp_data["tests"].values():
798-
tp_total += test["total"]
799-
tp_passed += test["passed"]
800-
801-
s_total += tp_total
802-
s_passed += tp_passed
803-
tp_data["total"] = tp_total
804-
tp_data["passed"] = tp_passed
805-
tp_data["percent"] = 100 * tp_passed / tp_total
806-
807-
f_total += s_total
808-
f_passed += s_passed
809-
stages[stage]["total"] = s_total
810-
stages[stage]["passed"] = s_passed
811-
stages[stage]["percent"] = 100 * s_passed / s_total
682+
# --- Coverage ---
683+
coverage: dict[str, float | None] = {}
684+
if self.cov_report_deploy:
685+
for k, v in self.cov_report_deploy.cov_results_dict.items():
686+
try:
687+
coverage[k.lower()] = float(v.rstrip("% "))
688+
except (ValueError, TypeError, AttributeError):
689+
coverage[k.lower()] = None
812690

691+
# --- Final result ---
813692
return FlowResults(
814-
block=IPMeta(
815-
name=results["block_name"],
816-
variant=results["block_variant"],
817-
commit=results["git_revision"],
818-
branch=results["git_branch_name"],
819-
url=f"https://github.com/lowrisc/opentitan/tree/{results['git_revision']}",
820-
),
821-
tool=ToolMeta(
822-
name=results["tool"],
823-
version="???",
824-
),
825-
timestamp=results["report_timestamp"],
693+
block=block,
694+
tool=tool,
695+
timestamp=timestamp,
826696
stages=stages,
827-
passed=f_passed,
828-
total=f_total,
829-
percent=100 * f_passed / f_total,
830-
coverage=results["results"]["coverage"],
697+
coverage=coverage,
698+
passed=total_passed,
699+
total=total_runs,
700+
percent=100.0 * total_passed / total_runs if total_runs else 0.0,
831701
)

0 commit comments

Comments
 (0)