import hashlib import importlib.util import json import tempfile import unittest from pathlib import Path def load_validate_reference_artifacts(): module_path = Path(__file__).resolve().parent / "validate_reference_artifacts.py" spec = importlib.util.spec_from_file_location("validate_reference_artifacts", module_path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def write_json(path: Path, payload: dict): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(payload, indent=2), encoding="utf-8") def write_text(path: Path, text: str = "ok\n"): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text, encoding="utf-8") def ready_metadata(source_hash: str) -> dict: return { "schema_version": "abaqus-user-subroutine-artifact-v1", "feature_id": "umat", "model_id": "single-element", "artifact_status": "ready-for-comparison", "abaqus": {"version": "2024", "precision": "double"}, "compiler": {"vendor": "Intel oneAPI", "name": "ifx", "version": "2024"}, "subroutine": { "entry_points": ["UMAT"], "source_files": [ { "path": "src/fortran/abaqus/UMAT.for", "language": "Fortran", "sha256": source_hash, } ], }, "input_file": "model.inp", "outputs": { "tails": { "msg": "job.msg.tail.txt", "dat": "job.dat.tail.txt", "log": "job.log.tail.txt", "sta": "job.sta.tail.txt", }, "csv": {"stresses": "extracted/stresses.csv"}, }, "extraction": { "source_odb": "job.odb", "tool": "Abaqus Python", "extracted_at": "2026-06-10T00:00:00+09:00", "csv_directory": "extracted", "script": "extraction/extract_odb_to_csv.py", }, } def create_ready_bundle(root: Path) -> tuple[Path, dict]: source = root / "src" / "fortran" / "abaqus" / "UMAT.for" write_text(source, " subroutine umat()\n end\n") source_hash = hashlib.sha256(source.read_bytes()).hexdigest() model_dir = root / "references" / "umat" / "single-element" for name in [ "model.inp", "job.msg.tail.txt", "job.dat.tail.txt", "job.log.tail.txt", "job.sta.tail.txt", "extracted/stresses.csv", "extraction/extract_odb_to_csv.py", ]: write_text(model_dir / name) return model_dir, ready_metadata(source_hash) class ValidateReferenceArtifactsTests(unittest.TestCase): def test_missing_references_directory_is_valid(self): validator = load_validate_reference_artifacts() with tempfile.TemporaryDirectory() as tmp: self.assertEqual(validator.validate_root(Path(tmp)), []) def test_draft_metadata_with_minimal_provenance_is_valid(self): validator = load_validate_reference_artifacts() with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) write_json( root / "references" / "umat" / "single-element" / "metadata.json", { "schema_version": "abaqus-user-subroutine-artifact-v1", "feature_id": "umat", "model_id": "single-element", "artifact_status": "draft", }, ) self.assertEqual(validator.validate_root(root), []) def test_ready_for_comparison_requires_declared_files(self): validator = load_validate_reference_artifacts() with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) write_json( root / "references" / "umat" / "single-element" / "metadata.json", { "schema_version": "abaqus-user-subroutine-artifact-v1", "feature_id": "umat", "model_id": "single-element", "artifact_status": "ready-for-comparison", "abaqus": {"version": "2024", "precision": "double"}, "compiler": {"vendor": "Intel oneAPI", "name": "ifx", "version": "2024"}, "subroutine": { "entry_points": ["UMAT"], "source_files": [{"path": "src/fortran/abaqus/UMAT.for", "language": "Fortran", "sha256": "abc"}], }, "input_file": "model.inp", "outputs": { "tails": {"msg": "job.msg.tail.txt", "dat": "job.dat.tail.txt", "log": "job.log.tail.txt"}, "csv": {"stresses": "extracted/stresses.csv"}, }, }, ) errors = validator.validate_root(root) self.assertTrue(any("missing input_file" in error for error in errors)) self.assertTrue(any("missing output tail sta" in error for error in errors)) self.assertTrue(any("missing ready-for-comparison key extraction" in error for error in errors)) self.assertTrue(any("missing csv output" in error for error in errors)) self.assertTrue(any("missing source file" in error for error in errors)) def test_ready_for_comparison_accepts_external_bundle_without_abaqus_command(self): validator = load_validate_reference_artifacts() with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) model_dir, payload = create_ready_bundle(root) payload["extraction"]["odb_sha256_file"] = "result.odb.sha256" write_text(model_dir / "result.odb.sha256", "0" * 64 + " job.odb\n") write_json(model_dir / "metadata.json", payload) self.assertEqual(validator.validate_root(root), []) def test_ready_for_comparison_checks_source_sha256(self): validator = load_validate_reference_artifacts() with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) model_dir, payload = create_ready_bundle(root) payload["subroutine"]["source_files"][0]["sha256"] = "0" * 64 write_json(model_dir / "metadata.json", payload) errors = validator.validate_root(root) self.assertTrue(any("sha256 mismatch" in error for error in errors)) def test_ready_for_comparison_rejects_csv_outside_extracted_directory(self): validator = load_validate_reference_artifacts() with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) model_dir, payload = create_ready_bundle(root) payload["outputs"]["csv"] = {"stresses": "stresses.csv"} write_text(model_dir / "stresses.csv") write_json(model_dir / "metadata.json", payload) errors = validator.validate_root(root) self.assertTrue(any("csv output stresses must match extracted/*.csv" in error for error in errors)) def test_ready_for_comparison_checks_optional_odb_sha256_file(self): validator = load_validate_reference_artifacts() with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) model_dir, payload = create_ready_bundle(root) payload["extraction"]["odb_sha256_file"] = "result.odb.sha256" write_text(model_dir / "result.odb.sha256", "not-a-sha\n") write_json(model_dir / "metadata.json", payload) errors = validator.validate_root(root) self.assertTrue(any("invalid odb_sha256_file" in error for error in errors)) if __name__ == "__main__": unittest.main()