mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-10-18 18:41:41 -07:00
forgetest: the lint reads a needs: list at the key's indentation and across lines, and an empty one fails closed
The coverage lint counts a step of a HOST_TESTED row's workflow only
in a job whose `needs:` are met on every run, and _needs read two
valid forms of the key as an empty list, which _needs_run takes as
needs met. A block list whose items sit at the key's own indentation
(`needs:` over `- build`) ended at its first item, which the loop took
for the next key. A flow list continued on the lines under the key
(`needs: [` over `build` over `]`) ended at the key's line, with no
item, and one split after a comma (`needs: [lint,` over `build]`)
kept the first line's names and dropped the rest. So a job that
needed a conditional job, written either way, counted as running on
every run, and a host test behind a conditional build kept its row
green and its paths covered. No workflow a row names writes its
`needs:` in these forms.
_needs now takes a `- ` item at the key's own indentation as part of
the block list, and ends the list at a key at the key's indentation
or less; items at two indentations are a form it does not read. A
flow list whose `]` is not on the key's line is joined with the lines
under it up to the `]`: a key, or a line at the key's indentation
that is not the `]`, before it ends the join as a form not read.
Every form not read gives None, a name no job has, so the need is
never met: fail closed. An empty `needs:` (`[]`, or nothing under the
key) gives None too: GitHub refuses an empty needs, and a job that
names one does not run. The docstring names every form that fails
closed, so its claim is true of each.
Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s
tests -v` in forgetest): test_manifest.py runs 41 tests, OK, on
Ubuntu 24.04 with python 3.12, in a private PID namespace.
test_a_job_runs_only_when_every_job_it_needs_runs gains the block
list at the key's indentation and the flow list across three lines
among the forms that run the test with the needed job unconditional
and do not with it under `if: false` or `if: github.event_name ==
'push'`; a flow list split after its comma in the chain of three,
which runs the test, and does not with the second-line job under
`if: false`; and, each failing closed with the needed job
unconditional, `needs: []`, a bare `needs:`, a flow list a key
interrupts before its `]`, and items at two indentations. Through
host_row_problems, a workflow on `[push]` whose test job needs, in a
block list at the key's indentation, a build job under `if: false`
is listed as "does not run it". Control: the parent, 8fc411f, with
the new test file fails the case (failures=1) at the block list at
the key's indentation, and run one by one against the parent's
manifest.py, each of the seven new forms reads as run and the row
passes. The coverage lint on the tree manifest from the recipe pins,
with meta-openglow at 1e163bd on scarthgap, reports 0 uncovered
paths, 0 empty entries, 0 host-tested row problems, 43 tests.
pyflakes reports nothing.
Acceptance: none. forgetest is dev-only and in no fingerprint; the
lint runs in CI, not in a campaign, and tests/ are in no fingerprint.
This commit is contained in:
@@ -563,25 +563,43 @@ def workflow_filters(text):
|
||||
|
||||
def _needs(lines, i):
|
||||
"""The job names the `needs:` key at lines[i] names: a flow list
|
||||
(`[build, lint]`), one scalar, or a block list on the lines under
|
||||
it. None for a form the lint does not read (a line under the key
|
||||
that is no list item): a need no job meets."""
|
||||
(`[build, lint]`, on the key's line or continued on the lines under
|
||||
it up to its `]`), one scalar, or a block list on the lines under it
|
||||
(its items deeper than the key, or at the key's own indentation).
|
||||
None for every other form, which the lint does not read: a name no
|
||||
job has, so the need is never met. That is an empty `needs:` (`[]`,
|
||||
or nothing under the key), a flow list a key or a shallower line
|
||||
interrupts before its `]`, a line under the key that is no list
|
||||
item, and list items at two indentations."""
|
||||
key_at = _indent(lines[i])
|
||||
val = _KEY_RX.match(lines[i]).group(3).strip()
|
||||
if val.startswith("["):
|
||||
return [_scalar(x) for x in val.strip("[]").split(",") if x.strip()]
|
||||
for ln in lines[i + 1:]:
|
||||
if "]" in val:
|
||||
break
|
||||
if not ln.strip():
|
||||
continue
|
||||
ind, body = _indent(ln), ln.strip()
|
||||
if ind < key_at or (ind == key_at and not body.startswith("]")):
|
||||
return None
|
||||
val += " " + body
|
||||
if "]" not in val:
|
||||
return None
|
||||
return [_scalar(x) for x in val[1:val.index("]")].split(",") if x.strip()] or None
|
||||
if val:
|
||||
return [_scalar(val)]
|
||||
out = []
|
||||
out, at = [], None
|
||||
for ln in lines[i + 1:]:
|
||||
if not ln.strip():
|
||||
continue
|
||||
if _indent(ln) <= _indent(lines[i]):
|
||||
ind, body = _indent(ln), ln.strip()
|
||||
if ind < key_at or (ind == key_at and not body.startswith("- ")):
|
||||
break
|
||||
body = ln.strip()
|
||||
if not body.startswith("- "):
|
||||
at = ind if at is None else at
|
||||
if not body.startswith("- ") or ind != at:
|
||||
return None
|
||||
out.append(_scalar(body[2:]))
|
||||
return out
|
||||
return out or None
|
||||
|
||||
|
||||
def _needs_run(jobs):
|
||||
|
||||
@@ -413,7 +413,8 @@ class CoverageReportTests(unittest.TestCase):
|
||||
# on not every run, however the needs: is written
|
||||
yml = "jobs:\n build:\n%s steps:\n - run: make\n test:\n%s steps:\n - run: ./build/tray_test\n"
|
||||
for needs in (" needs: build\n", " needs: [build]\n", " needs: ['build']\n",
|
||||
" needs:\n - build\n"):
|
||||
" needs:\n - build\n", " needs:\n - build\n",
|
||||
" needs: [\n build\n ]\n"):
|
||||
self.assertTrue(self._runs(yml % ("", needs), test), needs)
|
||||
self.assertFalse(self._runs(yml % (" if: false\n", needs), test), needs)
|
||||
self.assertFalse(self._runs(yml % (" if: github.event_name == 'push'\n", needs), test), needs)
|
||||
@@ -430,6 +431,29 @@ class CoverageReportTests(unittest.TestCase):
|
||||
self.assertFalse(self._runs(yml % (" needs: test\n", " needs: build\n"), test))
|
||||
self.assertFalse(self._runs(yml % ("", " needs: test\n"), test))
|
||||
self.assertFalse(self._runs(yml % ("", " needs:\n build: true\n"), test))
|
||||
# a flow list split after a comma keeps the name on its second line: a conditional job
|
||||
# there holds the dependent job back
|
||||
split = (chain % "").replace(" needs: [b]\n", " needs: [a,\n b]\n")
|
||||
self.assertTrue(self._runs(split, test))
|
||||
self.assertFalse(self._runs(split.replace(" b:\n needs: a\n", " b:\n needs: a\n if: false\n"), test))
|
||||
# an empty needs: (which GitHub refuses), a flow list a key interrupts before its ], and
|
||||
# list items at two indentations each fail closed
|
||||
for needs in (" needs: []\n", " needs:\n", " needs: [\n build\n",
|
||||
" needs:\n - build\n - build\n"):
|
||||
self.assertFalse(self._runs(yml % ("", needs), test), needs)
|
||||
# through a row: the test job needs, in a block list at the key's own indentation, a
|
||||
# build job under if: false (the checkout in a job of its own, as the lint reads a
|
||||
# checkout for the whole workflow)
|
||||
man = helpers.make_manifest()
|
||||
root = self._root({"scripts/bench/stream_test.py": "",
|
||||
".github/workflows/needs.yml": "on: [push]\njobs:\n lint:\n steps:\n"
|
||||
" - uses: actions/checkout@v4\n build:\n if: false\n steps:\n"
|
||||
" - run: make\n test:\n needs:\n - build\n steps:\n"
|
||||
" - run: python3 scripts/bench/stream_test.py\n"})
|
||||
row = ("grblhal-glowforge", "src/driver.c", "forgefirm:scripts/bench/stream_test.py",
|
||||
"forgefirm:.github/workflows/needs.yml")
|
||||
self.assertEqual(m.host_row_problems(man, [row], root=root, sources=None),
|
||||
[row[:3] + (row[3] + " does not run it",)])
|
||||
# a job under always() runs on every run, and the lint evaluates no condition: it counts as
|
||||
# no job for its own steps and for the jobs after it
|
||||
self.assertFalse(self._runs(yml % (" if: false\n", " needs: build\n if: always()\n"), test))
|
||||
|
||||
Reference in New Issue
Block a user