PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
 
Loading...
Searching...
No Matches
audit_starter_content.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Certify that shipped starter templates and reusable configuration profiles stay usable."""
3
4from __future__ import annotations
5
6import json
7import subprocess
8import sys
9import tempfile
10from pathlib import Path
11from typing import Optional
12
13import yaml
14
15
16REPO_ROOT = Path(__file__).resolve().parents[2]
17CONTRACT_PATH = REPO_ROOT / "tests" / "tooling" / "starter_content_contract.json"
18PICURV = REPO_ROOT / "picurv_cli" / "picurv"
19RUNTIME_EXECUTION_EXAMPLE = "execution.example.yml"
20
21
22def run_cli(args: list[str], cwd: Path = REPO_ROOT) -> subprocess.CompletedProcess[str]:
23 """!
24 @brief Run one CLI command used to validate a shipped starter artifact.
25 @param[in] args CLI argument list.
26 @param[in] cwd Working directory for the command.
27 @return Completed CLI process.
28 """
29 return subprocess.run(
30 [sys.executable, str(PICURV), *args], cwd=cwd, text=True, capture_output=True, timeout=90, check=False
31 )
32
33
34def fail(context: str, result: subprocess.CompletedProcess[str]) -> None:
35 """!
36 @brief Raise a compact error that preserves the relevant CLI output.
37 @param[in] context Human-readable operation description.
38 @param[in] result Completed failing CLI process.
39 @return None.
40 """
41 raise RuntimeError(f"{context} failed:\n{result.stdout}\n{result.stderr}")
42
43
44def validate_bundle(bundle: dict[str, str], label: str) -> None:
45 """!
46 @brief Validate one declared case/solver/monitor/post or cluster/study composition.
47 @param[in] bundle Declared role-to-path mapping.
48 @param[in] label Human-readable bundle description.
49 @return None.
50 """
51 args = ["validate"]
52 for role, path in bundle.items():
53 args.extend([f"--{role}", str(REPO_ROOT / path)])
54 result = run_cli(args)
55 if result.returncode:
56 fail(label, result)
57
58
59def _yaml_role(path: Path) -> Optional[str]:
60 """!
61 @brief Classify a starter YAML by the same stable top-level shapes used by init.
62 @param[in] path Source YAML path.
63 @return Canonical configuration role, or None for unclassified YAML.
64 """
65 payload = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
66 if not isinstance(payload, dict):
67 return None
68 keys = set(payload)
69 if {"grid", "properties", "run_control"} <= keys:
70 return "case"
71 if "base_configs" in keys and ("study_type" in keys or "parameters" in keys or "parameter_sets" in keys):
72 return "study"
73 if "scheduler" in keys and "resources" in keys:
74 return "cluster"
75 if "source_data" in keys or "eulerian_pipeline" in keys or "lagrangian_pipeline" in keys:
76 return "post"
77 if "io" in keys and ("logging" in keys or "profiling" in keys or "diagnostics" in keys):
78 return "monitor"
79 if "momentum_solver" in keys or "poisson_solver" in keys or "operation_mode" in keys:
80 return "solver"
81 return None
82
83
84def audit_template_copy(template_name: str, temporary_root: Path) -> None:
85 """!
86 @brief Initialize one declared template and verify canonical relocation preserves its content.
87 @param[in] template_name Top-level example directory name.
88 @param[in] temporary_root Temporary parent directory for initialized cases.
89 @return None.
90 """
91 source = REPO_ROOT / "examples" / template_name
92 destination = temporary_root / template_name
93 result = run_cli(["init", template_name, "--dest", str(destination)])
94 if result.returncode:
95 fail(f"picurv init {template_name}", result)
96 required_dirs = (
97 "config", "config/studies", "inputs", "assets/objects", "assets/sets", "runs", "studies"
98 )
99 for relative in required_dirs:
100 if not (destination / relative).is_dir():
101 raise RuntimeError(f"picurv init {template_name} omitted workspace directory {relative}")
102 if not (destination / ".picurv-workspace.yml").is_file():
103 raise RuntimeError(f"picurv init {template_name} omitted .picurv-workspace.yml")
104
105 source_yamls = [path for path in source.rglob("*.yml") if path.name != RUNTIME_EXECUTION_EXAMPLE]
106 initialized_yamls = list((destination / "config").rglob("*.yml"))
107 if len(initialized_yamls) < len(source_yamls):
108 raise RuntimeError(f"picurv init {template_name} lost one or more YAML configurations")
109
110 for source_path in source.rglob("*"):
111 if not source_path.is_file():
112 continue
113 relative = source_path.relative_to(source)
114 if relative.as_posix() == RUNTIME_EXECUTION_EXAMPLE:
115 continue
116 if source_path.suffix.lower() in {".yml", ".yaml"}:
117 role = _yaml_role(source_path)
118 stem = source_path.stem
119 candidates = [
120 destination / "config" / source_path.name,
121 destination / "config" / f"{role}.yml" if role else destination / source_path.name,
122 destination / "config" / f"{role}-{stem}{source_path.suffix}" if role else destination / source_path.name,
123 destination / "config" / "studies" / source_path.name,
124 ]
125 if not any(candidate.is_file() for candidate in candidates):
126 raise RuntimeError(f"picurv init {template_name} did not relocate YAML {relative}")
127 continue
128 matches = [path for path in destination.rglob(source_path.name) if path.is_file()]
129 if not any(path.read_bytes() == source_path.read_bytes() for path in matches):
130 raise RuntimeError(f"picurv init {template_name} did not preserve {relative}")
131 if (destination / RUNTIME_EXECUTION_EXAMPLE).exists():
132 raise RuntimeError(f"picurv init {template_name} copied site-specific {RUNTIME_EXECUTION_EXAMPLE}")
133
134
135def main() -> int:
136 """!
137 @brief Run the starter-content inventory, composition, and initializer audit.
138 @return Process status code.
139 """
140 contract = json.loads(CONTRACT_PATH.read_text(encoding="utf-8"))
141 actual_templates = sorted(path.name for path in (REPO_ROOT / "examples").iterdir() if path.is_dir())
142 if actual_templates != sorted(contract["template_directories"]):
143 raise RuntimeError(f"Template inventory differs from contract: actual={actual_templates}")
144 unknown_reference_templates = set(contract["reference_only_templates"]) - set(actual_templates)
145 if unknown_reference_templates:
146 raise RuntimeError(f"Reference-only templates are not in the template inventory: {sorted(unknown_reference_templates)}")
147
148 actual_config_assets = sorted(str(path.relative_to(REPO_ROOT)) for path in (REPO_ROOT / "config").rglob("*") if path.is_file())
149 if actual_config_assets != sorted(contract["config_assets"]):
150 raise RuntimeError("Configuration asset inventory differs from the starter-content contract.")
151
152 declared_example_yamls = set(contract["auxiliary_example_yamls"])
153 for bundle in contract["case_bundles"] + contract["study_bundles"]:
154 declared_example_yamls.update(path for path in bundle.values() if path.startswith("examples/"))
155 actual_example_yamls = {str(path.relative_to(REPO_ROOT)) for path in (REPO_ROOT / "examples").rglob("*.yml")}
156 if actual_example_yamls != declared_example_yamls:
157 raise RuntimeError("Example YAML inventory differs from the declared runnable/reference compositions.")
158
159 for bundle in contract["case_bundles"]:
160 validate_bundle(bundle, f"case bundle {bundle['case']}")
161 for bundle in contract["config_role_bundles"]:
162 validate_bundle(bundle, f"config role bundle {bundle['solver']}")
163 for bundle in contract["study_bundles"]:
164 validate_bundle(bundle, f"study bundle {bundle['study']}")
165
166 with tempfile.TemporaryDirectory(prefix="picurv-starter-content-") as temporary_directory:
167 temporary_root = Path(temporary_directory)
168 for template_name in contract["template_directories"]:
169 audit_template_copy(template_name, temporary_root)
170
171 print("Starter template, example, and configuration audit passed.")
172 return 0
173
174
175if __name__ == "__main__":
176 try:
177 raise SystemExit(main())
178 except (RuntimeError, subprocess.TimeoutExpired) as error:
179 print(f"Starter-content audit failed: {error}", file=sys.stderr)
180 raise SystemExit(1)
None audit_template_copy(str template_name, Path temporary_root)
Initialize one declared template and verify canonical relocation preserves its content.
None fail(str context, subprocess.CompletedProcess[str] result)
Raise a compact error that preserves the relevant CLI output.
subprocess.CompletedProcess[str] run_cli(list[str] args, Path cwd=REPO_ROOT)
Run one CLI command used to validate a shipped starter artifact.
int main()
Run the starter-content inventory, composition, and initializer audit.
None validate_bundle(dict[str, str] bundle, str label)
Validate one declared case/solver/monitor/post or cluster/study composition.
Optional[str] _yaml_role(Path path)
Classify a starter YAML by the same stable top-level shapes used by init.
Head of a generic C-style linked list.
Definition variables.h:475