From ed1322518badd48b53a9c58c203e02b7ad462fd8 Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Tue, 13 Oct 2026 09:45:16 -0400 Subject: [PATCH] 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. --- forgetest/forgetest/manifest.py | 41 +++++++++++++++++++++++++++----- forgetest/tests/test_manifest.py | 10 ++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/forgetest/forgetest/manifest.py b/forgetest/forgetest/manifest.py index f26dae6..b9ff82d 100644 --- a/forgetest/forgetest/manifest.py +++ b/forgetest/forgetest/manifest.py @@ -430,6 +430,30 @@ def _discovers(args, full, at): 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): """Whether pytest ARGS collects the test: named, or its directory given 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 (a script run directly, or a C test's built binary, /); 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 - the 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.""" + whole path; a for loop's list over the test; python -m unittest given + the test's file or a dotted name whose module is that file + (tests.test_x, tests.test_x.Case.test_one); unittest discover from the + 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) stem, ext = posixpath.splitext(posixpath.basename(full)) compiled = ext in (".c", ".cc", ".cpp") @@ -478,8 +504,11 @@ def workflow_runs(steps, test_path): if "-m" in args: i = args.index("-m") 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 \ - (mod == "pytest" and _pytest_runs(rest, full, at)): + 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)): return True else: script = next((a for a in args if not a.startswith("-")), None) diff --git a/forgetest/tests/test_manifest.py b/forgetest/tests/test_manifest.py index 4c15f47..85e6385 100644 --- a/forgetest/tests/test_manifest.py +++ b/forgetest/tests/test_manifest.py @@ -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("steps:\n - run: |\n sudo -E FORGEEXT=build/forgeext \\\n" " 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): import contextlib