treefmt: format Python with ruff

Add ruff to the tree formatter so Python sources are kept consistently
formatted by 'nix fmt' and the CI format check. Test fixtures under
tests/modules are excluded since some are intentionally invalid Python.
Reformats the existing lib/python and tests scripts accordingly.
This commit is contained in:
Austin Horstman 2026-06-19 09:05:54 -05:00
parent 6aaf73d18d
commit fea021792d
9 changed files with 302 additions and 119 deletions

View file

@ -18,6 +18,7 @@ pkgs.treefmt.withConfig {
deadnix
keep-sorted
nixf-diagnose
ruff
];
settings = pkgs.lib.importTOML ../treefmt.toml;
}

View file

@ -17,52 +17,67 @@ def main():
print("🔍 Checking for duplicate maintainers between HM and nixpkgs...")
# Get home-manager maintainers
hm_result = subprocess.run(['nix', 'eval', '--file', 'modules/lib/maintainers.nix', '--json'],
capture_output=True, text=True, check=True)
hm_result = subprocess.run(
["nix", "eval", "--file", "modules/lib/maintainers.nix", "--json"],
capture_output=True,
text=True,
check=True,
)
hm_maintainers = json.loads(hm_result.stdout)
hm_github_users = set()
for name, data in hm_maintainers.items():
if 'github' in data:
hm_github_users.add(data['github'])
if "github" in data:
hm_github_users.add(data["github"])
# Read nixpkgs revision from flake.lock to ensure consistency between local and CI
flake_lock_path = Path('flake.lock')
with open(flake_lock_path, 'r') as f:
flake_lock_path = Path("flake.lock")
with open(flake_lock_path, "r") as f:
flake_lock = json.load(f)
nixpkgs_rev = flake_lock['nodes']['nixpkgs']['locked']['rev']
nixpkgs_rev = flake_lock["nodes"]["nixpkgs"]["locked"]["rev"]
print(f"📌 Using nixpkgs from flake.lock: {nixpkgs_rev[:7]}")
# Get nixpkgs maintainers from the locked revision
nixpkgs_result = subprocess.run(
['nix', 'eval', f'github:NixOS/nixpkgs/{nixpkgs_rev}#lib.maintainers', '--json'],
capture_output=True, text=True, check=True
[
"nix",
"eval",
f"github:NixOS/nixpkgs/{nixpkgs_rev}#lib.maintainers",
"--json",
],
capture_output=True,
text=True,
check=True,
)
nixpkgs_maintainers = json.loads(nixpkgs_result.stdout)
nixpkgs_github_users = set()
for name, data in nixpkgs_maintainers.items():
if isinstance(data, dict) and 'github' in data:
nixpkgs_github_users.add(data['github'])
if isinstance(data, dict) and "github" in data:
nixpkgs_github_users.add(data["github"])
# Find duplicates
duplicates = hm_github_users.intersection(nixpkgs_github_users)
if duplicates:
print(f'❌ Found {len(duplicates)} duplicate maintainers between HM and nixpkgs:')
print(
f"❌ Found {len(duplicates)} duplicate maintainers between HM and nixpkgs:"
)
for github_user in sorted(duplicates):
# Find the HM attribute name for this github user
hm_attr = None
for attr_name, data in hm_maintainers.items():
if data.get('github') == github_user:
if data.get("github") == github_user:
hm_attr = attr_name
break
print(f' - {github_user} (HM attribute: {hm_attr})')
print(f" - {github_user} (HM attribute: {hm_attr})")
print()
print('These maintainers should be removed from HM maintainers file to avoid duplication.')
print('They can be referenced directly from nixpkgs instead.')
print(
"These maintainers should be removed from HM maintainers file to avoid duplication."
)
print("They can be referenced directly from nixpkgs instead.")
sys.exit(1)
else:
print('✅ No duplicate maintainers found')
print("✅ No duplicate maintainers found")
if __name__ == "__main__":

View file

