Skip to content

Commit 15cc201

Browse files
committed
refactor: Improve constructors for testplan elements
This doesn't change the behaviour (except that it might be slightly more careful about setting arbitrary field names). I've rather simplified the flow though. It now looks like this: - The derived class constructor can set some placeholder values (to help tooling see their expected types). - It then calls super().__init__ - The Element constructor checks it can find strings for the "name" and "desc" fields. - The "tags" field gets initialised to [] (so the element will have an empty list of tags if none are specified). - The Element constructor then sets all the requested fields, but only allows them to be strings or lists of strings. (No need to allow more complicated types: these are the only types we expect anyway). - It finally checks that "tags" is still a list of strings. - When we get back to the derived class constructor, we check that any other expected fields have been supplied, given the right type, and (for "stage") have a known value. - The Testpoint class also has some fields that it doesn't expect to load from the dictionary. So we check that none were supplied, then set them appropriately. Phew! Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
1 parent ea59667 commit 15cc201

1 file changed

Lines changed: 128 additions & 59 deletions

File tree

src/dvsim/testplan.py

Lines changed: 128 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -56,50 +56,90 @@ class Element:
5656
This is either a testpoint or a covergroup.
5757
"""
5858

59-
# Type of the testplan element. Must be set by the extended class.
60-
kind = "none"
59+
@staticmethod
60+
def get_str(src: dict[str, Any], field_name: str, elt_name: str | None) -> str:
61+
"""Get the named field from src, whose value should be a string.
62+
63+
If the field is missing or its value is not a string, raise a ValueError.
64+
65+
Args:
66+
src: The dictionary being read.
6167
62-
# Mandatory fields in a testplan element.
63-
fields = ["name", "desc"]
68+
field_name: The key whose value should be returned.
6469
65-
def __init__(self, raw_dict) -> None:
70+
elt_name: The name of the Element being parsed (if known).
71+
72+
"""
73+
raw = src.get(field_name)
74+
if raw is None:
75+
name_comment = f" with name {elt_name}" if elt_name is not None else ""
76+
msg = f"Testplan element{name_comment} does not have a {field_name} field."
77+
raise ValueError(msg)
78+
79+
if not isinstance(raw, str):
80+
name_comment = f" with name {elt_name}" if elt_name is not None else ""
81+
msg = (
82+
f"Testplan element{name_comment} has a {field_name} field but this is not a string."
83+
)
84+
raise TypeError(msg)
85+
86+
return raw
87+
88+
def __init__(self, raw_dict: dict[str, Any]) -> None:
6689
"""Initialize the testplan element.
6790
6891
raw_dict is the dictionary parsed from the Hjson file.
6992
"""
70-
# 'tags' is an optional field in addition to the mandatory self.fields.
71-
self.tags = []
93+
self.name = Element.get_str(raw_dict, "name", None)
94+
self.desc = Element.get_str(raw_dict, "desc", self.name)
7295

73-
for field in self.fields:
74-
try:
75-
setattr(self, field, raw_dict.pop(field))
76-
except KeyError as e:
77-
msg = (
78-
f"Error: {self.kind} does not contain all of "
79-
f"the required fields:\n{raw_dict}\nRequired:\n"
80-
f"{self.fields}\n{e}"
81-
)
82-
raise KeyError(
83-
msg,
84-
)
96+
# Check that the name field is not empty
97+
if not self.name:
98+
raise ValueError("Cannot have a testplan element with empty name.")
8599

86-
# Set the remaining k-v pairs in raw_dict as instance attributes.
87-
for k, v in raw_dict.items():
88-
setattr(self, k, v)
100+
# Set a default value for self.tags as an empty list. We'll check we
101+
# haven't changed type at the end.
102+
self.tags: list[str] = []
89103

90-
# Verify things are in order.
91-
self._validate()
104+
# Convert all the k/v pairs in raw_dict into instance attributes for
105+
# this object. These should all either be strings or lists of strings.
106+
for k, v in raw_dict.items():
107+
# We have extracted the element name and description already
108+
if k in ["name", "desc"]:
109+
continue
92110

93-
def _validate(self) -> None:
94-
"""Runs some basic consistency checks."""
95-
if not self.name:
96-
msg = f"Error: {self.kind.capitalize()} name cannot be empty:\n{self}"
97-
raise ValueError(msg)
111+
if isinstance(v, str):
112+
setattr(self, k, v)
113+
elif isinstance(v, list):
114+
# This is a list, but we should check slightly more: it should
115+
# be a list of strings.
116+
strings: list[str] = []
117+
for idx, item in enumerate(v):
118+
if isinstance(item, str):
119+
strings.append(item)
120+
else:
121+
msg = (
122+
f"Item {idx} of the {k} field in the testplan "
123+
f"element with name {self.name} is not a string."
124+
)
125+
raise TypeError(msg)
126+
setattr(self, k, strings)
127+
else:
128+
msg = (
129+
f"The {k} field in the testplan element with name "
130+
f"{self.name} is neither a string nor a list of strings."
131+
)
132+
raise TypeError(msg)
98133

99-
# "tags", if updated key must be list.
134+
# Check that self.tags is still a list (and wasn't overwritten with a
135+
# string when parsing the raw dict)
100136
if not isinstance(self.tags, list):
101-
msg = f"'tags' key in {self} is not a list."
102-
raise ValueError(msg)
137+
msg = (
138+
"The tags field in the testplan element with name "
139+
f"{self.name} is a string but should be a list of strings "
140+
"(if supplied)."
141+
)
142+
raise TypeError(msg)
103143

104144
def has_tags(self, tags: set[str]) -> bool:
105145
"""Return true if the element matches the provided tags.
@@ -133,60 +173,89 @@ class Covergroup(Element):
133173
include individual coverpoints and crosses in the description.
134174
"""
135175

136-
kind = "covergroup"
176+
def __init__(self, raw_dict: dict[str, Any]) -> None:
177+
"""Create a Covergroup based on the given parsed hjson dictionary."""
178+
super().__init__(raw_dict)
137179

138-
def _validate(self) -> None:
139-
super()._validate()
140180
if not self.name.endswith("_cg"):
141181
msg = f'Error: Covergroup name {self.name} needs to end with suffix "_cg".'
142-
raise ValueError(
143-
msg,
144-
)
182+
raise ValueError(msg)
145183

146184

147185
class Testpoint(Element):
148-
"""An testcase entry in the testplan.
186+
"""A testcase entry in the testplan.
149187
150-
A testpoint maps to a unique design feature that is planned to be verified.
188+
A testpoint maps to a unique design feature that should be verified.
151189
It captures following information:
152190
- name of the planned test
153191
- a brief description indicating intent, stimulus and checking procedure
154192
- the targeted stage
155193
- the list of actual developed tests that verify it
156194
"""
157195

