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
23 changes: 23 additions & 0 deletions configs/exit_code_registry.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"description": "OpenAMP Foundry CLI exit code registry",
"codes": {
"0": {"meaning": "success", "description": "Command completed successfully"},
"1": {"meaning": "internal_error", "description": "Unexpected error during execution"},
"2": {"meaning": "input_error", "description": "Missing or invalid input (file not found, bad arguments)"},
"3": {"meaning": "validation_failure", "description": "Validation or gate check failed (data did not pass criteria)"}
},
"command_patterns": {
"rank": {"expected": [0, 2], "notes": "0=success, 2=missing input"},
"validate": {"expected": [0, 2], "notes": "0=valid, 2=missing input"},
"bench leakage": {"expected": [0, 2], "notes": "0=success, 2=missing input"},
"pilot-panel": {"expected": [0, 2], "notes": "0=success, 2=missing input"},
"calibration-intake": {"expected": [0, 2], "notes": "0=success, 2=missing input"},
"check_wave1_pass_fail.py": {"expected": [0, 2, 3], "notes": "0=all pass, 2=input error, 3=criteria fail"},
"validate_lab_data_return.py": {"expected": [0, 2, 3], "notes": "0=all valid, 2=input error, 3=validation fail"},
"validate_negative_archive.py": {"expected": [0, 2, 3], "notes": "0=all valid, 2=input error, 3=validation fail"},
"validate_panel_csv.py": {"expected": [0, 2, 3], "notes": "0=valid, 2=input error, 3=invalid"},
"check_claims.py": {"expected": [0, 3], "notes": "0=ok, 3=findings (advisory)"},
"check_doc_links.py": {"expected": [0, 3], "notes": "0=ok, 3=broken links found"},
"check_pyproject.py": {"expected": [0, 2, 3], "notes": "0=ok, 2=input error, 3=inconsistency found"}
}
}
93 changes: 93 additions & 0 deletions scripts/check_pyproject.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Check pyproject.toml consistency with the actual package."""

from __future__ import annotations

import importlib
import sys
import tomllib
from pathlib import Path


def check_pyproject(pyproject_path: str = "pyproject.toml") -> dict:
p = Path(pyproject_path)
if not p.exists():
return {"error": f"pyproject.toml not found: {pyproject_path}"}

with p.open("rb") as f:
data = tomllib.load(f)

issues = []
checks = {}

# 1. Check Python version requirement
requires_python = data.get("project", {}).get("requires-python", "")
checks["python_requirement"] = requires_python

# 2. Check console scripts entrypoints
scripts = data.get("project", {}).get("scripts", {})
entrypoint_issues = []
for name, ref in scripts.items():
module_path = ref.split(":")[0]
try:
importlib.import_module(module_path)
except ImportError as e:
entrypoint_issues.append(f"Entrypoint '{name}' -> '{ref}': {e}")
if entrypoint_issues:
issues.extend(entrypoint_issues)
checks["entrypoints_ok"] = len(entrypoint_issues) == 0
checks["entrypoint_issues"] = entrypoint_issues

# 3. Check optional extras reference valid dependencies
optional_deps = data.get("project", {}).get("optional-dependencies", {})
extras_ok = len(optional_deps) > 0
checks["extras_defined"] = extras_ok
checks["extras_count"] = len(optional_deps)

# 4. Check project name matches package
pkg_name = data.get("project", {}).get("name", "")
# Convert hyphens to underscores for import check
import_name = pkg_name.replace("-", "_")
try:
importlib.import_module(import_name)
checks["package_importable"] = True
except ImportError:
checks["package_importable"] = False
issues.append(f"Package '{import_name}' cannot be imported")

return {
"pyproject": pyproject_path,
"project_name": pkg_name,
"python_requirement": requires_python,
"entrypoints": len(scripts),
"entrypoints_ok": checks["entrypoints_ok"],
"extras": len(optional_deps),
"issues": issues,
"all_ok": len(issues) == 0,
}


def main() -> int:
import argparse
parser = argparse.ArgumentParser(description="Check pyproject.toml consistency")
parser.add_argument("--pyproject", default="pyproject.toml")
args = parser.parse_args()

result = check_pyproject(args.pyproject)
if "error" in result:
print(result["error"], file=sys.stderr)
return 2

print(f"Project: {result['project_name']}")
print(f"Python: {result['python_requirement']}")
eok = result.get("entrypoints_ok", "?")
print(f"Entrypoints: {result['entrypoints']}, OK: {eok}")
print(f"Extras: {result['extras']}")
print(f"All OK: {result['all_ok']}")
for issue in result["issues"]:
print(f" ❌ {issue}")

return 0 if result["all_ok"] else 3


if __name__ == "__main__":
sys.exit(main())
46 changes: 46 additions & 0 deletions tests/test_check_pyproject.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Tests for pyproject consistency checker."""
import subprocess
import sys
import tomllib
from pathlib import Path
import pytest

from scripts.check_pyproject import check_pyproject


def test_check_runs():
r = check_pyproject()
assert "project_name" in r
assert r["project_name"]


def test_python_requirement_present():
r = check_pyproject()
assert r["python_requirement"] != ""


def test_entrypoints_defined():
r = check_pyproject()
assert r["entrypoints"] > 0


def test_extras_defined():
r = check_pyproject()
assert r["extras"] >= 1


def test_cli_exit():
r = subprocess.run([sys.executable, "scripts/check_pyproject.py"],
capture_output=True, text=True, env={"PYTHONPATH": "src"})
assert r.returncode == 0


def test_pyproject_is_valid_toml():
data = tomllib.loads(Path("pyproject.toml").read_text())
assert "project" in data
assert data["project"]["name"] == "openamp-foundry"


def test_missing_file():
r = check_pyproject("/nonexistent.toml")
assert "error" in r
46 changes: 46 additions & 0 deletions tests/test_exit_code_registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Tests for CLI exit code registry."""
import json
from pathlib import Path


def _registry():
return json.loads(Path("configs/exit_code_registry.json").read_text())


def test_file_exists():
assert Path("configs/exit_code_registry.json").exists()


def test_is_valid_json():
r = _registry()
assert "codes" in r
assert "command_patterns" in r


def test_exit_codes_defined():
r = _registry()
for code_str, info in r["codes"].items():
assert "meaning" in info
assert "description" in info
assert isinstance(int(code_str), int)


def test_rank_has_expected_codes():
r = _registry()
assert "rank" in r["command_patterns"]
assert 0 in r["command_patterns"]["rank"]["expected"]


def test_check_scripts_have_exit_3():
r = _registry()
for name, info in r["command_patterns"].items():
if name.endswith(".py"):
assert 3 in info["expected"], f"{name} should have exit code 3"


def test_all_expected_codes_are_defined():
r = _registry()
defined = set(int(k) for k in r["codes"])
for name, info in r["command_patterns"].items():
for code in info["expected"]:
assert code in defined, f"{name} expects code {code} which is not defined"
Loading