3
This lint verifies that every Python test file (file that matches test_*.py or
4
*_test.py in the test folder) has a main block which raises an exception or
5
calls run_tests to ensure that the test will be run in OSS CI.
7
Takes ~2 minuters to run without the multiprocessing, probably overkill.
11
import multiprocessing as mp
13
from typing import List, NamedTuple, Optional
16
import libcst.matchers as m
18
LINTER_CODE = "TEST_HAS_MAIN"
21
class HasMainVisiter(cst.CSTVisitor):
22
def __init__(self) -> None:
26
def visit_Module(self, node: cst.Module) -> bool:
27
name = m.Name("__name__")
28
main = m.SimpleString('"__main__"') | m.SimpleString("'__main__'")
29
run_test_call = m.Call(
30
func=m.Name("run_tests") | m.Attribute(attr=m.Name("run_tests"))
32
raise_block = m.Raise()
35
if_main1 = m.Comparison(
37
[m.ComparisonTarget(m.Equal(), main)],
39
if_main2 = m.Comparison(
41
[m.ComparisonTarget(m.Equal(), name)],
43
for child in node.children:
44
if m.matches(child, m.If(test=if_main1 | if_main2)):
45
if m.findall(child, raise_block | run_test_call):
52
class LintSeverity(str, Enum):
59
class LintMessage(NamedTuple):
64
severity: LintSeverity
66
original: Optional[str]
67
replacement: Optional[str]
68
description: Optional[str]
71
def check_file(filename: str) -> List[LintMessage]:
74
with open(filename) as f:
77
cst.parse_module(file).visit(v)
80
"Test files need to have a main block which either calls run_tests "
81
+ "(to ensure that the tests are run during OSS CI) or raises an exception "
82
+ "and added to the blocklist in test/run_test.py"
90
severity=LintSeverity.ERROR,
101
parser = argparse.ArgumentParser(
102
description="test files should have main block linter",
103
fromfile_prefix_chars="@",
108
help="paths to lint",
111
args = parser.parse_args()
114
lint_messages = pool.map(check_file, args.filenames)
118
flat_lint_messages = []
119
for sublist in lint_messages:
120
flat_lint_messages.extend(sublist)
122
for lint_message in flat_lint_messages:
123
print(json.dumps(lint_message._asdict()), flush=True)
126
if __name__ == "__main__":