Skip to content

Commit 93fc0fc

Browse files
[CI] Added Test Suite Verification to CI
Found that we were regressing on many features in VTR due to tasks being added to the appropriate test suite directory, but not being included in the necessary task list. As such, it appeared as though the tests were being run, but in reality they were not. Added a script which will be run by the CI which will verify that all of the test suites that we care about have all their tasks in the appropriate task list. From this tool, found many tasks which were not in the task lists. Marked these tasks as "ignored" for now. These should be handled in a separate PR.
1 parent c8f1161 commit 93fc0fc

File tree

3 files changed

+345
-0
lines changed

3 files changed

+345
-0
lines changed

.github/workflows/test.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,25 @@ jobs:
9999
run: ./dev/${{ matrix.script }}
100100

101101

102+
VerifyTestSuites:
103+
runs-on: ubuntu-24.04
104+
name: 'Verify Test Suites'
105+
steps:
106+
107+
- uses: actions/setup-python@v5
108+
with:
109+
python-version: 3.12.3
110+
111+
- uses: actions/checkout@v4
112+
# NOTE: We do not need sub-modules. This only verifies the tests, does not run them.
113+
114+
- name: 'Run test suite verification'
115+
run: |
116+
./dev/vtr_test_suite_verifier/verify_test_suites.py \
117+
-vtr_regression_tests_dir vtr_flow/tasks/regression_tests \
118+
-test_suite_info dev/vtr_test_suite_verifier/test_suites_info.json
119+
120+
102121
UnitTests:
103122
name: 'U: C++ Unit Tests'
104123
runs-on: ubuntu-24.04
@@ -540,6 +559,7 @@ jobs:
540559
needs:
541560
- Build
542561
- Format
562+
- VerifyTestSuites
543563
- UnitTests
544564
- BuildVariations
545565
- Regression
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{"test_suites": [
2+
{
3+
"name": "vtr_reg_basic",
4+
"ignored_tasks": []
5+
},
6+
{
7+
"name": "vtr_reg_basic_odin",
8+
"ignored_tasks": []
9+
},
10+
{
11+
"name": "parmys_reg_basic",
12+
"ignored_tasks": []
13+
},
14+
{
15+
"name": "vtr_reg_valgrind_small",
16+
"ignored_tasks": []
17+
},
18+
{
19+
"name": "vtr_reg_strong",
20+
"ignored_tasks": [
21+
"strong_ap/gen_mass_report",
22+
"strong_cluster_seed_type",
23+
"strong_router_heap",
24+
"strong_verify_rr_graph_3d",
25+
"strong_xilinx_support"
26+
]
27+
},
28+
{
29+
"name": "vtr_reg_strong_odin",
30+
"ignored_tasks": [
31+
"strong_pack_modes",
32+
"strong_xilinx_support",
33+
"strong_router_heap",
34+
"strong_cluster_seed_type"
35+
]
36+
},
37+
{
38+
"name": "vtr_reg_nightly_test1",
39+
"ignored_tasks": [
40+
"arithmetic_tasks/FIR_filters",
41+
"arithmetic_tasks/FIR_filters_frac",
42+
"arithmetic_tasks/adder_trees",
43+
"symbiflow"
44+
]
45+
},
46+
{
47+
"name": "vtr_reg_nightly_test2",
48+
"ignored_tasks": [
49+
"complex_switch",
50+
"vpr_verify_custom_sb_diff_chan_width",
51+
"vtr_xilinx_qor"
52+
]
53+
},
54+
{
55+
"name": "vtr_reg_nightly_test3",
56+
"ignored_tasks": [
57+
"vtr_reg_qor_chain_large"
58+
]
59+
},
60+
{
61+
"name": "vtr_reg_nightly_test4",
62+
"ignored_tasks": []
63+
},
64+
{
65+
"name": "vtr_reg_nightly_test5",
66+
"ignored_tasks": [
67+
"vpr_noc_mlp_odin_ii",
68+
"vpr_3d_noc_star_topology",
69+
"vpr_3d_noc_nearest_neighbor_topology",
70+
"vpr_3d_noc_clique_topology"
71+
]
72+
},
73+
{
74+
"name": "vtr_reg_nightly_test6",
75+
"ignored_tasks": []
76+
},
77+
{
78+
"name": "vtr_reg_nightly_test7",
79+
"ignored_tasks": [
80+
"vtr_reg_qor_large_depop_run_flat",
81+
"vtr_reg_qor_large_run_flat",
82+
"verify_router_lookahead_run_flat"
83+
]
84+
}
85+
]}
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Module to verify VTR test suites run by the GitHub CI.
4+
5+
Test suites in VTR are verified by ensuring that all tasks in the test suite
6+
appear in the task list and that there are no tasks in the task list which
7+
are not in the test suite.
8+
9+
A JSON file is used to tell this module which test suites to verify.
10+
11+
This module is designed to be used within the CI of VTR to ensure that tasks
12+
within test suites are running all the tasks they are intended to.
13+
"""
14+
import os
15+
import argparse
16+
import json
17+
import sys
18+
from dataclasses import dataclass, field
19+
from typing import List, Set
20+
from pathlib import Path
21+
22+
23+
@dataclass(order=True, frozen=True)
24+
class TestSuite:
25+
"""
26+
Data class used to store information about a test suite.
27+
"""
28+
29+
name: str
30+
ignored_tasks: List[str] = field(default_factory=list)
31+
32+
33+
def parse_test_suite_info(test_suite_info_file: str) -> List[TestSuite]:
34+
"""
35+
Parses the given test_suite_info file. The test suite info file is expected
36+
to be a JSON file which contains information on which test suites in the
37+
regression tests to verify and if any of the tasks should be ignored.
38+
39+
The JSON should have the following form:
40+
{"test_suites": [
41+
{
42+
"name": "<test_suite_name>",
43+
"ignored_tasks": [
44+
"<ignored_task_name>",
45+
...
46+
]
47+
},
48+
{
49+
...
50+
}
51+
]}
52+
"""
53+
with open(test_suite_info_file, "r") as file:
54+
data = json.load(file)
55+
56+
assert isinstance(data, dict), "Test suite info should be a dictionary"
57+
assert "test_suites" in data, "A list of test suites must be provided"
58+
59+
test_suites = []
60+
for test_suite in data["test_suites"]:
61+
assert isinstance(test_suite, dict), "Test suite should be a dictionary"
62+
assert "name" in test_suite, "All test suites must have names"
63+
assert "ignored_tasks" in test_suite, "All test suite must have an ignored task list"
64+
65+
test_suites.append(
66+
TestSuite(
67+
name=test_suite["name"],
68+
ignored_tasks=test_suite["ignored_tasks"],
69+
)
70+
)
71+
72+
return test_suites
73+
74+
75+
def parse_task_list(task_list_file: str) -> Set[str]:
76+
"""
77+
Parses the given task_list file and returns a list of the tasks within
78+
the task list.
79+
"""
80+
tasks = set()
81+
with open(task_list_file, "r") as file:
82+
for line in file:
83+
# Strip the whitespace from the line.
84+
line.strip()
85+
# If this is a comment line, skip it.
86+
if line[0] == "#":
87+
continue
88+
# Split the line. This is used in case there is a comment on the line.
89+
split_line = line.split()
90+
if split_line:
91+
# If the line can be split (i.e. the line is not empty), add
92+
# the first part of the line to the tasks list, stripping any
93+
# trailing "/" characters.
94+
tasks.add(split_line[0].rstrip("/"))
95+
96+
return tasks
97+
98+
99+
def get_expected_task_list(test_suite_dir: str, reg_tests_parent_dir: str) -> Set[str]:
100+
"""
101+
Get the expected task list by parsing the test suite directory and finding
102+
all files that look like config files.
103+
"""
104+
# Get all config files in the test suite. These will indicated where all
105+
# the tasks are in the suite.
106+
base_path = Path(test_suite_dir)
107+
assert base_path.is_dir()
108+
config_files = list(base_path.rglob("config.txt"))
109+
110+
# Get a list of all the expected tasks in the task list
111+
expected_task_list = set()
112+
for config_file in config_files:
113+
config_dir = os.path.dirname(config_file)
114+
task_dir = os.path.dirname(config_dir)
115+
# All tasks in the task list are relative to the parent of the regression
116+
# tests directory.
117+
expected_task_list.add(os.path.relpath(task_dir, reg_tests_parent_dir))
118+
119+
return expected_task_list
120+
121+
122+
def verify_test_suite(test_suite: TestSuite, regression_tests_dir: str):
123+
"""
124+
Verifies the given test suite by looking into the regression tests directory
125+
for the suite and ensures that all expected tasks are present in the suite's
126+
task list.
127+
128+
Returns the number of failures found in the test suite.
129+
"""
130+
# Check that the test suite exists in the regression tests directory
131+
test_suite_dir = os.path.join(regression_tests_dir, test_suite.name)
132+
if not os.path.exists(test_suite_dir):
133+
print("\tError: Test suite not found in regression tests directory")
134+
return 1
135+
136+
# Get the expected tasks list from the test suite directory.
137+
reg_tests_parent_dir = os.path.dirname(regression_tests_dir.rstrip("/"))
138+
expected_task_list = get_expected_task_list(test_suite_dir, reg_tests_parent_dir)
139+
140+
# Get the task list file from the test suite and parse it to get the actual
141+
# task list.
142+
task_list_file = os.path.join(test_suite_dir, "task_list.txt")
143+
if not os.path.exists(task_list_file):
144+
print("\tError: Test suite does not have a root-level task list")
145+
return 1
146+
actual_task_list = parse_task_list(task_list_file)
147+
148+
# Keep track of the number of failures
149+
num_failures = 0
150+
151+
# Process the ignored tests
152+
ignored_tasks = set()
153+
for ignored_task in test_suite.ignored_tasks:
154+
# Ignored tasks are relative to the test directory, get their full path.
155+
ignored_task_path = os.path.join(test_suite_dir, ignored_task)
156+
# Check that the task exists.
157+
if not os.path.exists(ignored_task_path):
158+
print(f"\tError: Ignored task '{ignored_task}' not found in test suite")
159+
num_failures += 1
160+
continue
161+
# If the task exists, add it to the ignored tasks list relative to the
162+
# reg test's parent directory so it can be compared properly.
163+
ignored_tasks.add(os.path.relpath(ignored_task_path, reg_tests_parent_dir))
164+
165+
if len(ignored_tasks) > 0:
166+
print(f"\tWarning: {len(ignored_tasks)} tasks were ignored")
167+
168+
# Check for any missing tasks in the task list
169+
for task in expected_task_list:
170+
# If this task is ignored, it is expected to be missing.
171+
if task in ignored_tasks:
172+
continue
173+
# If the task is not in the actual task list, this is an error.
174+
if task not in actual_task_list:
175+
print(f"\tError: Failed to find task '{task}' in task list!")
176+
num_failures += 1
177+
178+
# Check for any tasks in the task list which should not be there
179+
for task in actual_task_list:
180+
# If a task is in the task list, but is not in the test directory, this
181+
# is a failure.
182+
if task not in expected_task_list:
183+
print(f"\tError: Task '{task}' found in task list but not in test directory")
184+
num_failures += 1
185+
# If a task is in the task list, but is marked as ignored, this must be
186+
# a mistake.
187+
if task in ignored_tasks:
188+
print(f"\tError: Task '{task}' found in task list but was marked as ignored")
189+
190+
return num_failures
191+
192+
193+
def verify_test_suites():
194+
"""
195+
Verify the VTR test suites.
196+
197+
Test suites are verified by checking the tasks within their test directory
198+
and the tasks within the task list at the root of that directory and ensuring
199+
that they match. If there are any tasks which appear in one but not the other,
200+
an error is produced and this script will return an error code.
201+
"""
202+
# Set up the argument parser object.
203+
parser = argparse.ArgumentParser(description="Verifies the test suites used in VTR.")
204+
parser.add_argument(
205+
"-vtr_regression_tests_dir",
206+
type=str,
207+
required=True,
208+
help="The path to the vtr_flow/tasks/regression_tests directory in VTR.",
209+
)
210+
parser.add_argument(
211+
"-test_suite_info",
212+
type=str,
213+
required=True,
214+
help="Information on the test suite (must be a JSON file).",
215+
)
216+
217+
# Parse the arguments from the command line.
218+
args = parser.parse_args()
219+
220+
# Verify each of the test suites.
221+
num_failures = 0
222+
test_suites = parse_test_suite_info(args.test_suite_info)
223+
for test_suite in test_suites:
224+
print(f"Verifying test suite: {test_suite.name}")
225+
test_suite_failures = verify_test_suite(test_suite, args.vtr_regression_tests_dir)
226+
print(f"\tTest suite had {test_suite_failures} failures\n")
227+
num_failures += test_suite_failures
228+
229+
# If any failures were found in any suite, return exit code 1.
230+
if num_failures != 0:
231+
print(f"Failure: Test suite verifcation failed with {num_failures} failures")
232+
print(f"Please fix the failing test suites found in {args.vtr_regression_tests_dir}")
233+
print(f"If necessary, update the test suites info found here: {args.test_suite_info}")
234+
sys.exit(1)
235+
236+
print(f"Success: All test suites in {args.test_suite_info} passed")
237+
238+
239+
if __name__ == "__main__":
240+
verify_test_suites()

0 commit comments

Comments
 (0)