forgetest: the lint takes python -m unittest given a test's file or module

forgectrl's CI runs its dev-server mock test as
`python3 -B -m unittest -v tests/test_devserver_mock.py`, and the
workflow judge counted python -m unittest only as `unittest discover`:
a HOST_TESTED row for that test was refused as not run.

_unittest_names: an argument to python -m unittest (not discover) runs
the test when it is the test's file (resolved against the step's
working-directory) or a dotted name whose module is that file
(tests.test_x, tests.test_x.Case.test_one); a -k pattern's value and an
argument with a shell variable are not names.

Proof: host tests: the CI's own command line, a module, a module with
a class and a method; not a module beside it, nor a -k pattern naming
it. The manifest tests (34) pass, and the forgetest host suite passes
under Linux. Against forgectrl's pinned workflow a row for
tests/test_devserver_mock.py is accepted; every earlier verdict on the
real workflows is unchanged.

Acceptance: none. forgetest is dev-only and in no fingerprint; the
lint runs in CI, not in a campaign.
This commit is contained in:
ScottW514
2026-10-13 09:45:16 -04:00
parent 8003d6f18c
commit ed1322518b
2 changed files with 45 additions and 6 deletions
+34 -5
View File
@@ -430,6 +430,30 @@ def _discovers(args, full, at):
return posixpath.dirname(full) == at(start) and fnmatch.fnmatchcase(posixpath.basename(full), pattern) return posixpath.dirname(full) == at(start) and fnmatch.fnmatchcase(posixpath.basename(full), pattern)
def _unittest_names(args, full, at):
"""Whether `python -m unittest ARGS` (not discover) runs the test: an
argument is its file (tests/test_x.py), or a dotted name whose module
is its file (tests.test_x, tests.test_x.Case.test_one)."""
skip = False
for a in args:
if skip:
skip = False
continue
if a in ("-k",):
skip = True
continue
if a.startswith("-") or "$" in a:
continue
if a.endswith(".py"):
if at(a) == full:
return True
continue
parts = a.split(".")
if any(at("/".join(parts[:k]) + ".py") == full for k in range(len(parts), 0, -1)):
return True
return False
def _pytest_runs(args, full, at): def _pytest_runs(args, full, at):
"""Whether pytest ARGS collects the test: named, or its directory given """Whether pytest ARGS collects the test: named, or its directory given
and its file name one pytest collects (test_*.py, *_test.py).""" and its file name one pytest collects (test_*.py, *_test.py)."""
@@ -451,10 +475,12 @@ def workflow_runs(steps, test_path):
resolved against the step's working-directory: the command itself resolved against the step's working-directory: the command itself
(a script run directly, or a C test's built binary, <dir>/<stem>); (a script run directly, or a C test's built binary, <dir>/<stem>);
the script an interpreter runs (python, bash, sh), a glob matching the the script an interpreter runs (python, bash, sh), a glob matching the
whole path; a for loop's list over the test; unittest discover from whole path; a for loop's list over the test; python -m unittest given
the test's own directory with a pattern its name matches; pytest on the test's file or a dotted name whose module is that file
the test or its own directory. An argument to anything else (echo, a (tests.test_x, tests.test_x.Case.test_one); unittest discover from the
build, an install) does not run the test.""" test's own directory with a pattern its name matches; pytest on the
test or its own directory. An argument to anything else (echo, a build,
an install) does not run the test."""
full = posixpath.normpath(test_path) full = posixpath.normpath(test_path)
stem, ext = posixpath.splitext(posixpath.basename(full)) stem, ext = posixpath.splitext(posixpath.basename(full))
compiled = ext in (".c", ".cc", ".cpp") compiled = ext in (".c", ".cc", ".cpp")
@@ -478,7 +504,10 @@ def workflow_runs(steps, test_path):
if "-m" in args: if "-m" in args:
i = args.index("-m") i = args.index("-m")
mod, rest = (args[i + 1], args[i + 2:]) if i + 1 < len(args) else ("", []) mod, rest = (args[i + 1], args[i + 2:]) if i + 1 < len(args) else ("", [])
if (mod == "unittest" and rest[:1] == ["discover"] and _discovers(rest[1:], full, at)) or \ if mod == "unittest" and rest[:1] == ["discover"]:
if _discovers(rest[1:], full, at):
return True
elif (mod == "unittest" and _unittest_names(rest, full, at)) or \
(mod == "pytest" and _pytest_runs(rest, full, at)): (mod == "pytest" and _pytest_runs(rest, full, at)):
return True return True
else: else:
+10
View File
@@ -293,6 +293,16 @@ class CoverageReportTests(unittest.TestCase):
self.assertTrue(self._runs(step % "bash tests/fflog_e2e.sh", "tests/fflog_e2e.sh")) self.assertTrue(self._runs(step % "bash tests/fflog_e2e.sh", "tests/fflog_e2e.sh"))
self.assertTrue(self._runs("steps:\n - run: |\n sudo -E FORGEEXT=build/forgeext \\\n" self.assertTrue(self._runs("steps:\n - run: |\n sudo -E FORGEEXT=build/forgeext \\\n"
" python3 -B tests/run_test.py\n", "tests/run_test.py")) " python3 -B tests/run_test.py\n", "tests/run_test.py"))
# python -m unittest given the test's file or its module (the third review: forgectrl's CI
# runs its mock test as a file), and not a module beside it or a -k pattern
self.assertTrue(self._runs(step % "python3 -B -m unittest -v tests/test_devserver_mock.py",
"tests/test_devserver_mock.py"))
self.assertTrue(self._runs(step % "python -m unittest tests.test_feeder", "tests/test_feeder.py"))
self.assertTrue(self._runs(step % "python -m unittest tests.test_feeder.FeederTests.test_one",
"tests/test_feeder.py"))
self.assertFalse(self._runs(step % "python -m unittest tests.test_lens", "tests/test_feeder.py"))
self.assertFalse(self._runs(step % "python -m unittest -k test_feeder tests.test_lens",
"tests/test_feeder.py"))
def test_the_lint_says_when_it_could_not_read_a_workflow(self): def test_the_lint_says_when_it_could_not_read_a_workflow(self):
import contextlib import contextlib