Skip to content

Commit 88ab62b

Browse files
redsun82Copilot
andcommitted
Just: let argparse own the options this script acts on
Hand-sorting every argument was justified while `--all-checks` was both a flag and an assignment, which argparse cannot express. Naming the offer separately removed that, and the loop was then keeping three silent mistakes alive: `--codeql built` made `built` a test path, and a bare `--codeql` or `--extra-check` was forwarded to `codeql test run` to fail there instead of here. argparse takes those three options; the shape test stays for the rest, which belongs to `codeql test run` and is forwarded untouched. Errors are routed back through `error` so they still arrive in a just banner. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 4b2718f commit 88ab62b

2 files changed

Lines changed: 50 additions & 20 deletions

File tree

misc/just/codeql_test_run.py

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
the caller supplies the switch, so the two are separate options rather than one.
1111
"""
1212

13+
import argparse
1314
import dataclasses
1415
import os
1516
import re
@@ -23,7 +24,6 @@
2324
CMD_END = os.environ.get("CMD_END", "")
2425
SEMMLE_CODE = os.environ.get("SEMMLE_CODE")
2526

26-
EXTRA_CHECK_PREFIX = "--extra-check="
2727
ENV_RE = re.compile(r"^[A-Z_][A-Z_0-9]*=.*$")
2828

2929

@@ -41,15 +41,37 @@ def error(message):
4141
print(f"{ERROR}{message}", file=sys.stderr)
4242

4343

44+
class _Parser(argparse.ArgumentParser):
45+
"""An `argparse` parser that fails the way the rest of this script does.
46+
47+
The default reports to `stderr` in its own format and exits 2, which would arrive
48+
in a `just` banner unprefixed and alongside a usage line naming this script rather
49+
than the recipe the caller actually typed.
50+
"""
51+
52+
def error(self, message):
53+
error(message)
54+
raise SystemExit(1)
55+
56+
57+
def build_parser():
58+
# `+` can be an option string only because it is also a prefix character. `-h` and
59+
# `--help` are left unclaimed so that they reach `codeql test run`.
60+
parser = _Parser(add_help=False, allow_abbrev=False, prefix_chars="-+")
61+
parser.add_argument("--codeql")
62+
parser.add_argument("--extra-check", action="append", dest="extra_checks")
63+
parser.add_argument("--all-checks", "+", action="store_true", dest="all")
64+
return parser
65+
66+
4467
@dataclasses.dataclass
4568
class Arguments:
4669
"""A command line sorted into the kinds that are handled differently.
4770
48-
Sorted by hand rather than by `argparse`, which could own the three options named
49-
below but none of the rest: every flag not named here belongs to `codeql test run`
50-
and has to survive untouched, `+` is not a spelling `argparse` has, and `CPUS=4` and
51-
`ql/test` are both positionals told apart only by shape. Handing it the half it can
52-
take would leave this loop in place for the other half.
71+
`argparse` owns the three options this script acts on itself. Everything else
72+
belongs to `codeql test run` and has to survive untouched, which is what
73+
`parse_known_args` hands back, and what is sorted by shape below: a test path and
74+
a `CPUS=4` are both positionals, told apart only by how they look.
5375
"""
5476

5577
codeql: str = dataclasses.field(
@@ -62,19 +84,19 @@ class Arguments:
6284
extra_checks: list = dataclasses.field(default_factory=list)
6385

6486
def parse(self, argv):
65-
"""Sort arguments into tests, flags and environment assignments."""
66-
for arg in argv:
67-
if not arg:
68-
# an empty argument can come from a caller interpolating an unset
69-
# variable
70-
continue
71-
if arg.startswith(EXTRA_CHECK_PREFIX):
72-
self.extra_checks.append(arg[len(EXTRA_CHECK_PREFIX) :])
73-
elif arg.startswith("--codeql="):
74-
self.codeql = arg.split("=", 1)[1]
75-
elif arg in ("+", "--all-checks"):
76-
self.all = True
77-
elif arg.startswith("-"):
87+
"""Sort arguments into tests, flags and environment assignments.
88+
89+
Additive, because `main` parses a second time to apply the checks held back
90+
until `--all-checks` asked for them.
91+
"""
92+
# An empty argument can come from a caller interpolating an unset variable.
93+
known, rest = build_parser().parse_known_args([arg for arg in argv if arg])
94+
if known.codeql:
95+
self.codeql = known.codeql
96+
self.all = self.all or known.all
97+
self.extra_checks += known.extra_checks or []
98+
for arg in rest:
99+
if arg.startswith("-"):
78100
self.flags.append(arg)
79101
elif ENV_RE.match(arg):
80102
self.env.append(arg)

misc/just/test_codeql_test_run.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ def test_an_extra_check_is_held_back_until_it_is_asked_for(self):
5858
self.assertEqual(held.flags, [])
5959
self.assertFalse(held.all)
6060

61+
def test_a_double_dash_hands_everything_after_it_to_codeql(self):
62+
# Standard `--`: past it, an option is the caller's business and not ours.
63+
args = sorted_args("--", "--codeql=built")
64+
self.assertEqual(args.codeql, "host")
65+
self.assertIn("--codeql=built", args.flags)
66+
6167
def test_an_empty_argument_is_ignored(self):
6268
# One of these comes of a caller interpolating a variable that was never set.
6369
self.assertEqual(sorted_args("", "test").tests, ["test"])
@@ -74,7 +80,9 @@ def test_an_assignment_whose_value_contains_a_space_stays_whole(self):
7480
self.assertEqual(sorted_args("EXTRA=a b").env, ["EXTRA=a b"])
7581

7682
def test_sorts_a_whole_command_line_at_once(self):
77-
args = sorted_args("-j2", "CPUS=4", "ql/test", "+", "--extra-check=--check-diff")
83+
args = sorted_args(
84+
"-j2", "CPUS=4", "ql/test", "+", "--extra-check=--check-diff"
85+
)
7886
self.assertEqual(args.flags, ["-j2"])
7987
self.assertEqual(args.env, ["CPUS=4"])
8088
self.assertEqual(args.tests, ["ql/test"])

0 commit comments

Comments
 (0)