Skip to content

Commit 1604d40

Browse files
committed
refactor: add JobSpec common abstraction
The `Deploy` classes are coupled to the scheduler and also the flow objects in a way that is not clear. It's also not clear which of the public attributes are depended on externally and which are internal working out. The intention is to make clear what the interface is that the scheduler depends on and what the flow object depends on as a result of the job runs. Initially I tried to refactor the `Deploy` objects themselves to make the existing objects clear, however this failed due to too many moving parts and the level of coupling. This commit introduces a `JobSpec` pydantic data model that gives a fully typed and runtime validated model containing everything the scheduler needs to run the job. The scheduler no longer receives `Deploy` objects, but instead `JobSpec` objects created from `Deploy` objects. Which means `Deploy` objects become `JobSpec` factories. In addition, the obvious dependencies on `Deploy` objects within the flow objects have been replaced with an expanded `CompletedJobStatus` pydantic model. There may still be dependencies that exist that have been missed, but the main ones have been removed in the results processing path. This opens the way for writing tests for the scheduler, and potentially the launchers, against clear interfaces using data classes as inputs and outputs. There is room for improvement in the data classes, as some attributes can potentially be removed with some refactoring. Signed-off-by: James McCorrie <[email protected]>
1 parent fd18ca5 commit 1604d40

File tree

21 files changed

+712
-475
lines changed

21 files changed

+712
-475
lines changed

