|
26 | 26 | from dvsim.logging import log |
27 | 27 | from dvsim.modes import BuildMode, Mode, RunMode, find_mode |
28 | 28 | 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 |
30 | 30 | from dvsim.sim_results import SimResults |
31 | 31 | from dvsim.test import Test |
32 | 32 | from dvsim.testplan import Testplan |
@@ -575,257 +575,127 @@ def cov_unr(self) -> None: |
575 | 575 | item._cov_unr() |
576 | 576 |
|
577 | 577 | def _gen_json_results(self, run_results: Sequence[CompletedJobStatus]) -> FlowResults: |
578 | | - """Return the run flow results.""" |
| 578 | + """Generate structured FlowResults from simulation run data. |
579 | 579 |
|
580 | | - def _pct_str_to_float(s: str) -> float | None: |
581 | | - """Extract percent or None. |
| 580 | + Args: |
| 581 | + run_results: completed job status. |
582 | 582 |
|
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. |
604 | 585 |
|
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 | + """ |
642 | 587 | sim_results = SimResults(results=run_results) |
643 | 588 | 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 |
645 | 612 |
|
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 |
647 | 622 | 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."}: |
651 | 624 | continue |
652 | 625 |
|
653 | | - # Extract test results for this testpoint. |
654 | | - tests = [] |
| 626 | + test_results: dict[str, TestResult] = {} |
655 | 627 | 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 |
663 | 630 |
|
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: |
667 | 633 | continue |
668 | 634 |
|
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()) |
677 | 638 |
|
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 | + ) |
695 | 645 |
|
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 | + ) |
741 | 661 |
|
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 | + ) |
764 | 678 |
|
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 |
784 | 681 |
|
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 |
812 | 690 |
|
| 691 | + # --- Final result --- |
813 | 692 | 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, |
826 | 696 | 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, |
831 | 701 | ) |
0 commit comments