Skip to content

Commit 7286cd2

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 a6fa23f commit 7286cd2

File tree

3 files changed

+318
-0
lines changed

3 files changed

+318
-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: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Module to verify VTR test suites run by the GitHub CI.
4+
"""
5+
import os
6+
import argparse
7+
import json
8+
import sys
9+
from dataclasses import dataclass, field
10+
from typing import List, Set
11+
from pathlib import Path
12+
13+
14+
@dataclass(order=True, frozen=True)
15+
class TestSuite:
16+
"""
17+
Data class used to store information about a test suite.
18+
"""
19+
20+
name: str
21+
ignored_tasks: List[str] = field(default_factory=list)
22+
23+
24+
def parse_test_suite_info(test_suite_info_file: str) -> List[TestSuite]:
25+
"""
26+
Parses the given test_suite_info file. The test suite info file is expected
27+
to be a JSON file which contains information on which test suites in the
28+
regression tests to verify and if any of the tasks should be ignored.
29+
"""
30+
with open(test_suite_info_file, "r") as file:
31+
data = json.load(file)
32+
33+
assert isinstance(data, dict), "Test suite info should be a dictionary"
34+
assert "test_suites" in data, "A list of test suites must be provided"
35+
36+
test_suites = []
37+
for test_suite in data["test_suites"]:
38+
assert isinstance(test_suite, dict), "Test suite should be a dictionary"
39+
assert "name" in test_suite, "All test suites must have names"
40+
assert "ignored_tasks" in test_suite, "All test suite must have an ignored task list"
41+
42+
test_suites.append(
43+
TestSuite(
44+
name=test_suite["name"],
45+
ignored_tasks=test_suite["ignored_tasks"],
46+
)
47+
)
48+
49+
return test_suites
50+
51+
52+
def parse_task_list(task_list_file: str) -> Set[str]:
53+
"""
54+
Parses the given task_list file and returns a list of the tasks within
55+
the task list.
56+
"""
57+
tasks = set()
58+
with open(task_list_file, "r") as file:
59+
for line in file:
60+
# Strip the whitespace from the line.
61+
line.strip()
62+
# If this is a comment line, skip it.
63+
if line[0] == "#":
64+
continue
65+
# Split the line. This is used in case there is a comment on the line.
66+
split_line = line.split()
67+
if split_line:
68+
# If the line can be split (i.e. the line is not empty), add
69+
# the first part of the line to the tasks list, stripping any
70+
# trailing "/" characters.
71+
tasks.add(split_line[0].rstrip("/"))
72+
73+
return tasks
74+
75+
76+
def get_expected_task_list(test_suite_dir: str,
77+
reg_tests_parent_dir: str) -> Set[str]:
78+
"""
79+
Get the expected task list by parsing the test suite directory and finding
80+
all files that look like config files.
81+
"""
82+
# Get all config files in the test suite. These will indicated where all
83+
# the tasks are in the suite.
84+
base_path = Path(test_suite_dir)
85+
assert base_path.is_dir()
86+
config_files = list(base_path.rglob("config.txt"))
87+
88+
# Get a list of all the expected tasks in the task list
89+
expected_task_list = set()
90+
for config_file in config_files:
91+
config_dir = os.path.dirname(config_file)
92+
task_dir = os.path.dirname(config_dir)
93+
# All tasks in the task list are relative to the parent of the regression
94+
# tests directory.
95+
expected_task_list.add(os.path.relpath(task_dir, reg_tests_parent_dir))
96+
97+
return expected_task_list
98+
99+
100+
def verify_test_suite(test_suite: TestSuite, regression_tests_dir: str):
101+
"""
102+
Verifies the given test suite by looking into the regression tests directory
103+
for the suite and ensures that all expected tasks are present in the suite's
104+
task list.
105+
106+
Returns the number of failures found in the test suite.
107+
"""
108+
# Check that the test suite exists in the regression tests directory
109+
test_suite_dir = os.path.join(regression_tests_dir, test_suite.name)
110+
if not os.path.exists(test_suite_dir):
111+
print("\tError: Test suite not found in regression tests directory")
112+
return 1
113+
114+
# Get the expected tasks list from the test suite directory.
115+
reg_tests_parent_dir = os.path.dirname(regression_tests_dir.rstrip("/"))
116+
expected_task_list = get_expected_task_list(test_suite_dir, reg_tests_parent_dir)
117+
118+
# Get the task list file from the test suite and parse it to get the actual
119+
# task list.
120+
task_list_file = os.path.join(test_suite_dir, "task_list.txt")
121+
if not os.path.exists(task_list_file):
122+
print("\tError: Test suite does not have a root-level task list")
123+
return 1
124+
actual_task_list = parse_task_list(task_list_file)
125+
126+
# Keep track of the number of failures
127+
num_failures = 0
128+
129+
# Process the ignored tests
130+
ignored_tasks = set()
131+
for ignored_task in test_suite.ignored_tasks:
132+
# Ignored tasks are relative to the test directory, get their full path.
133+
ignored_task_path = os.path.join(test_suite_dir, ignored_task)
134+
# Check that the task exists.
135+
if not os.path.exists(ignored_task_path):
136+
print(f"\tError: Ignored task '{ignored_task}' not found in test suite")
137+
num_failures += 1
138+
continue
139+
# If the task exists, add it to the ignored tasks list relative to the
140+
# reg test's parent directory so it can be compared properly.
141+
ignored_tasks.add(os.path.relpath(ignored_task_path, reg_tests_parent_dir))
142+
143+
if len(ignored_tasks) > 0:
144+
print(f"\tWarning: {len(ignored_tasks)} tasks were ignored")
145+
146+
# Check for any missing tasks in the task list
147+
for task in expected_task_list:
148+
# If this task is ignored, it is expected to be missing.
149+
if task in ignored_tasks:
150+
continue
151+
# If the task is not in the actual task list, this is an error.
152+
if task not in actual_task_list:
153+
print(f"\tError: Failed to find task '{task}' in task list!")
154+
num_failures += 1
155+
156+
# Check for any tasks in the task list which should not be there
157+
for task in actual_task_list:
158+
# If a task is in the task list, but is not in the test directory, this
159+
# is a failure.
160+
if task not in expected_task_list:
161+
print(f"\tError: Task '{task}' found in task list but not in test directory")
162+
num_failures += 1
163+
# If a task is in the task list, but is marked as ignored, this must be
164+
# a mistake.
165+
if task in ignored_tasks:
166+
print(f"\tError: Task '{task}' found in task list but was marked as ignored")
167+
168+
return num_failures
169+
170+
171+
def verify_test_suites():
172+
"""
173+
Verify the VTR test suites.
174+
"""
175+
# Set up the argument parser object.
176+
parser = argparse.ArgumentParser(description="Verifies the test suites used in VTR.")
177+
parser.add_argument(
178+
"-vtr_regression_tests_dir",
179+
type=str,
180+
required=True,
181+
help="The path to the vtr_flow/tasks/regression_tests directory in VTR.",
182+
)
183+
parser.add_argument(
184+
"-test_suite_info",
185+
type=str,
186+
required=True,
187+
help="Information on the test suite (must be a JSON file).",
188+
)
189+
190+
# Parse the arguments from the command line.
191+
args = parser.parse_args()
192+
193+
# Verify each of the test suites.
194+
num_failures = 0
195+
test_suites = parse_test_suite_info(args.test_suite_info)
196+
for test_suite in test_suites:
197+
print(f"Verifying test suite: {test_suite.name}")
198+
test_suite_failures = verify_test_suite(test_suite, args.vtr_regression_tests_dir)
199+
print(f"\tTest suite had {test_suite_failures} failures\n")
200+
num_failures += test_suite_failures
201+
202+
# If any failures were found in any suite, return exit code 1.
203+
if num_failures != 0:
204+
print(f"Failure: Test suite verifcation failed with {num_failures} failures")
205+
print(f"Please fix the failing test suites found in {args.vtr_regression_tests_dir}")
206+
print(f"If necessary, update the test suites info found here: {args.test_suite_info}")
207+
sys.exit(1)
208+
209+
print(f"Success: All test suites in {args.test_suite_info} passed")
210+
211+
212+
if __name__ == "__main__":
213+
verify_test_suites()

0 commit comments

Comments
 (0)