158-
kind = "testpoint"
159-
fields = [*Element.fields, "stage", "tests"]
160-
161196
# Verification stages.
162197
stages = ("N.A.", "V1", "V2", "V2S", "V3")
163198

164-
def __init__(self, raw_dict) -> None:
199+
def __init__(self, raw_dict: dict[str, Any]) -> None:
200+
"""Construct a Testpoint from the given dictionary."""
201+
# These will get overridden from the dictionary, but allow the tooling
202+
# to see that the fields exist and know their expected types.
203+
self.tests: list[str] = []
204+
self.stage: str = ""
205+
165206
super().__init__(raw_dict)
166207

208+
# The dictionary should have specified a list of tests (possibly empty)
209+
# for the testpoint.
210+
if "tests" not in raw_dict:
211+
msg = f"The testpoint named {self.name} has no list of tests."
212+
raise ValueError(msg)
213+
if not isinstance(self.tests, list):
214+
msg = (
215+
f"The testpoint named {self.name} should have a list of "
216+
"tests, not a single test name."
217+
)
218+
raise TypeError(msg)
219+
220+
# The dictionary should have specified a stage for the testpoint, which
221+
# should have been a string.
222+
if "stage" not in raw_dict:
223+
msg = f"The testpoint named {self.name} has no stage."
224+
raise ValueError(msg)
225+
if not isinstance(self.stage, str):
226+
msg = f"The stage of the testpoint named {self.name} should be a string, not a list."
227+
raise TypeError(msg)
228+
if self.stage not in Testpoint.stages:
229+
msg = (
230+
f"The stage of the testpoint named {self.name} is "
231+
f"{self.stage} but this is not a known testpoint stage. "
232+
f"Legal values: {Testpoint.stages}."
233+
)
234+
raise ValueError(msg)
235+
236+
# There are some special fields that we use for a Testpoint object.
237+
# Because the Element constructor allows the hjson file to set
238+
# arbitrary fields, we need to make sure that they haven't already been
239+
# set.
240+
self._check_field_unset("test_results")
241+
self._check_field_unset("not_mapped")
242+
167243
# List of Result objects indicating test results mapped to this
168244
# testpoint.
169-
self.test_results = []
245+
self.test_results: list[Result] = []
170246

171247
# If tests key is set to ["N/A"], then don't map this testpoint to the
172248
# simulation results.
173-
self.not_mapped = False
174-
if self.tests == ["N/A"]:
175-
self.not_mapped = True
249+
self.not_mapped: bool = self.tests == ["N/A"]
176250

177-
def _validate(self) -> None:
178-
super()._validate()
179-
if self.stage not in Testpoint.stages:
251+
def _check_field_unset(self, field_name: str) -> None:
252+
"""Check that the field with the given name has not been set in the class."""
253+
if hasattr(self, field_name):
180254
msg = (
181-
f"Testpoint stage {self.stage} is invalid:\n{self}\nLegal values: Testpoint.stages"
182-
)
183-
raise ValueError(
184-
msg,
255+
f"The dictionary defining the testpoint named {self.name} "
256+
f"defined a field called {field_name}, but this name is "
257+
"reserved for the tooling."
185258
)
186-
187-
# "tests" key must be list.
188-
if not isinstance(self.tests, list):
189-
msg = f"'tests' key in {self} is not a list."
190259
raise ValueError(msg)
191260

192261
@staticmethod

0 commit comments

Comments
 (0)