diff --git a/forgetest/forgetest/manifest.py b/forgetest/forgetest/manifest.py index 76de921..96c5633 100644 --- a/forgetest/forgetest/manifest.py +++ b/forgetest/forgetest/manifest.py @@ -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): diff --git a/forgetest/tests/test_manifest.py b/forgetest/tests/test_manifest.py index 911e2df..08300f6 100644 --- a/forgetest/tests/test_manifest.py +++ b/forgetest/tests/test_manifest.py @@ -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))