Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ is adding a file.
| `test_app_wrapper.sh` | Launching through either `app.sh` execution path does not leak an unrelated non-boolean `DEBUG` value into app2unit or xdg-terminal-exec |
| `test_binds.sh` | Loads the Lua keybinds against a stubbed Hyprland API: no two binds share a combination once modifiers are folded to what Hyprland matches on, no keysym sits in a modifier position, no bind uses the `code:NN` form the Lua parser rejects, every bind has a description, every `hyde-shell` command it runs exists. Touchpad gestures are checked in the same pass: a valid finger count, a direction and action Hyprland accepts, and no two gestures on the same finger count and direction |
| `test_git.sh` | The tree holds no gitlink without a matching `.gitmodules` entry, which would break `git submodule` and anything walking submodules |
| `test_dots.sh` | Every installer metafile under `Scripts/dots` parses, declares the keys the installer needs, uses a known action, and points at a source directory and source paths that exist. It also keeps Grimblast on the fixed official source |
| `test_dots.sh` | Every installer metafile under `Scripts/dots` parses, declares the keys the installer needs, uses a known action, and declares a usable `source` where it has one. A local entry's `source_root` and its non-glob paths have to exist inside the checkout; an entry backed by a remote `source` is exempt from those checks, since its files come from an archive the installer downloads, but its path types are still checked. It also keeps Grimblast on the fixed official source |
| `test_lua_syntax.sh` | Every shipped Lua file parses |
| `test_schema.sh` | Generated schema artifacts use the current Lua battery notification daemon rather than the removed shell implementation |
| `test_screenshot_wrapper.sh` | Satty receives a compatible default GTK renderer while preserving explicit renderer overrides |
Expand Down
82 changes: 66 additions & 16 deletions tests/python/check_dots.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,71 @@


def declared_paths(table: dict) -> list:
"""`paths` is a single string or a list of them."""
"""`paths` is a single string or a list of them.

Anything else is wrapped rather than iterated, so a malformed value is
reported as a wrong type instead of crashing the checker.
"""
paths = table.get("paths", [])
return [paths] if isinstance(paths, str) else list(paths)
if isinstance(paths, str):
return [paths]
if isinstance(paths, list):
return list(paths)
return [paths]


def has_glob(relative: str) -> bool:
"""A pattern is resolved at deploy time, so it cannot be checked here."""
return any(character in relative for character in "*?[")


def inside_repo(relative: str) -> bool:
"""A source path has to stay in the checkout, absolute or `..` included."""
root = REPO_ROOT.resolve()
def inside(root: pathlib.Path, relative: str) -> bool:
"""A declared path has to stay under `root`, absolute or `..` included."""
try:
return (root / relative).resolve().is_relative_to(root)
except (OSError, ValueError):
except (OSError, RuntimeError, ValueError):
# Older Python raises RuntimeError rather than OSError on a symlink loop.
return False
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def entries(document: dict) -> list[tuple[str, dict]]:
"""Yields (component, table) for every files table in a metafile."""
def inside_repo(relative: str) -> bool:
"""A source root has to stay in the checkout."""
return inside(REPO_ROOT.resolve(), relative)


def declared_sources(document: dict) -> list[tuple[str, object]]:
"""Yields (where, source) for every source declared in a metafile."""
found = []
for component, body in document.items():
if isinstance(body, dict):
if "source" in body:
found.append((component, body["source"]))
for index, table in enumerate(body.get("files", []) or []):
if isinstance(table, dict) and "source" in table:
found.append((f"{component}.files[{index}]", table["source"]))
return found


def is_remote_source(source: object) -> bool:
"""Only a non-empty string names something the installer can fetch."""
return isinstance(source, str) and bool(source.strip())


def entries(document: dict) -> list[tuple[str, dict, bool]]:
"""Yields (component, table, remote) for every files table in a metafile.

`remote` is true when the component or the entry declares a usable `source`.
Those files come from an archive the installer downloads, so their paths
resolve against the extracted tree rather than this checkout and cannot be
validated here. A malformed `source` does not earn that exemption.
"""
found = []
for component, body in document.items():
if isinstance(body, dict):
component_remote = is_remote_source(body.get("source"))
for table in body.get("files", []) or []:
found.append((component, table))
remote = component_remote or is_remote_source(table.get("source"))
found.append((component, table, remote))
return found


Expand Down Expand Up @@ -80,7 +119,11 @@ def fail(message: str) -> None:
fail(f"{name} is not valid TOML: {error}")
continue

for component, table in entries(document):
for where_source, source in declared_sources(document):
if not is_remote_source(source):
fail(f"{name} [{where_source}] declares an unusable source {source!r}")

for component, table, remote in entries(document):
where = f"{name} [{component}.files]"

if "paths" not in table:
Expand All @@ -92,19 +135,26 @@ def fail(message: str) -> None:
if action is not None and action not in ACTIONS:
fail(f"{where} has an unknown action {action!r}")

for relative in declared_paths(table):
if not isinstance(relative, str):
fail(f"{where} declares a path as {type(relative).__name__}, expected a string")

source_root = table.get("source_root")
if source_root is not None:
if not isinstance(source_root, str):
fail(f"{where} declares source_root as {type(source_root).__name__}, expected a string")
elif not inside_repo(source_root):
if source_root is not None and not isinstance(source_root, str):
fail(f"{where} declares source_root as {type(source_root).__name__}, expected a string")
elif isinstance(source_root, str) and not remote:
if not inside_repo(source_root):
fail(f"{where} points outside the repository with source_root {source_root!r}")
elif not (REPO_ROOT / source_root).is_dir():
fail(f"{where} points at a missing source_root {source_root!r}")
else:
root = (REPO_ROOT / source_root).resolve()
for relative in declared_paths(table):
if not isinstance(relative, str):
fail(f"{where} declares a path as {type(relative).__name__}, expected a string")
elif not has_glob(relative) and not (REPO_ROOT / source_root / relative).exists():
continue
if not inside(root, relative):
fail(f"{where} points outside its source_root with path {relative!r}")
elif not has_glob(relative) and not (root / relative).exists():
fail(f"{where} points at a missing path {relative!r}")

for component, table in dependencies(document):
Expand Down
Loading