Skip to content

Commit c8dd48b

Browse files
committed
refactor: make Job Status an Enum
Currently the job status is passed around DVSim as a string, where the expectation is that it is always a single upper case character in ('Q', 'D', 'P', 'F', 'K') - some limited parts of the code also handle an 'E' error case. Turn these into a small Enum type so that we have much stricter typing on these values and improved ergonomics when referring to different job statuses in code. This status Enum lives in its own module instead of with the other job data to avoid circular imports (as the job status' registered callbacks reference the launcher definition). Signed-off-by: Alex Jones <alex.jones@lowrisc.org>
1 parent fbe9914 commit c8dd48b

15 files changed

Lines changed: 169 additions & 119 deletions

File tree

src/dvsim/flow/formal.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from dvsim.flow.one_shot import OneShotCfg
1212
from dvsim.job.data import CompletedJobStatus
13+
from dvsim.job.status import JobStatus
1314
from dvsim.logging import log
1415
from dvsim.utils import subst_wildcards
1516

@@ -230,7 +231,7 @@ def _gen_results(self, results: Sequence[CompletedJobStatus]) -> None:
230231
assert len(self.deploy) == 1
231232
mode = self.deploy[0]
232233

233-
if complete_job.status == "P":
234+
if complete_job.status == JobStatus.PASSED:
234235
result_data = Path(
235236
subst_wildcards(self.build_dir, {"build_mode": mode.name}),
236237
"results.hjson",
@@ -262,7 +263,7 @@ def _gen_results(self, results: Sequence[CompletedJobStatus]) -> None:
262263
else:
263264
summary += ["N/A", "N/A", "N/A"]
264265

265-
if complete_job.status != "P":
266+
if complete_job.status != JobStatus.PASSED:
266267
results_str += "\n## List of Failures\n" + "".join(complete_job.fail_msg.message)
267268

268269
messages = self.result.get("messages")

src/dvsim/flow/one_shot.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from dvsim.flow.base import FlowCfg
1313
from dvsim.job.data import CompletedJobStatus
14+
from dvsim.job.status import JobStatus
1415
from dvsim.job.deploy import CompileOneShot
1516
from dvsim.logging import log
1617
from dvsim.modes import BuildMode, Mode
@@ -92,10 +93,10 @@ def _expand(self) -> None:
9293

9394
# Set directories with links for ease of debug / triage.
9495
self.links = {
95-
"D": self.scratch_path + "/" + "dispatched",
96-
"P": self.scratch_path + "/" + "passed",
97-
"F": self.scratch_path + "/" + "failed",
98-
"K": self.scratch_path + "/" + "killed",
96+
JobStatus.DISPATCHED: self.scratch_path + "/" + "dispatched",
97+
JobStatus.PASSED: self.scratch_path + "/" + "passed",
98+
JobStatus.FAILED: self.scratch_path + "/" + "failed",
99+
JobStatus.KILLED: self.scratch_path + "/" + "killed",
99100
}
100101

101102
# Use the default build mode for tests that do not specify it

src/dvsim/job/data.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from pydantic import BaseModel, ConfigDict
1616

17+
from dvsim.job.status import JobStatus
1718
from dvsim.launcher.base import ErrorMessage, Launcher
1819
from dvsim.report.data import IPMeta, ToolMeta
1920

@@ -97,13 +98,13 @@ class JobSpec(BaseModel):
9798
"""Output directory for the job results files."""
9899
log_path: Path
99100
"""Path for the job log file."""
100-
links: Mapping[str, Path]
101+
links: Mapping[JobStatus, Path]
101102
"""Path for links directories."""
102103

103104
# TODO: remove the need for these callables here
104105
pre_launch: Callable[[Launcher], None]
105106
"""Callback function for pre-launch actions."""
106-
post_finish: Callable[[str], None]
107+
post_finish: Callable[[JobStatus], None]
107108
"""Callback function for tidy up actions once the job is finished."""
108109

109110
pass_patterns: Sequence[str]
@@ -153,7 +154,7 @@ class CompletedJobStatus(BaseModel):
153154
simulated_time: float
154155
"""Simulation time."""
155156

156-
status: str
157-
"""Job status string [P,F,K,...]"""
157+
status: JobStatus
158+
"""Status of the job."""
158159
fail_msg: ErrorMessage
159160
"""Error message."""

src/dvsim/job/deploy.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from tabulate import tabulate
1515

1616
from dvsim.job.data import JobSpec, WorkspaceConfig
17+
from dvsim.job.status import JobStatus
1718
from dvsim.job.time import JobTime
1819
from dvsim.launcher.base import Launcher
1920
from dvsim.logging import log
@@ -347,10 +348,10 @@ def callback(launcher: Launcher) -> None:
347348

348349
return callback
349350

350-
def post_finish(self) -> Callable[[str], None]:
351+
def post_finish(self) -> Callable[[JobStatus], None]:
351352
"""Get post finish callback."""
352353

353-
def callback(status: str) -> None:
354+
def callback(status: JobStatus) -> None:
354355
"""Perform additional post-finish activities (callback).
355356
356357
This is invoked by launcher::_post_finish().
@@ -641,12 +642,12 @@ def callback(launcher: Launcher) -> None:
641642

642643
return callback
643644

644-
def post_finish(self) -> Callable[[str], None]:
645+
def post_finish(self) -> Callable[[JobStatus], None]:
645646
"""Get post finish callback."""
646647

647-
def callback(status: str) -> None:
648+
def callback(status: JobStatus) -> None:
648649
"""Perform tidy up tasks."""
649-
if status != "P":
650+
if status != JobStatus.PASSED:
650651
# Delete the coverage data if available.
651652
rm_path(self.cov_db_test_dir)
652653

@@ -812,16 +813,16 @@ def _set_attrs(self) -> None:
812813
self.cov_results = ""
813814
self.cov_results_dict = {}
814815

815-
def post_finish(self) -> Callable[[str], None]:
816+
def post_finish(self) -> Callable[[JobStatus], None]:
816817
"""Get post finish callback."""
817818

818-
def callback(status: str) -> None:
819+
def callback(status: JobStatus) -> None:
819820
"""Extract the coverage results summary for the dashboard.
820821
821822
If the extraction fails, an appropriate exception is raised, which must
822823
be caught by the caller to mark the job as a failure.
823824
"""
824-
if self.dry_run or status != "P":
825+
if self.dry_run or status != JobStatus.PASSED:
825826
return
826827

827828
plugin = get_sim_tool_plugin(tool=self.sim_cfg.tool)

src/dvsim/job/status.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Copyright lowRISC contributors (OpenTitan project).
2+
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
"""An enum definition for the various job statuses."""
6+
7+
from enum import Enum
8+
9+
__all__ = ("JobStatus",)
10+
11+
12+
class JobStatus(Enum):
13+
"""Status of a Job."""
14+
15+
ERROR = 0 # Internal error / failure to build
16+
QUEUED = 1
17+
DISPATCHED = 2
18+
PASSED = 3
19+
FAILED = 4 # A 'failed' result from a valid run.
20+
KILLED = 5
21+
22+
@property
23+
def shorthand(self) -> str:
24+
"""Shorthand for the job status, e.g. 'D' for 'Dispatched'."""
25+
return self.name[0]
26+
27+
@property
28+
def completed(self) -> bool:
29+
"""Whether this status corresponds to some (non-error) completed job."""
30+
return self in (JobStatus.PASSED, JobStatus.FAILED, JobStatus.KILLED)

src/dvsim/launcher/base.py

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from pydantic import BaseModel, ConfigDict
1717

18+
from dvsim.job.status import JobStatus
1819
from dvsim.job.time import JobTime
1920
from dvsim.logging import log
2021
from dvsim.tool.utils import get_sim_tool_plugin
@@ -210,7 +211,7 @@ def _make_odir(self) -> None:
210211

211212
Path(self.job_spec.odir).mkdir(exist_ok=True, parents=True)
212213

213-
def _link_odir(self, status: str) -> None:
214+
def _link_odir(self, status: JobStatus) -> None:
214215
"""Soft-links the job's directory based on job's status.
215216
216217
The dispatched, passed and failed directories in the scratch area
@@ -220,8 +221,8 @@ def _link_odir(self, status: str) -> None:
220221
mk_symlink(path=self.job_spec.odir, link=dest)
221222

222223
# Delete the symlink from dispatched directory if it exists.
223-
if status != "D":
224-
old = Path(self.job_spec.links["D"], self.job_spec.qual_name)
224+
if status != JobStatus.DISPATCHED:
225+
old = Path(self.job_spec.links[JobStatus.DISPATCHED], self.job_spec.qual_name)
225226
rm_path(old)
226227

227228
def _dump_env_vars(self, exports: Mapping[str, str]) -> None:
@@ -258,7 +259,7 @@ def launch(self) -> None:
258259
self._do_launch()
259260

260261
@abstractmethod
261-
def poll(self) -> str | None:
262+
def poll(self) -> JobStatus | None:
262263
"""Poll the launched job for completion.
263264
264265
Invokes _check_status() and _post_finish() when the job completes.
@@ -272,14 +273,14 @@ def poll(self) -> str | None:
272273
def kill(self) -> None:
273274
"""Terminate the job."""
274275

275-
def _check_status(self) -> tuple[str, ErrorMessage | None]:
276-
"""Determine the outcome of the job (P/F if it ran to completion).
276+
def _check_status(self) -> tuple[JobStatus, ErrorMessage | None]:
277+
"""Determine the outcome of the job (Passed/Failed if it ran to completion).
277278
278279
Returns:
279280
(status, err_msg) extracted from the log, where the status is
280-
"P" if the it passed, "F" otherwise. This is invoked by poll() just
281-
after the job finishes. err_msg is an instance of the named tuple
282-
ErrorMessage.
281+
Passed if the job passed, Failed otherwise. This is invoked by
282+
poll() just after the job finishes. err_msg is an instance of the
283+
named tuple ErrorMessage.
283284
284285
"""
285286

@@ -307,7 +308,7 @@ def _find_patterns(patterns: Sequence[str], line: str) -> Sequence[str] | None:
307308
return None
308309

309310
if self.job_spec.dry_run:
310-
return "P", None
311+
return JobStatus.PASSED, None
311312

312313
# Only one fail pattern needs to be seen.
313314
chk_failed = bool(self.job_spec.fail_patterns)
@@ -324,7 +325,7 @@ def _find_patterns(patterns: Sequence[str], line: str) -> Sequence[str] | None:
324325
) as f:
325326
lines = f.readlines()
326327
except OSError as e:
327-
return "F", ErrorMessage(
328+
return JobStatus.FAILED, ErrorMessage(
328329
line_number=None,
329330
message=f"Error opening file {self.job_spec.log_path}:\n{e}",
330331
context=[],
@@ -368,7 +369,7 @@ def _find_patterns(patterns: Sequence[str], line: str) -> Sequence[str] | None:
368369
# If failed, then nothing else to do. Just return.
369370
# Provide some extra lines for context.
370371
end = cnt + 5
371-
return "F", ErrorMessage(
372+
return JobStatus.FAILED, ErrorMessage(
372373
line_number=cnt + 1,
373374
message=line.strip(),
374375
context=lines[cnt:end],
@@ -384,32 +385,32 @@ def _find_patterns(patterns: Sequence[str], line: str) -> Sequence[str] | None:
384385
# exit code for whatever reason, then show the last 10 lines of the log
385386
# as the failure message, which might help with the debug.
386387
if self.exit_code != 0:
387-
return "F", ErrorMessage(
388+
return JobStatus.FAILED, ErrorMessage(
388389
line_number=None,
389390
message="Job returned non-zero exit code",
390391
context=lines[-10:],
391392
)
392393
if chk_passed:
393-
return "F", ErrorMessage(
394+
return JobStatus.FAILED, ErrorMessage(
394395
line_number=None,
395396
message=f"Some pass patterns missing: {pass_patterns}",
396397
context=lines[-10:],
397398
)
398-
return "P", None
399+
return JobStatus.PASSED, None
399400

400-
def _post_finish(self, status: str, err_msg: ErrorMessage) -> None:
401+
def _post_finish(self, status: JobStatus, err_msg: ErrorMessage) -> None:
401402
"""Do post-completion activities, such as preparing the results.
402403
403404
Must be invoked by poll(), after the job outcome is determined.
404405
405406
Args:
406-
status: status of the job, either 'P', 'F' or 'K'.
407+
status: status of the completed job (must be either Passed, Failed or Killed).
407408
err_msg: an instance of the named tuple ErrorMessage.
408409
409410
"""
410-
assert status in ["P", "F", "K"]
411+
assert status.completed
411412
self._link_odir(status)
412-
log.debug("Item %s has completed execution: %s", self, status)
413+
log.debug("Item %s has completed execution: %s", self, status.shorthand)
413414

414415
try:
415416
# Run the target-specific cleanup tasks regardless of the job's
@@ -419,16 +420,16 @@ def _post_finish(self, status: str, err_msg: ErrorMessage) -> None:
419420
except Exception as e:
420421
# If the job had already failed, then don't do anything. If it's
421422
# cleanup task failed, then mark the job as failed.
422-
if status == "P":
423-
status = "F"
423+
if status == JobStatus.PASSED:
424+
status = JobStatus.FAILED
424425
err_msg = ErrorMessage(
425426
line_number=None,
426427
message=f"{e}",
427428
context=[f"{e}"],
428429
)
429430

430431
self.status = status
431-
if self.status != "P":
432+
if self.status != JobStatus.PASSED:
432433
assert err_msg
433434
assert isinstance(err_msg, ErrorMessage)
434435
self.fail_msg = err_msg

src/dvsim/launcher/fake.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from random import choice
88
from typing import TYPE_CHECKING
99

10+
from dvsim.job.status import JobStatus
1011
from dvsim.launcher.base import ErrorMessage, Launcher
1112

1213
if TYPE_CHECKING:
@@ -16,12 +17,12 @@
1617
__all__ = ("FakeLauncher",)
1718

1819

19-
def _run_test_handler(job_spec: "JobSpec") -> str:
20+
def _run_test_handler(job_spec: "JobSpec") -> JobStatus:
2021
"""Handle a RunTest deploy job."""
21-
return choice(("P", "F"))
22+
return choice((JobStatus.PASSED, JobStatus.FAILED))
2223

2324

24-
def _cov_report_handler(job_spec: "JobSpec") -> str:
25+
def _cov_report_handler(job_spec: "JobSpec") -> JobStatus:
2526
"""Handle a CompileSim deploy job."""
2627
# TODO: this hack doesn't work any more and needs implementing by writing
2728
# a file that can be parsed as if it's been generated by the tool.
@@ -38,7 +39,7 @@ def _cov_report_handler(job_spec: "JobSpec") -> str:
3839
# ]
3940
# job_spec.cov_results_dict = {k: f"{random() * 100:.2f} %" for k in keys}
4041

41-
return "P"
42+
return JobStatus.PASSED
4243

4344

4445
_DEPLOY_HANDLER = {
@@ -56,19 +57,19 @@ class FakeLauncher(Launcher):
5657
def _do_launch(self) -> None:
5758
"""Do the launch."""
5859

59-
def poll(self) -> str | None:
60+
def poll(self) -> JobStatus | None:
6061
"""Check status of the running process."""
6162
deploy_cls = self.job_spec.job_type
6263
if deploy_cls in _DEPLOY_HANDLER:
6364
return _DEPLOY_HANDLER[deploy_cls](job_spec=self.job_spec)
6465

6566
# Default result is Pass
66-
return "P"
67+
return JobStatus.PASSED
6768

6869
def kill(self) -> None:
6970
"""Kill the running process."""
7071
self._post_finish(
71-
"K",
72+
JobStatus.KILLED,
7273
ErrorMessage(line_number=None, message="Job killed!", context=[]),
7374
)
7475

0 commit comments

Comments
 (0)