ruff-ci.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ ignore = [
9292
"S103",
9393
"S202",
9494
"S311",
95-
"S602",
9695
"S605",
9796
"SIM115",
9897
"SLF001",

src/dvsim/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def resolve_scratch_root(arg_scratch_root, proj_root):
8282
arg_scratch_root = os.path.realpath(arg_scratch_root)
8383

8484
try:
85-
os.makedirs(arg_scratch_root, exist_ok=True)
85+
Path(arg_scratch_root).mkdir(exist_ok=True, parents=True)
8686
except PermissionError as e:
8787
log.fatal(f"Failed to create scratch root {arg_scratch_root}:\n{e}.")
8888
sys.exit(1)
@@ -238,7 +238,7 @@ def copy_repo(src, dest) -> None:
238238
log.verbose("[copy_repo] [cmd]: \n%s", " ".join(cmd))
239239

240240
# Make sure the dest exists first.
241-
os.makedirs(dest, exist_ok=True)
241+
Path(dest).mkdir(exist_ok=True, parents=True)
242242
try:
243243
subprocess.run(cmd, check=True, capture_output=True)
244244
except subprocess.CalledProcessError as e:

src/dvsim/flow/base.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@
1616
import hjson
1717

1818
from dvsim.flow.hjson import set_target_attribute
19+
from dvsim.job.data import CompletedJobStatus
1920
from dvsim.launcher.factory import get_launcher_cls
2021
from dvsim.logging import log
21-
from dvsim.scheduler import CompletedJobStatus, Scheduler
22+
from dvsim.scheduler import Scheduler
2223
from dvsim.utils import (
2324
find_and_substitute_wildcards,
2425
md_results_to_html,
@@ -403,7 +404,7 @@ def create_deploy_objects(self) -> None:
403404
for item in self.cfgs:
404405
item._create_deploy_objects()
405406

406-
def deploy_objects(self) -> Mapping[str, CompletedJobStatus]:
407+
def deploy_objects(self) -> Sequence[CompletedJobStatus]:
407408
"""Public facing API for deploying all available objects.
408409
409410
Runs each job and returns a map from item to status.
@@ -430,13 +431,13 @@ def deploy_objects(self) -> Mapping[str, CompletedJobStatus]:
430431
)
431432

432433
return Scheduler(
433-
items=deploy,
434+
items=[d.get_job_spec() for d in deploy],
434435
launcher_cls=get_launcher_cls(),
435436
interactive=self.interactive,
436437
).run()
437438

438439
@abstractmethod
439-
def _gen_results(self, results: Mapping[str, CompletedJobStatus]) -> str:
440+
def _gen_results(self, results: Sequence[CompletedJobStatus]) -> str:
440441
"""Generate flow results.
441442
442443
The function is called after the flow has completed. It collates
@@ -446,18 +447,18 @@ def _gen_results(self, results: Mapping[str, CompletedJobStatus]) -> str:
446447
report, which is in markdown format.
447448
448449
Args:
449-
results: dictionary mapping deployed item names to completed job status.
450+
results: completed job status objects.
450451
451452
Returns:
452453
Results as a formatted string
453454
454455
"""
455456

456-
def gen_results(self, results: Mapping[str, CompletedJobStatus]) -> None:
457+
def gen_results(self, results: Sequence[CompletedJobStatus]) -> None:
457458
"""Generate flow results.
458459
459460
Args:
460-
results: dictionary mapping deployed item names to completed job status.
461+
results: completed job status objects.
461462
462463
"""
463464
for item in self.cfgs:
@@ -476,7 +477,7 @@ def gen_results(self, results: Mapping[str, CompletedJobStatus]) -> None:
476477
self.write_results(self.results_html_name, self.results_summary_md)
477478

478479
@abstractmethod
479-
def gen_results_summary(self) -> None:
480+
def gen_results_summary(self) -> str:
480481
"""Public facing API to generate summary results for each IP/cfg file."""
481482

482483
def write_results(self, html_filename: str, text_md: str, json_str: str | None = None) -> None:

src/dvsim/flow/formal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ def _gen_results(self, results):
228228
"results.hjson",
229229
)
230230
try:
231-
with open(result_data) as results_file:
231+
with Path(result_data).open() as results_file:
232232
self.result = hjson.load(results_file, use_decimal=True)
233233
except OSError as err:
234234
log.warning("%s", err)

src/dvsim/flow/one_shot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
"""Class describing a one-shot build configuration object."""
66

7-
import os
7+
import pathlib
88
from collections import OrderedDict
99

1010
from dvsim.flow.base import FlowCfg
@@ -135,7 +135,7 @@ def _create_dirs(self) -> None:
135135
"""Create initial set of directories."""
136136
for link in self.links:
137137
rm_path(self.links[link])
138-
os.makedirs(self.links[link])
138+
pathlib.Path(self.links[link]).mkdir(parents=True)
139139

140140
def _create_deploy_objects(self) -> None:
141141
"""Create deploy objects from build modes."""

src/dvsim/flow/sim.py

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,24 @@
66

77
import fnmatch
88
import json
9-
import os
109
import re
1110
import sys
1211
from collections import OrderedDict, defaultdict
13-
from collections.abc import Mapping
12+
from collections.abc import Sequence
1413
from datetime import datetime, timezone
1514
from pathlib import Path
1615
from typing import ClassVar
1716

1817
from tabulate import tabulate
1918

2019
from dvsim.flow.base import FlowCfg
20+
from dvsim.job.data import CompletedJobStatus, JobSpec
2121
from dvsim.job.deploy import (
2222
CompileSim,
2323
CovAnalyze,
2424
CovMerge,
2525
CovReport,
2626
CovUnr,
27-
Deploy,
2827
RunTest,
2928
)
3029
from dvsim.logging import log
@@ -423,7 +422,7 @@ def _create_dirs(self) -> None:
423422
"""Create initial set of directories."""
424423
for link in self.links:
425424
rm_path(self.links[link])
426-
os.makedirs(self.links[link])
425+
Path(self.links[link]).mkdir(parents=True)
427426

428427
def _expand_run_list(self, build_map):
429428
"""Generate a list of tests to be run.
@@ -540,8 +539,10 @@ def _create_deploy_objects(self) -> None:
540539
self._create_dirs()
541540

542541
def _cov_analyze(self) -> None:
543-
"""Use the last regression coverage data to open up the GUI tool to
544-
analyze the coverage.
542+
"""Open GUI tool for coverage analysis.
543+
544+
Use the last regression coverage data to open up the GUI tool to analyze
545+
the coverage.
545546
"""
546547
# Create initial set of directories, such as dispatched, passed etc.
547548
self._create_dirs()
@@ -555,8 +556,10 @@ def cov_analyze(self) -> None:
555556
item._cov_analyze()
556557

557558
def _cov_unr(self) -> None:
558-
"""Use the last regression coverage data to generate unreachable
559-
coverage exclusions.
559+
"""Generate unreachable coverage exclusions.
560+
561+
Use the last regression coverage data to generate unreachable coverage
562+
exclusions.
560563
"""
561564
# TODO, Only support VCS
562565
if self.tool not in ["vcs", "xcelium"]:
@@ -573,7 +576,7 @@ def cov_unr(self) -> None:
573576
for item in self.cfgs:
574577
item._cov_unr()
575578

576-
def _gen_json_results(self, run_results: Mapping[Deploy, str]) -> str:
579+
def _gen_json_results(self, run_results: Sequence[CompletedJobStatus]) -> str:
577580
"""Return the run results as json-formatted dictionary."""
578581

579582
def _pct_str_to_float(s: str) -> float | None:
@@ -589,14 +592,8 @@ def _pct_str_to_float(s: str) -> float | None:
589592

590593
def _test_result_to_dict(tr) -> dict:
591594
"""Map a test result entry to a dict."""
592-
job_time_s = (
593-
tr.job_runtime.with_unit("s").get()[0] if tr.job_runtime is not None else None
594-
)
595-
sim_time_us = (
596-
tr.simulated_time.with_unit("us").get()[0]
597-
if tr.simulated_time is not None
598-
else None
599-
)
595+
job_time_s = tr.job_runtime
596+
sim_time_us = tr.simulated_time
600597
pass_rate = tr.passing * 100.0 / tr.total if tr.total > 0 else 0
601598
return {
602599
"name": tr.name,
@@ -644,7 +641,7 @@ def _test_result_to_dict(tr) -> dict:
644641

645642
# If the testplan does not yet have test results mapped to testpoints,
646643
# map them now.
647-
sim_results = SimResults(self.deploy, run_results)
644+
sim_results = SimResults(results=run_results)
648645
if not self.testplan.test_results_mapped:
649646
self.testplan.map_test_results(test_results=sim_results.table)
650647

@@ -707,12 +704,14 @@ def _test_result_to_dict(tr) -> dict:
707704
# Extract failure buckets.
708705
if sim_results.buckets:
709706
by_tests = sorted(sim_results.buckets.items(), key=lambda i: len(i[1]), reverse=True)
707+
710708
for bucket, tests in by_tests:
711709
unique_tests = defaultdict(list)
712710
for test, line, context in tests:
713-
if not isinstance(test, RunTest):
711+
if test.job_type != "RunTest":
714712
continue
715713
unique_tests[test.name].append((test, line, context))
714+
716715
fts = []
717716
for test_name, test_runs in unique_tests.items():
718717
frs = []
@@ -721,12 +720,13 @@ def _test_result_to_dict(tr) -> dict:
721720
{
722721
"seed": str(test.seed),
723722
"failure_message": {
724-
"log_file_path": test.get_log_path(),
723+
"log_file_path": str(test.log_path),
725724
"log_file_line_num": line,
726725
"text": "".join(context),
727726
},
728727
},
729728
)
729+
730730
fts.append(
731731
{
732732
"name": test_name,
@@ -747,7 +747,7 @@ def _test_result_to_dict(tr) -> dict:
747747
# Return the `results` dictionary as json string.
748748
return json.dumps(self.results_dict)
749749

750-
def _gen_results(self, results: Mapping[Deploy, str]) -> str:
750+
def _gen_results(self, results: Sequence[CompletedJobStatus]) -> str:
751751
"""Generate simulation results.
752752
753753
The function is called after the regression has completed. It collates the
@@ -761,12 +761,12 @@ def _gen_results(self, results: Mapping[Deploy, str]) -> str:
761761
def indent_by(level: int) -> str:
762762
return " " * (4 * level)
763763

764-
def create_failure_message(test, line, context):
764+
def create_failure_message(test: JobSpec, line, context):
765765
message = [f"{indent_by(2)}* {test.qual_name}\\"]
766766
if line:
767-
message.append(f"{indent_by(2)} Line {line}, in log " + test.get_log_path())
767+
message.append(f"{indent_by(2)} Line {line}, in log {test.log_path}")
768768
else:
769-
message.append(f"{indent_by(2)} Log {test.get_log_path()}")
769+
message.append(f"{indent_by(2)} Log {test.log_path}")
770770
if context:
771771
message.append("")
772772
lines = [f"{indent_by(4)}{c.rstrip()}" for c in context]
@@ -817,8 +817,7 @@ def create_bucket_report(buckets):
817817
fail_msgs.append("")
818818
return fail_msgs
819819

820-
deployed_items = self.deploy
821-
sim_results = SimResults(deployed_items, results)
820+
sim_results = SimResults(results=results)
822821

823822
# Generate results table for runs.
824823
results_str = "## " + self.results_title + "\n"
@@ -887,8 +886,17 @@ def create_bucket_report(buckets):
887886

888887
# Append coverage results if coverage was enabled.
889888
if self.cov_report_deploy is not None:
890-
report_status = results[self.cov_report_deploy.full_name]
891-
if report_status == "P":
889+
report_status: CompletedJobStatus | None = None
890+
for job_status in results:
891+
if job_status.full_name == self.cov_report_deploy.full_name:
892+
report_status = job_status
893+
break
894+
895+
if report_status is None:
896+
msg = f"Coverage report not found for {self.cov_report_deploy.full_name}"
897+
raise KeyError(msg)
898+
899+
if report_status.status == "P":
892900
results_str += "\n## Coverage Results\n"
893901
# Link the dashboard page using "cov_report_page" value.
894902
if hasattr(self, "cov_report_page"):
@@ -907,7 +915,7 @@ def create_bucket_report(buckets):
907915
self.results_md = results_str
908916
return results_str
909917

910-
def gen_results_summary(self):
918+
def gen_results_summary(self) -> str:
911919
"""Generate the summary results table.
912920
913921
This method is specific to the primary cfg. It summarizes the results

0 commit comments

Comments
 (0)