Skip to content

Commit 546a58c

Browse files
authored
Clarify testcase vs test chunk (#1112)
Two distinct concepts were both being called testcases. This clarifies the terminology and lays out a clear hierarchy: ``` test suite └── test file └── test chunk └── testcase ```
1 parent 14fcd7e commit 546a58c

65 files changed

Lines changed: 500 additions & 476 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/DeveloperGuide.md

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Each is described below.
88
## Table of Contents
99

1010
- [Certification Test Plan](#certification-test-plan)
11+
- [Test Hierarchy](#test-hierarchy)
1112
- [Table-Driven Unprivileged Coverpoints and Tests](#table-driven-unprivileged-coverpoints-and-tests)
1213
- [Creating New CSV Testplans](#creating-new-csv-testplans)
1314
- [Adding New Coverpoints](#adding-new-coverpoints)
@@ -59,6 +60,25 @@ The `generate_param_table.py` script turns these into .adoc files in ctp/src/par
5960

6061
This script is also run automatically when making the CTP. Hence, all the developer must do is create YAML files in `coverpoints/param` for test suites with parameters.
6162

63+
## Test Hierarchy
64+
65+
The testgen package organizes generated tests into four levels:
66+
67+
```
68+
test suite
69+
└── test file
70+
└── test chunk
71+
└── testcase
72+
```
73+
74+
- **Testcase**: The smallest unit of testing. Each testcase checks a single bin of a coverpoint. In the generated assembly, a testcase corresponds to one call to `test_data.add_testcase()`, which creates a label and debug string for that specific bin. For example, testing that `add` writes to `x5` is one testcase of the `cp_rd` coverpoint.
75+
76+
- **Test chunk** (`TestChunk`): An unsplittable group of one or more testcases. A test chunk is the building block of test files. Test chunks are never split across multiple files. Standard coverpoint generators (e.g., `cp_rd`, `cp_imm_edges`) produce one chunk per testcase via `format_single_testcase()`. Special coverpoint generators and privileged tests bundle multiple testcases into a single chunk using `test_data.begin_test_chunk()` / `test_data.end_test_chunk()`, typically because the testcases share setup code.
77+
78+
- **Test file**: A complete `.S` assembly file that is compiled into a self-checking ELF. Each test file contains one or more test chunks. When an instruction has many testcases (e.g., hundreds of register/immediate combinations), the framework splits the chunks across multiple test files using `TESTCASES_PER_FILE` as the limit. Test files are named like `I-add-00.S`, `I-add-01.S`, etc., where the suffix indicates the file index.
79+
80+
- **Test suite**: All test files in a given directory. Each test suite corresponds to one extension or combination of extensions (e.g., `I`, `Zcb`, `ZcbZbb`, `ExceptionsSm`) and maps to a single coverage file. Unprivileged test suites contain one or more test files per instruction. For privileged tests, a test suite typically contains a single test file covering all coverpoints for that feature.
81+
6282
## Table-Driven Unprivileged Coverpoints and Tests
6383

6484
Unprivileged tests are tests that exercise individual instructions and do not trap.
@@ -137,14 +157,14 @@ The following applies to all coverpoint test generators:
137157
- All coverpoint generator functions must use the following signature:
138158

139159
```py
140-
def make_cp_name(instr_name: str, instr_type: str, coverpoint: str, test_data: TestData) -> list[TestCase]:
160+
def make_cp_name(instr_name: str, instr_type: str, coverpoint: str, test_data: TestData) -> list[TestChunk]:
141161
```
142162

143163
- `instr_name` is the instruction currently being tested. This allows coverpoint test generators to be reused for multiple instructions.
144164
- `instr_type` is the type of the instruction currently being tested. This allows the correct instruction formatter (see below) to be selected.
145165
- `coverpoint` is the full name of the coverpoint, including any variant suffix. Coverpoint test generators can match multiple variants of a coverpoint. This argument allows different values, registers, etc. to be selected based on the variant.
146-
- `test_data` is the generation context that is passed to all parts of the test generation process and manages register allocation, test counting, and the active `TestCase`.
147-
- The generator must return a list of `TestCase` objects. Each `TestCase` holds its own assembly code, data values, debug strings, and signature update count. The framework uses these to split tests across files and combine them into the final output.
166+
- `test_data` is the generation context that is passed to all parts of the test generation process and manages register allocation, test counting, and the active `TestChunk`.
167+
- The generator must return a list of `TestChunk` objects. Each `TestChunk` is an unsplittable group of one or more testcases. It holds its own assembly code, data values, debug strings, and signature update count. The framework uses these to split test chunks across test files and combine their data for the final output.
148168

149169
Coverpoint test generators can largely be broken into two categories: standard and special.
150170
Standard generators use the [instruction formatters](#python-instruction-formatters) and can be applied to a wide range
@@ -166,7 +186,7 @@ It is also included below with many additional comments added to explain how it
166186
# which coverpoints they apply to.
167187
@add_coverpoint_generator("cp_rd")
168188
# Coverpoint generators all use the standard signature described above.
169-
def make_rd(instr_name: str, instr_type: str, coverpoint: str, test_data: TestData) -> list[TestCase]:
189+
def make_rd(instr_name: str, instr_type: str, coverpoint: str, test_data: TestData) -> list[TestChunk]:
170190
"""Generate tests for destination register coverpoints."""
171191
# Determine which rd registers to test based on the coverpoint variant.
172192
# Multiple variants can match to the same generator. This is useful when
@@ -182,8 +202,8 @@ def make_rd(instr_name: str, instr_type: str, coverpoint: str, test_data: TestDa
182202
# to make debugging easy.
183203
raise ValueError(f"Unknown cp_rd coverpoint variant: {coverpoint} for {instr_name}")
184204

185-
# Initialize a list of TestCase objects to collect results
186-
test_cases: list[TestCase] = []
205+
# Initialize a list of TestChunk objects to collect results
206+
test_chunks: list[TestChunk] = []
187207

188208
# Generate tests
189209
# A common pattern is to use a loop to iterate over some value that is being tested
@@ -200,22 +220,22 @@ def make_rd(instr_name: str, instr_type: str, coverpoint: str, test_data: TestDa
200220
# will get random values.
201221
params = generate_random_params(test_data, instr_type, rd=rd)
202222
desc = f"{coverpoint} (Test destination rd = x{rd})"
203-
# format_single_test is the key part of standard coverpoint generators. It takes
204-
# the provided instruction parameters (created above) and produces a TestCase object
223+
# format_single_testcase is the key part of standard coverpoint generators. It takes
224+
# the provided instruction parameters (created above) and produces a TestChunk object
205225
# containing the assembly code and associated data. It also calls test_data.add_testcase
206226
# to add a label and debugging string.
207-
tc = format_single_test(instr_name, instr_type, test_data, params, desc, f"b{rd}", coverpoint)
208-
# If consume_registers returned setup code (register moves), prepend it to the TestCase
227+
tc = format_single_testcase(instr_name, instr_type, test_data, params, desc, f"b{rd}", coverpoint)
228+
# If consume_registers returned setup code (register moves), prepend it to the TestChunk
209229
if asm_setup:
210230
tc.code = asm_setup + "\n" + tc.code
211-
test_cases.append(tc)
231+
test_chunks.append(tc)
212232
# Once registers are no longer in use, they need to be marked as available again
213233
# so that the register allocator knows that they can be reused.
214234
return_test_regs(test_data, params)
215235

216-
# Return the list of TestCase objects. The framework will use these to split tests
217-
# across files (based on num_tests counts) and combine their data for the final output.
218-
return test_cases
236+
# Return the list of TestChunk objects. The framework will use these to split test chunks
237+
# across test files (based on num_testcases counts) and combine their data for the final output.
238+
return test_chunks
219239
```
220240

221241
Additional documentation for all of these functions (and many other helper functions) is
@@ -232,16 +252,16 @@ cases each individual instruction).
232252

233253
Special coverpoint generators vary widely, so it is impossible to provide a complete guide,
234254
but they usually follow the same initial flow as a standard coverpoint and then diverge
235-
where the call to `format_single_test` would be. Instead of calling `format_single_test`,
236-
special coverpoint generators use `test_data.begin_testcase()` and `test_data.end_testcase()`
237-
to wrap their inline assembly in a single `TestCase`. The typical pattern is:
255+
where the call to `format_single_testcase` would be. Instead of calling `format_single_testcase`,
256+
special coverpoint generators use `test_data.begin_test_chunk()` and `test_data.end_test_chunk()`
257+
to wrap their inline assembly in a single `TestChunk`. The typical pattern is:
238258

239259
```py
240-
tc = test_data.begin_testcase()
260+
tc = test_data.begin_test_chunk()
241261
test_lines: list[str] = []
242262
# ... build assembly lines, call test_data.add_testcase(), load_int_reg(), write_sigupd(), etc. ...
243263
tc.code = "\n".join(test_lines)
244-
return [test_data.end_testcase()]
264+
return [test_data.end_test_chunk()]
245265
```
246266

247267
While most of this code is handwritten, you are still encouraged to use helper Python

generators/testgen/src/testgen/asm/csr.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ def gen_csr_read_sigupd(check_reg: int, csr_name: str, test_data: TestData) -> s
2727
Returns:
2828
Assembly line for the CSR read SIGUPD
2929
"""
30-
assert test_data.testcase is not None, "No active testcase — call begin_testcase() first"
31-
test_data.testcase.sigupd_count += 1
30+
assert test_data.test_chunk is not None, "No active test chunk — call begin_test_chunk() first"
31+
test_data.test_chunk.sigupd_count += 1
3232
return (
3333
f"{INDENT}# Read {csr_name} into x{check_reg} and check against expected.\n"
3434
f"RVTEST_SIGUPD_CSR_READ({csr_name}, x{check_reg}, {test_data.current_testcase_label}, {test_data.current_testcase_label}_str)"
@@ -50,8 +50,8 @@ def gen_csr_write_sigupd(check_reg: int, csr_name: str, test_data: TestData) ->
5050
Returns:
5151
Assembly line for the CSR write SIGUPD
5252
"""
53-
assert test_data.testcase is not None, "No active testcase — call begin_testcase() first"
54-
test_data.testcase.sigupd_count += 1
53+
assert test_data.test_chunk is not None, "No active test chunk — call begin_test_chunk() first"
54+
test_data.test_chunk.sigupd_count += 1
5555
return (
5656
f"{INDENT}# Write x{check_reg} to {csr_name}, read back and check against expected.\n"
5757
f"RVTEST_SIGUPD_CSR_WRITE({csr_name}, x{check_reg}, {test_data.current_testcase_label}, {test_data.current_testcase_label}_str)"

generators/testgen/src/testgen/asm/helpers.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ def to_hex(value: int, bits: int) -> str:
5454

5555
def load_int_reg(name: str, reg: int, val: int, test_data: TestData) -> str:
5656
"""Generate assembly to load an integer register with a specific value."""
57-
assert test_data.testcase is not None, "No active testcase — call begin_testcase() first"
58-
test_data.testcase.data_values.append(val)
57+
assert test_data.test_chunk is not None, "No active test chunk — call begin_test_chunk() first"
58+
test_data.test_chunk.data_values.append(val)
5959
return f"{INDENT}RVTEST_TESTDATA_LOAD_INT(x{test_data.int_regs.data_reg}, x{reg}) # load {name}: x{reg} = {to_hex(val, test_data.xlen)}"
6060

6161

@@ -70,32 +70,32 @@ def load_float_reg(
7070
if fp_load_type is None:
7171
fp_load_type = test_data.fp_load_size
7272

73-
assert test_data.testcase is not None, "No active testcase — call begin_testcase() first"
74-
test_data.testcase.data_values.append(val)
73+
assert test_data.test_chunk is not None, "No active test chunk — call begin_test_chunk() first"
74+
test_data.test_chunk.data_values.append(val)
7575
return f"{INDENT}RVTEST_TESTDATA_LOAD_FLOAT_{fp_load_type.upper()}(x{test_data.int_regs.data_reg}, f{reg}) # load {name}: f{reg} = {to_hex(val, test_data.flen)}"
7676

7777

7878
def write_sigupd(check_reg: int, test_data: TestData, sig_type: Literal["int", "float"] = "int") -> str:
7979
"""
8080
Generate assembly for SIGUPD and increment sigupd_count.
8181
"""
82-
assert test_data.testcase is not None, "No active testcase — call begin_testcase() first"
82+
assert test_data.test_chunk is not None, "No active test chunk — call begin_test_chunk() first"
8383
sig_reg = test_data.int_regs.sig_reg
8484
link_reg = test_data.int_regs.link_reg
8585
temp_reg = test_data.int_regs.temp_reg
8686
fp_temp_reg = test_data.float_regs.temp_reg
8787
if sig_type == "int":
88-
test_data.testcase.sigupd_count += 1
88+
test_data.test_chunk.sigupd_count += 1
8989
return (
9090
f"{INDENT}# Check if x{check_reg} contains the expected result. x{sig_reg} is the signature ptr, "
9191
f"x{link_reg} is the link ptr, x{temp_reg} is a temp reg.\n"
9292
f"{INDENT}RVTEST_SIGUPD(x{sig_reg}, x{link_reg}, x{temp_reg}, x{check_reg}, {test_data.current_testcase_label}, {test_data.current_testcase_label}_str)"
9393
)
9494
elif sig_type == "float":
9595
if test_data.flen > test_data.xlen:
96-
test_data.testcase.sigupd_count += 3
96+
test_data.test_chunk.sigupd_count += 3
9797
else:
98-
test_data.testcase.sigupd_count += 2
98+
test_data.test_chunk.sigupd_count += 2
9999
return (
100100
f"{INDENT}# Check if f{check_reg} contains the expected result. Also checks fflags. "
101101
f"x{sig_reg} is the signature ptr, x{link_reg} is the link ptr, x{temp_reg} "

generators/testgen/src/testgen/constants.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ def indent_asm(line: str) -> str:
2626
# Test Generation Configuration
2727
# =============================================================================
2828

29-
# Max testcases per file before splitting. Individual coverpoints won't be split,
30-
# so if one coverpoint exceeds this, the file will exceed this limit.
31-
# TODO: Currently only applies to unpriv tests. Should this apply to priv tests too?
29+
# Max testcases per test file before splitting into multiple files. Individual test
30+
# chunks won't be split, so if one test chunk exceeds this, the file will exceed this limit.
3231
TESTCASES_PER_FILE = 1000
3332

3433
# =============================================================================

0 commit comments

Comments
 (0)