@ -16,6 +16,7 @@ from pathlib import Path
class NixEvalError(Exception):
"""Custom exception for errors during Nix evaluation."""
pass
@ -39,7 +40,9 @@ def run_nix_eval(nix_file: Path, *args: str) -> str:
)
return result.stdout.strip()
except FileNotFoundError:
logging.error("'nix-instantiate' command not found. Is Nix installed and in your PATH?")
logging.error(
"'nix-instantiate' command not found. Is Nix installed and in your PATH?"
)
raise NixEvalError("'nix-instantiate' not found")
except subprocess.CalledProcessError as e:
logging.error(f"Nix evaluation failed with exit code {e.returncode}")

View file

@ -40,6 +40,7 @@ def get_project_root() -> Path:
# Assumes this script is at: <root>/flake/dev/generate-all-maintainers/
return Path(__file__).parent.parent.parent.parent.resolve()
class MetaMaintainerGenerator:
"""Generates maintainers list using meta.maintainers from Home Manager evaluation."""
@ -54,9 +55,12 @@ class MetaMaintainerGenerator:
print("🔍 Extracting maintainers using meta.maintainers...")
try:
result = subprocess.run([
"nix", "eval", "--file", str(self.extractor_script), "--json"
], capture_output=True, text=True, timeout=60)
result = subprocess.run(
["nix", "eval", "--file", str(self.extractor_script), "--json"],
capture_output=True,
text=True,
timeout=60,
)
if result.returncode == 0:
data = json.loads(result.stdout)
@ -87,7 +91,7 @@ class MetaMaintainerGenerator:
print(f"🏠 Home Manager maintainers: {len(hm_maintainers)}")
print(f"📦 Nixpkgs maintainers: {len(nixpkgs_maintainers)}")
with open(self.output_file, 'w') as f:
with open(self.output_file, "w") as f:
f.write(
inspect.cleandoc("""
# Home Manager all maintainers list.
@ -114,9 +118,12 @@ class MetaMaintainerGenerator:
def validate_generated_file(self) -> bool:
"""Validate the generated Nix file syntax."""
try:
result = subprocess.run([
'nix-instantiate', '--eval', str(self.output_file), '--strict'
], capture_output=True, text=True, timeout=10)
result = subprocess.run(
["nix-instantiate", "--eval", str(self.output_file), "--strict"],
capture_output=True,
text=True,
timeout=10,
)
if result.returncode == 0:
print("✅ Generated file has valid Nix syntax")
@ -148,16 +155,16 @@ def main():
description="Generate Home Manager all-maintainers.nix using meta.maintainers"
)
parser.add_argument(
'--root',
"--root",
type=Path,
default=None,
help='Path to Home Manager root (default: auto-detect)'
help="Path to Home Manager root (default: auto-detect)",
)
parser.add_argument(
'--output',
"--output",
type=Path,
default=None,
help='Output file path (default: <root>/all-maintainers.nix)'
help="Output file path (default: <root>/all-maintainers.nix)",
)
args = parser.parse_args()

View file

@ -60,6 +60,7 @@ query($owner: String!, $repo: String!, $prNumber: Int!) {
class GHError(Exception):
"""Custom exception for errors related to 'gh' CLI commands."""
pass
@ -94,15 +95,28 @@ def get_manual_reviewer_actions(
tuple: (manually_requested, manually_removed) sets of usernames
"""
try:
result = run_gh_command([
"api", "graphql",
"-f", f"query={MANUAL_REVIEW_REQUEST_QUERY}",
"-F", f"owner={owner}",
"-F", f"repo={repo}",
"-F", f"prNumber={pr_number}",
])
result = run_gh_command(
[
"api",
"graphql",
"-f",
f"query={MANUAL_REVIEW_REQUEST_QUERY}",
"-F",
f"owner={owner}",
"-F",
f"repo={repo}",
"-F",
f"prNumber={pr_number}",
]
)
data = json.loads(result.stdout)
nodes = data.get("data", {}).get("repository", {}).get("pullRequest", {}).get("timelineItems", {}).get("nodes", [])
nodes = (
data.get("data", {})
.get("repository", {})
.get("pullRequest", {})
.get("timelineItems", {})
.get("nodes", [])
)
manually_requested = set()
manually_removed = set()
@ -143,7 +157,15 @@ def get_users_from_gh(args: list[str], error_message: str) -> set[str]:
def get_pending_reviewers(pr_number: int) -> set[str]:
"""Gets the set of currently pending reviewers for a PR."""
return get_users_from_gh(
["pr", "view", str(pr_number), "--json", "reviewRequests", "--jq", ".reviewRequests[].login"],
[
"pr",
"view",
str(pr_number),
"--json",
"reviewRequests",
"--jq",
".reviewRequests[].login",
],
"Error getting pending reviewers",
)
@ -151,7 +173,12 @@ def get_pending_reviewers(pr_number: int) -> set[str]:
def get_past_reviewers(owner: str, repo: str, pr_number: int) -> set[str]:
"""Gets the set of users who have already reviewed the PR."""
return get_users_from_gh(
["api", f"repos/{owner}/{repo}/pulls/{pr_number}/reviews", "--jq", ".[].user.login"],
[
"api",
f"repos/{owner}/{repo}/pulls/{pr_number}/reviews",
"--jq",
".[].user.login",
],
"Error getting past reviewers",
)
@ -162,17 +189,14 @@ def is_collaborator(owner: str, repo: str, username: str) -> bool:
Handles 404 as a non-collaborator, while other errors are raised.
"""
result = run_gh_command(
["api", f"repos/{owner}/{repo}/collaborators/{username}"],
check=False
["api", f"repos/{owner}/{repo}/collaborators/{username}"], check=False
)
if result.returncode == 0:
return True
if "HTTP 404" in result.stderr:
logging.error(
"'%s' is not a collaborator in this repository.", username
)
logging.error("'%s' is not a collaborator in this repository.", username)
return False
else:
logging.error(
@ -195,22 +219,32 @@ def update_reviewers(
if reviewers_to_add:
logging.info("Requesting reviews from: %s", ", ".join(reviewers_to_add))
try:
run_gh_command([
"pr", "edit", str(pr_number),
"--add-reviewer", ",".join(reviewers_to_add)
])
run_gh_command(
[
"pr",
"edit",
str(pr_number),
"--add-reviewer",
",".join(reviewers_to_add),
]
)
except GHError as e:
logging.error("Failed to add reviewers: %s", e)
if reviewers_to_remove and owner and repo:
logging.info("Removing review requests from: %s", ", ".join(reviewers_to_remove))
logging.info(
"Removing review requests from: %s", ", ".join(reviewers_to_remove)
)
payload = json.dumps({"reviewers": list(reviewers_to_remove)})
try:
run_gh_command(
[
"api", "--method", "DELETE",
"api",
"--method",
"DELETE",
f"repos/{owner}/{repo}/pulls/{pr_number}/requested_reviewers",
"--input", "-",
"--input",
"-",
],
input_data=payload,
)
@ -220,15 +254,33 @@ def update_reviewers(
def main() -> None:
"""Main function to handle command-line arguments and manage reviewers."""
parser = argparse.ArgumentParser(description="Manage pull request reviewers for Home Manager.")
parser = argparse.ArgumentParser(
description="Manage pull request reviewers for Home Manager."
)
parser.add_argument("--owner", required=True, help="Repository owner.")
parser.add_argument("--repo", required=True, help="Repository name.")
parser.add_argument("--pr-number", type=int, required=True, help="Pull request number.")
parser.add_argument(
"--pr-number", type=int, required=True, help="Pull request number."
)
parser.add_argument("--pr-author", required=True, help="PR author's username.")
parser.add_argument("--current-maintainers", default="", help="Space-separated list of maintainers for the changed files.")
parser.add_argument("--changed-files", default="", help="Newline-separated list of changed files.")
parser.add_argument("--bot-user-name", default="", help="Bot user name to distinguish manual vs automated review requests.")
parser.add_argument("--dry-run", action="store_true", help="Show what would be done without making actual changes.")
parser.add_argument(
"--current-maintainers",
default="",
help="Space-separated list of maintainers for the changed files.",
)
parser.add_argument(
"--changed-files", default="", help="Newline-separated list of changed files."
)
parser.add_argument(
"--bot-user-name",
default="",
help="Bot user name to distinguish manual vs automated review requests.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be done without making actual changes.",
)
args = parser.parse_args()
no_changed_files = not args.changed_files.strip()
@ -237,13 +289,15 @@ def main() -> None:
maintainers: set[str] = set(args.current_maintainers.split())
pending_reviewers = get_pending_reviewers(args.pr_number)
past_reviewers = get_past_reviewers(args.owner, args.repo, args.pr_number)
manually_requested, manually_removed = get_manual_reviewer_actions(args.owner, args.repo, args.pr_number, args.bot_user_name)
manually_requested, manually_removed = get_manual_reviewer_actions(
args.owner, args.repo, args.pr_number, args.bot_user_name
)
logging.info("File Maintainers: %s", ' '.join(maintainers) or "None")
logging.info("Pending Reviewers: %s", ' '.join(pending_reviewers) or "None")
logging.info("Past Reviewers: %s", ' '.join(past_reviewers) or "None")
logging.info("Manually Requested: %s", ' '.join(manually_requested) or "None")
logging.info("Manually Removed: %s", ' '.join(manually_removed) or "None")
logging.info("File Maintainers: %s", " ".join(maintainers) or "None")
logging.info("Pending Reviewers: %s", " ".join(pending_reviewers) or "None")
logging.info("Past Reviewers: %s", " ".join(past_reviewers) or "None")
logging.info("Manually Requested: %s", " ".join(manually_requested) or "None")
logging.info("Manually Removed: %s", " ".join(manually_removed) or "None")
# --- 2. Determine reviewers to remove ---
reviewers_to_remove: set[str] = set()
@ -257,13 +311,15 @@ def main() -> None:
if reviewers_to_remove:
if args.dry_run:
logging.info("DRY RUN: Would remove reviewers: %s", ", ".join(reviewers_to_remove))
logging.info(
"DRY RUN: Would remove reviewers: %s", ", ".join(reviewers_to_remove)
)
else:
update_reviewers(
args.pr_number,
owner=args.owner,
repo=args.repo,
reviewers_to_remove=reviewers_to_remove
reviewers_to_remove=reviewers_to_remove,
)
else:
logging.info("No reviewers to remove.")
@ -278,20 +334,29 @@ def main() -> None:
MAX_MAINTAINERS_THRESHOLD,
)
else:
users_to_exclude = {args.pr_author} | past_reviewers | pending_reviewers | manually_removed
users_to_exclude = (
{args.pr_author} | past_reviewers | pending_reviewers | manually_removed
)
potential_reviewers = maintainers - users_to_exclude
reviewers_to_add = {
user for user in potential_reviewers if is_collaborator(args.owner, args.repo, user)
user
for user in potential_reviewers
if is_collaborator(args.owner, args.repo, user)
}
non_collaborators = potential_reviewers - reviewers_to_add
if non_collaborators:
logging.warning("Ignoring non-collaborators: %s", ", ".join(non_collaborators))
logging.warning(
"Ignoring non-collaborators: %s", ", ".join(non_collaborators)
)
manually_removed_maintainers = reviewers_to_add & manually_removed
if manually_removed_maintainers:
logging.info("Not re-adding manually removed maintainers: %s", ", ".join(manually_removed_maintainers))
logging.info(
"Not re-adding manually removed maintainers: %s",
", ".join(manually_removed_maintainers),
)
reviewers_to_add -= manually_removed
if len(reviewers_to_add) > MAX_REVIEWERS:
@ -304,7 +369,9 @@ def main() -> None:
if reviewers_to_add:
if args.dry_run:
logging.info("DRY RUN: Would add reviewers: %s", ", ".join(reviewers_to_add))
logging.info(
"DRY RUN: Would add reviewers: %s", ", ".join(reviewers_to_add)
)
else:
update_reviewers(args.pr_number, reviewers_to_add=reviewers_to_add)
else:

View file

@ -14,32 +14,38 @@ import sys
def main():
print("🔍 Validating maintainer entries...")
result = subprocess.run(['nix', 'eval', '--file', 'modules/lib/maintainers.nix', '--json'],
capture_output=True, text=True, check=True)
result = subprocess.run(
["nix", "eval", "--file", "modules/lib/maintainers.nix", "--json"],
capture_output=True,
text=True,
check=True,
)
maintainers = json.loads(result.stdout)
errors = []
for name, data in maintainers.items():
if 'github' not in data:
if "github" not in data:
errors.append(f'{name}: Missing required field "github"')
if 'githubId' not in data:
if "githubId" not in data:
errors.append(f'{name}: Missing required field "githubId"')
if 'githubId' in data:
github_id = data['githubId']
if "githubId" in data:
github_id = data["githubId"]
if not isinstance(github_id, int):
errors.append(f'{name}: githubId must be a number, not a string: {github_id} (type: {type(github_id).__name__})')
errors.append(
f"{name}: githubId must be a number, not a string: {github_id} (type: {type(github_id).__name__})"
)
elif github_id <= 0:
errors.append(f'{name}: githubId must be positive: {github_id}')
errors.append(f"{name}: githubId must be positive: {github_id}")
if errors:
print('❌ Validation errors found:')
print("❌ Validation errors found:")
for error in errors:
print(f' - {error}')
print(f" - {error}")
sys.exit(1)
else:
print('✅ All maintainer entries are valid')
print(f'✅ Validated {len(maintainers)} maintainer entries')
print("✅ All maintainer entries are valid")
print(f"✅ Validated {len(maintainers)} maintainer entries")
if __name__ == "__main__":

View file

@ -13,8 +13,12 @@ def main():
print("🔍 Validating maintainers.nix syntax...")
try:
subprocess.run(['nix', 'eval', '--file', 'modules/lib/maintainers.nix', '--json'],
capture_output=True, text=True, check=True)
subprocess.run(
["nix", "eval", "--file", "modules/lib/maintainers.nix", "--json"],
capture_output=True,
text=True,
check=True,
)
print("✅ Valid Nix syntax")
except subprocess.CalledProcessError:
print("❌ Invalid Nix syntax")

View file

@ -13,10 +13,13 @@ SUCCESS_EMOJI = "✅"
FAILURE_EMOJI = ""
INFO_EMOJI = ""
class TestRunnerError(Exception):
"""Custom exception for TestRunner errors."""
pass
def _run_command(
cmd: Sequence[str],
*,
@ -35,14 +38,20 @@ def _run_command(
cwd=cwd,
)
except FileNotFoundError as e:
print(f"{FAILURE_EMOJI} Error: Command '{e.filename}' not found. Is it in your PATH?", file=sys.stderr)
print(
f"{FAILURE_EMOJI} Error: Command '{e.filename}' not found. Is it in your PATH?",
file=sys.stderr,
)
raise TestRunnerError(f"Command not found: {e.filename}") from e
except subprocess.CalledProcessError as e:
print(f"{FAILURE_EMOJI} Error executing command: {' '.join(cmd)}", file=sys.stderr)
print(
f"{FAILURE_EMOJI} Error executing command: {' '.join(cmd)}", file=sys.stderr
)
if e.stderr:
print(f"Nix Error Output:\n{e.stderr.strip()}", file=sys.stderr)
raise TestRunnerError("Subprocess command failed.") from e
def _read_flake_override_paths(overrides_path: str | None) -> dict[str, str]:
"""Read flake input override paths from the test wrapper."""
if not overrides_path:
@ -52,7 +61,9 @@ def _read_flake_override_paths(overrides_path: str | None) -> dict[str, str]:
with open(overrides_path, encoding="utf-8") as overrides_file:
overrides = json.load(overrides_file)
except OSError as e:
raise TestRunnerError(f"Failed to read input overrides: {overrides_path}") from e
raise TestRunnerError(
f"Failed to read input overrides: {overrides_path}"
) from e
except json.JSONDecodeError as e:
raise TestRunnerError(f"Invalid input overrides: {overrides_path}") from e
@ -65,6 +76,7 @@ def _read_flake_override_paths(overrides_path: str | None) -> dict[str, str]:
return overrides
def _format_flake_override_args(overrides: dict[str, str]) -> list[str]:
"""Format flake input overrides as Nix CLI arguments."""
nix_args = []
@ -72,6 +84,7 @@ def _format_flake_override_args(overrides: dict[str, str]) -> list[str]:
nix_args.extend(["--override-input", name, path])
return nix_args
class TestRunner:
"""Manages the discovery and execution of Nix-based tests."""
@ -101,12 +114,17 @@ class TestRunner:
nix_apply_expr = (
'pkgs: builtins.concatStringsSep "\\n" '
f'(builtins.filter (name: builtins.match "{test_prefix}.*" name != null) '
'(builtins.attrNames pkgs))'
"(builtins.attrNames pkgs))"
)
cmd = [
"nix", "eval", "--raw", *self._flake_overrides,
f".#legacyPackages.{system}", "--apply", nix_apply_expr
"nix",
"eval",
"--raw",
*self._flake_overrides,
f".#legacyPackages.{system}",
"--apply",
nix_apply_expr,
]
result = _run_command(cmd, cwd=self.repo_root)
discovered = result.stdout.splitlines()
@ -124,10 +142,7 @@ class TestRunner:
if not big_only:
return discovered
discovered_suffixes = {
test.removeprefix(test_prefix)
for test in discovered
}
discovered_suffixes = {test.removeprefix(test_prefix) for test in discovered}
modules_root = self.repo_root / "tests" / "modules"
big_test_suffixes = set()
@ -140,7 +155,9 @@ class TestRunner:
if "config.test.enableBig" not in content:
continue
name_parts = list(module_path.relative_to(modules_root).with_suffix("").parts)
name_parts = list(
module_path.relative_to(modules_root).with_suffix("").parts
)
if name_parts and name_parts[-1] == "default":
name_parts = name_parts[:-1]
if not name_parts:
@ -153,7 +170,8 @@ class TestRunner:
break
return [
test for test in discovered
test
for test in discovered
if test.removeprefix(test_prefix) in big_test_suffixes
]
@ -169,7 +187,11 @@ class TestRunner:
return []
fzf_input = "\n".join(tests)
cmd = ["fzf", "--multi", "--header=Select tests (TAB to select, ENTER to confirm)"]
cmd = [
"fzf",
"--multi",
"--header=Select tests (TAB to select, ENTER to confirm)",
]
try:
result = _run_command(cmd, text_input=fzf_input)
@ -182,8 +204,15 @@ class TestRunner:
"""Retrieve the store path of a test."""
try:
store_cmd = [
"nix", "build", "--no-link", "--json", "--reference-lock-file", "flake.lock",
*self._flake_overrides, f"./tests#{test}", *nix_args
"nix",
"build",
"--no-link",
"--json",
"--reference-lock-file",
"flake.lock",
*self._flake_overrides,
f"./tests#{test}",
*nix_args,
]
result = _run_command(store_cmd, cwd=self.repo_root, check=False)
if result.returncode == 0:
@ -207,8 +236,15 @@ class TestRunner:
for i, test in enumerate(tests_to_run, 1):
print(f"\n--- Running test {i}/{count}: {test} ---")
cmd = [
"nix", "build", "-L", "--keep-failed", "--reference-lock-file", "flake.lock",
*self._flake_overrides, f"./tests#{test}", *nix_args
"nix",
"build",
"-L",
"--keep-failed",
"--reference-lock-file",
"flake.lock",
*self._flake_overrides,
f"./tests#{test}",
*nix_args,
]
try:
subprocess.run(cmd, check=True, cwd=self.repo_root, capture_output=True)
@ -216,7 +252,10 @@ class TestRunner:
store_path = self._get_store_path(test, nix_args)
if store_path:
print(f"{INFO_EMOJI} Test directory available at: {store_path}/tested/", file=sys.stderr)
print(
f"{INFO_EMOJI} Test directory available at: {store_path}/tested/",
file=sys.stderr,
)
except subprocess.CalledProcessError as e:
failed_tests.append(test)
@ -228,27 +267,42 @@ class TestRunner:
print(stderr_text, file=sys.stderr)
import re
if stderr_text:
build_dir_match = re.search(r"keeping build directory '([^']+)'", stderr_text)
build_dir_match = re.search(
r"keeping build directory '([^']+)'", stderr_text
)
if build_dir_match:
build_dir = build_dir_match.group(1)
try:
import glob
attr_files = glob.glob(f"{build_dir}/.attr-*")
for attr_file in attr_files:
with open(attr_file, 'r') as f:
with open(attr_file, "r") as f:
content = f.read()
tested_match = re.search(r'TESTED="([^"]+)"', content)
tested_match = re.search(
r'TESTED="([^"]+)"', content
)
if tested_match:
tested_path = tested_match.group(1)
print(f"{INFO_EMOJI} Generated test directory at: {tested_path}/", file=sys.stderr)
print(
f"{INFO_EMOJI} Generated test directory at: {tested_path}/",
file=sys.stderr,
)
break
except Exception:
print(f"{INFO_EMOJI} Build directory available at: {build_dir}", file=sys.stderr)
print(
f"{INFO_EMOJI} Build directory available at: {build_dir}",
file=sys.stderr,
)
store_path = self._get_store_path(test, nix_args)
if store_path:
print(f"{INFO_EMOJI} Test directory available at: {store_path}/tested/", file=sys.stderr)
print(
f"{INFO_EMOJI} Test directory available at: {store_path}/tested/",
file=sys.stderr,
)
print("\n--- Summary ---")
if not failed_tests:
@ -260,6 +314,7 @@ class TestRunner:
print(f" - {test}")
return False
def main() -> None:
"""Main entry point for the test runner script."""
parser = argparse.ArgumentParser(
@ -285,36 +340,50 @@ def main() -> None:
Run integration tests interactively.
%(prog)s -- --show-trace
Pass '--show-trace' to all 'nix build' commands.
""")
"""),
)
parser.add_argument(
'-l', '--list', action='store_true', help='List available tests instead of running them.'
"-l",
"--list",
action="store_true",
help="List available tests instead of running them.",
)
parser.add_argument(
'-i', '--interactive', action='store_true', help='Force interactive test selection using fzf.'
"-i",
"--interactive",
action="store_true",
help="Force interactive test selection using fzf.",
)
parser.add_argument(
'-t', '--integration', action='store_true', help='Discover and run integration tests.'
"-t",
"--integration",
action="store_true",
help="Discover and run integration tests.",
)
parser.add_argument(
'--big-only', action='store_true', help='Only run tests enabled by `test.enableBig`.'
"--big-only",
action="store_true",
help="Only run tests enabled by `test.enableBig`.",
)
parser.add_argument(
'filters', nargs='*', help='Filter tests by name (partial matches work).'
"filters", nargs="*", help="Filter tests by name (partial matches work)."
)
parser.add_argument(
'nix_args', nargs=argparse.REMAINDER,
help="Arguments to pass to 'nix build', must be after '--'."
"nix_args",
nargs=argparse.REMAINDER,
help="Arguments to pass to 'nix build', must be after '--'.",
)
args = parser.parse_args()
# Strip the '--' if it exists
nix_args = [arg for arg in args.nix_args if arg != '--']
nix_args = [arg for arg in args.nix_args if arg != "--"]
runner = TestRunner()
try:
print(f"{INFO_EMOJI} Discovering tests...", file=sys.stderr)
all_tests = runner.discover_tests(integration=args.integration, big_only=args.big_only)
all_tests = runner.discover_tests(
integration=args.integration, big_only=args.big_only
)
if not all_tests:
print("No tests found for the current configuration.", file=sys.stderr)
sys.exit(1)
@ -326,7 +395,10 @@ def main() -> None:
if args.list:
print("\n".join(tests_to_consider))
print(f"\n{INFO_EMOJI} Found {len(tests_to_consider)} matching tests.", file=sys.stderr)
print(
f"\n{INFO_EMOJI} Found {len(tests_to_consider)} matching tests.",
file=sys.stderr,
)
return
# Determine which tests to run
@ -343,5 +415,6 @@ def main() -> None:
# Error messages are printed by the functions that raise the exception
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -24,6 +24,13 @@ options = [
]
includes = [ "*.nix" ]
[formatter.ruff]
command = "ruff"
options = [ "format" ]
includes = [ "*.py" ]
# Test fixtures are sample inputs (some intentionally invalid Python).
excludes = [ "tests/modules/**/*.py" ]
[formatter.keep-sorted]
command = "keep-sorted"
includes = [ "*" ]