diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b6fbaec6..94cf74eac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,6 +12,27 @@ permissions: contents: read jobs: + # Determine which tests to run based on changed files + changes: + runs-on: ubuntu-latest + outputs: + schema-generation: ${{ steps.filter.outputs.schema-generation }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + schema-generation: + - 'src/allotropy/allotrope/schemas/**' + - 'src/allotropy/allotrope/schema_parser/**' + - 'src/allotropy/allotrope/models/**' + - 'src/allotropy/allotrope/schemas.py' + - 'src/allotropy/exceptions.py' + - 'scripts/generate_schemas.py' + - 'scripts/download_schema.py' + - 'tests/allotrope/schema_parser/**' + - 'pyproject.toml' test_py_310: runs-on: ubuntu-latest name: Tests (python 3.10) @@ -27,9 +48,9 @@ jobs: run: pip install "hatch>=1.13.0" - name: Install click run: pip install click!=8.3.0 - - name: Run Tests - run: hatch run test_all.py3.10:pytest -n 2 tests - timeout-minutes: 15 + - name: Run Tests (excluding schema generation) + run: hatch run test_all.py3.10:pytest -n 2 tests --ignore=tests/allotrope/schema_parser/generate_schemas_test.py + timeout-minutes: 10 test_py_311: runs-on: ubuntu-latest @@ -47,9 +68,9 @@ jobs: # NOTE: due to bug: https://github.com/pallets/click/issues/3066 - name: Install click run: pip install click!=8.3.0 - - name: Run Tests - run: hatch run test_all.py3.11:pytest -n 2 tests - timeout-minutes: 15 + - name: Run Tests (excluding schema generation) + run: hatch run test_all.py3.11:pytest -n 2 tests --ignore=tests/allotrope/schema_parser/generate_schemas_test.py + timeout-minutes: 10 test_py_312: runs-on: ubuntu-latest @@ -66,9 +87,35 @@ jobs: run: pip install "hatch>=1.13.0" - name: Install click run: pip install click!=8.3.0 - - name: Run Tests - run: hatch run test_all.py3.12:pytest -n 2 tests - timeout-minutes: 15 + - name: Run Tests (excluding schema generation) + run: hatch run test_all.py3.12:pytest -n 2 tests --ignore=tests/allotrope/schema_parser/generate_schemas_test.py + timeout-minutes: 10 + + # Schema generation test - only runs when relevant files change or on main branch + schema_generation_test: + needs: changes + # Always run on main branch, otherwise only when schema files change + if: ${{ github.ref == 'refs/heads/main' || needs.changes.outputs.schema-generation == 'true' }} + runs-on: ubuntu-latest + name: Schema Generation Test + strategy: + matrix: + python-version: ["3.10"] # Only run on one Python version since output is the same + + steps: + - uses: actions/checkout@v4 + with: + lfs: true + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install hatch + run: pip install "hatch>=1.13.0" + - name: Install click + run: pip install click!=8.3.0 + - name: Run Schema Generation Test + run: hatch run test_all.py${{ matrix.python-version }}:pytest tests/allotrope/schema_parser/generate_schemas_test.py -v + timeout-minutes: 5 lint: runs-on: ubuntu-latest diff --git a/src/allotropy/allotrope/schema_parser/backup_manager.py b/src/allotropy/allotrope/schema_parser/backup_manager.py index 4d9cdc890..3117fa522 100644 --- a/src/allotropy/allotrope/schema_parser/backup_manager.py +++ b/src/allotropy/allotrope/schema_parser/backup_manager.py @@ -3,8 +3,7 @@ from itertools import zip_longest from pathlib import Path import shutil - -from allotropy.parsers.utils.uuids import random_uuid_str +import uuid PathType = Path | str @@ -19,7 +18,7 @@ def _files_equal(path1: PathType, path2: PathType) -> bool: def _get_backup_path(path: PathType) -> Path: _path = Path(path) - return Path(_path.parent, f".{_path.stem}.{random_uuid_str()}.bak{_path.suffix}") + return Path(_path.parent, f".{_path.stem}.{uuid.uuid4()!s}.bak{_path.suffix}") def get_original_path(path: PathType) -> Path: diff --git a/src/allotropy/allotrope/schema_parser/model_class_editor.py b/src/allotropy/allotrope/schema_parser/model_class_editor.py index 801cb6d2b..71a7d3bfd 100644 --- a/src/allotropy/allotrope/schema_parser/model_class_editor.py +++ b/src/allotropy/allotrope/schema_parser/model_class_editor.py @@ -17,7 +17,6 @@ get_all_schema_components, get_schema_definitions_mapping, ) -from allotropy.parsers.utils.values import assert_not_none SCHEMA_DIR_PATH = "src/allotropy/allotrope/schemas" SHARED_FOLDER_MODULE = "allotropy.allotrope.models.shared" @@ -196,7 +195,10 @@ def create(contents: str) -> DataclassField: type_string, default_value = ( content.split("=") if "=" in content else (content, None) ) - types = _parse_field_types(assert_not_none(type_string)) + if type_string is None: + msg = f"Expected non-null type string for field {name}." + raise ValueError(msg) + types = _parse_field_types(type_string) return DataclassField(name, default_value, types) diff --git a/src/allotropy/allotrope/schema_parser/path_util.py b/src/allotropy/allotrope/schema_parser/path_util.py index 4cbd6df76..06078a262 100644 --- a/src/allotropy/allotrope/schema_parser/path_util.py +++ b/src/allotropy/allotrope/schema_parser/path_util.py @@ -4,8 +4,6 @@ import re from typing import Any -from allotropy.exceptions import AllotropeValidationError - ALLOTROPE_DIR: Path = Path(__file__).parent.parent ALLOTROPY_DIR: Path = ALLOTROPE_DIR.parent ROOT_DIR: Path = ALLOTROPE_DIR.parent.parent.parent @@ -74,10 +72,10 @@ def get_schema_path_from_manifest(manifest: str) -> Path: def get_schema_path_from_asm(asm_dict: Mapping[str, Any]) -> Path: if "$asm.manifest" not in asm_dict: msg = "File is not valid ASM - missing $asm.manifest field" - raise AllotropeValidationError(msg) + raise ValueError(msg) if not isinstance(asm_dict["$asm.manifest"], str): msg = f"File is not valid ASM - $asm.manifest is not a string: {asm_dict['$asm.manifest']}" - raise AllotropeValidationError(msg) + raise ValueError(msg) return get_schema_path_from_manifest(asm_dict["$asm.manifest"]) diff --git a/src/allotropy/allotrope/schemas.py b/src/allotropy/allotrope/schemas.py index d48a595ee..ec8905f89 100644 --- a/src/allotropy/allotrope/schemas.py +++ b/src/allotropy/allotrope/schemas.py @@ -12,9 +12,10 @@ get_schema_path_from_manifest, SHARED_SCHEMAS_DEFINITIONS_PATH, ) -from allotropy.constants import DEFAULT_ENCODING from allotropy.exceptions import AllotropeSerializationError, AllotropeValidationError +DEFAULT_ENCODING = "UTF-8" + # Override format checker to remove "uri-reference" check, which ASM schemas fail against. FORMAT_CHECKER = copy.deepcopy( jsonschema.validators.Draft202012Validator.FORMAT_CHECKER diff --git a/tests/allotrope/schema_parser/generate_schemas_test.py b/tests/allotrope/schema_parser/generate_schemas_test.py index e60744fdc..e44d4fc90 100644 --- a/tests/allotrope/schema_parser/generate_schemas_test.py +++ b/tests/allotrope/schema_parser/generate_schemas_test.py @@ -1,5 +1,4 @@ from pathlib import Path -import re import pytest @@ -20,33 +19,11 @@ def _get_schema_paths() -> list[Path]: ] -def _pick_subset(paths: list[Path], count: int = 8) -> list[Path]: - if not paths: - return [] - if len(paths) <= count: - return paths - step = max(1, len(paths) // count) - # Evenly sample across the set for broad coverage - return [paths[i] for i in range(0, len(paths), step)][:count] - - -def test_generate_schemas_smoke_subset() -> None: - """Fast smoke test over a representative subset of schemas.""" - paths = _get_schema_paths() - subset = _pick_subset(paths, count=8) - # Build a regex that matches exactly any of the chosen relative schema paths - escaped = [re.escape(str(p)) for p in subset] - pattern = rf"^({'|'.join(escaped)})$" - models_changed = generate_schemas(dry_run=True, schema_regex=pattern) - assert ( - not models_changed - ), f"Expected no models files to have changed by generate-schemas script, found changes in: {models_changed}.\nPlease run 'hatch run scripts:generate-schemas' and validate the changes." - - @pytest.mark.long -def test_generate_schemas_runs_to_completion() -> None: - """Full run once across all schemas. Marked long for optional execution.""" - models_changed = generate_schemas(dry_run=True) +@pytest.mark.parametrize("schema_path", _get_schema_paths()) +def test_generate_schemas_runs_to_completion(schema_path: Path) -> None: + """Test that schema generation produces no unexpected changes.""" + models_changed = generate_schemas(dry_run=True, schema_regex=str(schema_path)) assert ( not models_changed ), f"Expected no models files to have changed by generate-schemas script, found changes in: {models_changed}.\nPlease run 'hatch run scripts:generate-schemas' and validate the changes."