From b738b5a41af5db141301b4018b7e62cf2ede2bc7 Mon Sep 17 00:00:00 2001 From: James McCorrie Date: Tue, 11 Nov 2025 17:06:14 +0000 Subject: [PATCH 1/2] feat: add ResultsSummary model At the moment we have a JSON file for each of the flow config objects, alongside the HTML reports. The results summary page which ties them all together is HTML only. There is no way of getting the information programmatically, without doing something like parsing the HTML... which is error prone and messy. The ResultsSummary is a Pydantic model which can then be dumped to JSON. Signed-off-by: James McCorrie --- src/dvsim/report/__init__.py | 5 +++ src/dvsim/report/data.py | 62 ++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 src/dvsim/report/__init__.py create mode 100644 src/dvsim/report/data.py diff --git a/src/dvsim/report/__init__.py b/src/dvsim/report/__init__.py new file mode 100644 index 0000000..24bb095 --- /dev/null +++ b/src/dvsim/report/__init__.py @@ -0,0 +1,5 @@ +# Copyright lowRISC contributors (OpenTitan project). +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +"""Job result reporting.""" diff --git a/src/dvsim/report/data.py b/src/dvsim/report/data.py new file mode 100644 index 0000000..8484274 --- /dev/null +++ b/src/dvsim/report/data.py @@ -0,0 +1,62 @@ +# Copyright lowRISC contributors (OpenTitan project). +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +"""Report data models.""" + +from collections.abc import Mapping +from datetime import datetime +from pathlib import Path + +from pydantic import BaseModel, ConfigDict + +__all__ = ( + "IPMeta", + "ResultsSummary", +) + + +class IPMeta(BaseModel): + """Meta data for an IP block.""" + + model_config = ConfigDict(frozen=True, extra="forbid") + + name: str + variant: str | None = None + commit: str + branch: str + url: str + + +class ResultsSummary(BaseModel): + """Summary of results.""" + + model_config = ConfigDict(frozen=True, extra="forbid") + + top: IPMeta + """Meta data for the top level config.""" + + timestamp: datetime + """Run time stamp.""" + + report_index: Mapping[str, Path] + """Index of the IP block reports.""" + + report_path: Path + """Path to the report JSON file.""" + + @staticmethod + def load(path: Path) -> "ResultsSummary": + """Load results from JSON file. + + Transform the fields of the loaded JSON into a more useful schema for + report generation. + + Args: + path: to the json file to load. + + Returns: + The loaded ResultsSummary from JSON. + + """ + return ResultsSummary.model_validate_json(path.read_text()) From f7c6ccbac8db72ce478a8f6fd63a7aa14235911a Mon Sep 17 00:00:00 2001 From: James McCorrie Date: Fri, 14 Nov 2025 09:56:00 +0000 Subject: [PATCH 2/2] feat: add JSON summary generation Signed-off-by: James McCorrie --- src/dvsim/flow/base.py | 49 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/dvsim/flow/base.py b/src/dvsim/flow/base.py index 098738b..3acad60 100644 --- a/src/dvsim/flow/base.py +++ b/src/dvsim/flow/base.py @@ -7,9 +7,11 @@ import json import os import pprint +import re import sys from abc import ABC, abstractmethod from collections.abc import Mapping, Sequence +from datetime import datetime, timezone from pathlib import Path from typing import TYPE_CHECKING, ClassVar @@ -19,6 +21,7 @@ from dvsim.job.data import CompletedJobStatus from dvsim.launcher.factory import get_launcher_cls from dvsim.logging import log +from dvsim.report.data import IPMeta, ResultsSummary from dvsim.scheduler import Scheduler from dvsim.utils import ( find_and_substitute_wildcards, @@ -493,8 +496,52 @@ def gen_results(self, results: Sequence[CompletedJobStatus]) -> None: self.errors_seen |= item.errors_seen if self.is_primary_cfg: + json_str = self._gen_json_results_summary() self.gen_results_summary() - self.write_results(self.results_html_name, self.results_summary_md) + + self.write_results( + html_filename=self.results_html_name, + text_md=self.results_summary_md, + json_str=json_str, + ) + + def _gen_json_results_summary(self) -> str: + """Generate results summary in JSON format.""" + # The timestamp for this run has been taken with `utcnow()` and is + # stored in a custom format. Store it in standard ISO format with + # explicit timezone annotation. + timestamp = ( + datetime.strptime(self.timestamp, "%Y%m%d_%H%M%S") + .replace(tzinfo=timezone.utc) + .isoformat() + ) + + # Extract Git properties. + m = re.search( + r"https://github.com/.+?/tree/([0-9a-fA-F]+)", + self.revision, + ) + commit = m.group(1) if m else None + + reports_dir = Path(self.scratch_base_path) / "reports" + + return ResultsSummary( + top=IPMeta( + name=self.name, + variant=self.variant, + commit=str(commit), + branch=self.branch, + url=self.revision, + ), + timestamp=timestamp, + report_index={ + item.name: (item.results_dir / item.results_html_name).relative_to(reports_dir) + for item in self.cfgs + }, + report_path=(Path(self.results_dir) / self.results_html_name) + .with_suffix(".json") + .relative_to(reports_dir), + ).model_dump_json() @abstractmethod def gen_results_summary(self) -> str: