-
-
Notifications
You must be signed in to change notification settings - Fork 127
ci: check the setting paths shipped scripts hand to cli #146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+187
−7
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| #!/usr/bin/env python3 | ||
| """Check the setting paths every shipped script hands to `cli`. | ||
|
|
||
| WHY THIS EXISTS. `cli -s` writes a value into /etc/majestic.yaml and cannot | ||
| fail: yaml-cli stores whatever dotted path it is given, creating the | ||
| intermediate mappings as it goes, and exits 0. Nothing downstream complains | ||
| either, because majestic simply never reads a key it does not recognise. A typo | ||
| in a setting path is therefore completely silent from the moment it is written | ||
| to the end of the device's life. | ||
|
|
||
| devices/t40_lite_movols-mo-805p/general/overlay/usr/share/openipc/customizer.sh | ||
| is what this file is made of. Six of its lines carry a trailing colon: | ||
|
|
||
| cli -s .video0.bitrate: 4000 | ||
| cli -s .video0.rcMode: avbr | ||
|
|
||
| so the path is `.video0.bitrate:`, which is not `.video0.bitrate`. Those six | ||
| settings -- bitrate, rate-control mode, profile, GOP size, GOP mode and OSD | ||
| size -- have never applied on that camera, and no build, no review and no boot | ||
| ever said so. The device has shipped that way. | ||
|
|
||
| SCOPE is the shape of the path, deliberately, and nothing else. Whether | ||
| `.video0.bitrate` is a key this majestic build actually declares is a question | ||
| only the running binary can answer (`curl localhost/api/v1/config.json` lists | ||
| what a given build has), and it varies by vendor and by flavour. Shape is what | ||
| can be decided from the tree alone, it is free, and it is the whole of the | ||
| class that shipped. | ||
|
|
||
| A path built at runtime is skipped rather than guessed at -- | ||
| devices/gk7205v200_otg_generic/.../uvc-gadget-setup reads `cli -g ".$1"` out of | ||
| a helper, which is fine and unknowable here. | ||
|
|
||
| Usage: | ||
| lint-cli-paths.py # check the tree | ||
| lint-cli-paths.py --self-test # check this checker still catches things | ||
| """ | ||
|
|
||
| import re | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| # `cli`, `sensor_cli` and the `yaml-cli` binary underneath them all take the | ||
| # same grammar: a mode flag, then the dotted path. `wifibroadcast cli` is the | ||
| # same applet against /etc/wfb.yaml and is caught by the same rule. | ||
| INVOCATION = re.compile( | ||
| r"\b(?:yaml-cli|sensor_cli|cli)\s+" | ||
| r"(?:-[a-zA-Z]+\s+\S+\s+)*" # any -i/-o pair that came first | ||
| r"(-s|--set|-g|--get|-d|--del)\s+" | ||
| r"(\"[^\"]*\"|'[^']*'|\S+)" | ||
| ) | ||
|
|
||
| # A leading dot is convention rather than syntax (yaml-cli splits on "." and | ||
| # drops empty components), so it is optional here. What is not optional is that | ||
| # every component is a plausible YAML key. | ||
| WELL_FORMED = re.compile(r"^\.?[A-Za-z0-9_][A-Za-z0-9_.-]*$") | ||
|
|
||
| # Interpolation of any kind means the path is not decidable from the tree. | ||
| DYNAMIC = re.compile(r"[$`]") | ||
|
|
||
|
|
||
| def path_is_malformed(path): | ||
| """True when this is a path yaml-cli will store and majestic never read.""" | ||
| if DYNAMIC.search(path): | ||
| return False | ||
| if not WELL_FORMED.match(path): | ||
| return True | ||
| # Empty components and a trailing separator address nothing. yaml-cli drops | ||
| # them silently rather than refusing, which is what makes them worth | ||
| # catching here. | ||
| return ".." in path or path.endswith(".") | ||
|
|
||
|
|
||
| def findings_in(text): | ||
| """(line number, offending path, source line) for each bad path in `text`.""" | ||
| out = [] | ||
| for lineno, line in enumerate(text.splitlines(), 1): | ||
| if line.lstrip().startswith("#"): | ||
| continue | ||
| for match in INVOCATION.finditer(line): | ||
| path = match.group(2).strip("\"'") | ||
| if path_is_malformed(path): | ||
| out.append((lineno, path, line.strip())) | ||
| return out | ||
|
|
||
|
|
||
| def is_shell(p): | ||
| """Shipped shell, by extension or by shebang. Markdown is not scanned: the | ||
| documentation spells paths as `.<path>` on purpose.""" | ||
| if p.suffix == ".sh": | ||
| return True | ||
| try: | ||
| with p.open("rb") as f: | ||
| return f.read(2) == b"#!" and b"sh" in f.readline() | ||
| except OSError: | ||
| return False | ||
|
|
||
|
|
||
| def check_tree(root): | ||
| scanned = 0 | ||
| bad = [] | ||
| for p in sorted(Path(root).rglob("*")): | ||
| if not p.is_file() or ".git/" in str(p) or not is_shell(p): | ||
| continue | ||
| try: | ||
| text = p.read_text(errors="ignore") | ||
| except OSError: | ||
| continue | ||
| if not INVOCATION.search(text): | ||
| continue | ||
| scanned += 1 | ||
| for lineno, path, line in findings_in(text): | ||
| bad.append((p, lineno, path, line)) | ||
|
|
||
| # A scan that finds nothing to check looks exactly like a clean run, which | ||
| # is the failure mode every silent-typo bug in this file is made of. | ||
| if scanned == 0: | ||
| print("FAIL: no script in the tree invokes cli -- is the matcher broken?") | ||
| return 1 | ||
|
|
||
| for p, lineno, path, line in bad: | ||
| print(f"{p}:{lineno}: malformed setting path {path!r}") | ||
| print(f" {line}") | ||
| if bad: | ||
| print(f"\nFAIL: {len(bad)} malformed cli setting path(s) in {scanned} script(s)") | ||
| print("A path yaml-cli stores but majestic never reads applies nothing,") | ||
| print("and says nothing. Fix the path, or use a variable if it is dynamic.") | ||
| return 1 | ||
| print(f"ok cli setting paths well formed in {scanned} script(s)") | ||
| return 0 | ||
|
|
||
|
|
||
| def self_test(): | ||
| cases = [ | ||
| ("cli -s .video0.bitrate 4096", False, "plain set"), | ||
| ("cli -s .video0.bitrate: 4000", True, "the movols trailing colon"), | ||
| ("cli -s .osd.size: 0.8", True, "trailing colon, float value"), | ||
| ("cli -g .video0.size", False, "get"), | ||
| ("cli -d .outgoing.server", False, "delete"), | ||
| ("cli -s .isp.sensorConfig /etc/sensors/imx335.ini", False, "path-like value"), | ||
| ('cli -g ".$1"', False, "runtime path is not decidable"), | ||
| ("cli -s .nightMode.irCutPin1 11", False, "digits in a component"), | ||
| ("cli -s .outgoing.0 rtmp://a/b", False, "sequence index"), | ||
| ("cli -s .video0..fps 20", True, "empty component"), | ||
| ("cli -s .video0. 20", True, "trailing separator"), | ||
| ("cli -s .video0.fps=20 x", True, "= is not a separator"), | ||
| ("yaml-cli -i /etc/wfb.yaml -s .wireless.txpower 20", False, "explicit -i first"), | ||
| ("# cli -s .video0.bitrate: 4000", False, "commented out"), | ||
| ("sensor_cli -s .sensor.width 1920", False, "sensor_cli applet"), | ||
| ] | ||
| rc = 0 | ||
| for line, want_bad, name in cases: | ||
| got_bad = bool(findings_in(line)) | ||
| if got_bad != want_bad: | ||
| verb = "flag" if want_bad else "accept" | ||
| print(f"FAIL self-test: expected to {verb} {name}: {line!r}") | ||
| rc = 1 | ||
| else: | ||
| print(f"ok self-test: {name}") | ||
| return rc | ||
|
|
||
|
|
||
| def main(): | ||
| if "--self-test" in sys.argv[1:]: | ||
| return self_test() | ||
| root = sys.argv[1] if len(sys.argv) > 1 else "." | ||
| return check_tree(root) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.