diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..6a57af9 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,28 @@ +name: Pack Validation CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install Test Dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + + - name: Run Pack Verification Suite + run: python -m pytest tests/ -v diff --git a/tests/test_pack.py b/tests/test_pack.py new file mode 100644 index 0000000..b561d45 --- /dev/null +++ b/tests/test_pack.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +import builtins +import json +import keyword +from pathlib import Path +import pytest + +# --------------------------------------------------------------------------- +# Dynamic Discovery: Find the pack file in this specific repository +# --------------------------------------------------------------------------- + + +def find_pack_file() -> Path: + """Scans the repository root for a Foreignthon language pack JSON.""" + repo_root = Path(__file__).parent.parent + + for path in repo_root.rglob("*.json"): + # Ignore virtual environments, cache files, and CI directories + ignored_dirs = {".venv", "venv", ".git", ".pytest_cache", ".gitea", ".github"} + if any(part in path.parts for part in ignored_dirs): + continue + + # Peek inside the JSON to see if it's a Foreignthon pack + try: + with open(path, encoding="utf-8") as f: + content = json.load(f) + if "meta" in content and "keywords" in content: + return path + except (json.JSONDecodeError, KeyError, TypeError): + continue + + raise FileNotFoundError( + "Could not find a valid Foreignthon language pack JSON file in this repository." + ) + + +# --------------------------------------------------------------------------- +# Global Validation Sets +# --------------------------------------------------------------------------- +VALID_KEYWORDS = set(keyword.kwlist) | {"True", "False", "None"} +VALID_BUILTINS = set(dir(builtins)) + +# --------------------------------------------------------------------------- +# The Universal Test Suite +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def pack_data() -> tuple[Path, dict]: + """Loads the dynamically discovered pack file once for the test module.""" + path = find_pack_file() + with open(path, encoding="utf-8") as f: + return path, json.load(f) + + +def test_meta_section_integrity(pack_data): + path, data = pack_data + assert "meta" in data, f"Missing 'meta' section in {path.name}" + meta = data["meta"] + + assert meta.get("name"), "meta.name cannot be empty" + assert meta.get("native_name"), "meta.native_name cannot be empty" + assert meta.get("code"), "meta.code cannot be empty" + + # Enforce that the file name matches the language code (e.g., es.json matches code 'es') + assert ( + path.stem == meta["code"] + ), f"Filename '{path.name}' must exactly match meta.code '{meta['code']}'" + + +def test_keywords_integrity(pack_data): + path, data = pack_data + assert "keywords" in data, f"Missing 'keywords' section in {path.name}" + + for foreign_word, python_keyword in data["keywords"].items(): + assert python_keyword in VALID_KEYWORDS, ( + f"Invalid target '{python_keyword}' for foreign keyword '{foreign_word}' in {path.name}. " + "It is not a recognized Python keyword." + ) + + +def test_builtins_and_exceptions_integrity(pack_data): + path, data = pack_data + + # Validate Builtins + assert "builtins" in data, f"Missing 'builtins' section in {path.name}" + for foreign_word, python_builtin in data["builtins"].items(): + assert ( + python_builtin in VALID_BUILTINS + ), f"Invalid target '{python_builtin}' for builtin function '{foreign_word}' in {path.name}." + + # Validate Exceptions + assert "exceptions" in data, f"Missing 'exceptions' section in {path.name}" + for foreign_word, python_exception in data["exceptions"].items(): + assert ( + python_exception in VALID_BUILTINS + ), f"Invalid target '{python_exception}' for exception '{foreign_word}' in {path.name}." + + +def test_postfix_keywords_integrity(pack_data): + path, data = pack_data + assert ( + "postfix_keywords" in data + ), f"Missing 'postfix_keywords' section in {path.name}" + assert isinstance( + data["postfix_keywords"], list + ), "'postfix_keywords' must be an array/list" + + # Ensure any declared postfix keyword is actually registered in the main keywords dictionary + for post_kw in data["postfix_keywords"]: + assert ( + post_kw in data["keywords"] + ), f"Postfix keyword '{post_kw}' is missing from the main 'keywords' mapping in {path.name}."