2"""Certify that shipped starter templates and reusable configuration profiles stay usable."""
4from __future__
import annotations
10from pathlib
import Path
11from typing
import Optional
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"
22def run_cli(args: list[str], cwd: Path = REPO_ROOT) -> subprocess.CompletedProcess[str]:
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.
29 return subprocess.run(
30 [sys.executable, str(PICURV), *args], cwd=cwd, text=
True, capture_output=
True, timeout=90, check=
False
34def fail(context: str, result: subprocess.CompletedProcess[str]) ->
None:
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.
41 raise RuntimeError(f
"{context} failed:\n{result.stdout}\n{result.stderr}")
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.
52 for role, path
in bundle.items():
53 args.extend([f
"--{role}", str(REPO_ROOT / path)])
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.
65 payload = yaml.safe_load(path.read_text(encoding=
"utf-8"))
or {}
66 if not isinstance(payload, dict):
69 if {
"grid",
"properties",
"run_control"} <= keys:
71 if "base_configs" in keys
and (
"study_type" in keys
or "parameters" in keys
or "parameter_sets" in keys):
73 if "scheduler" in keys
and "resources" in keys:
75 if "source_data" in keys
or "eulerian_pipeline" in keys
or "lagrangian_pipeline" in keys:
77 if "io" in keys
and (
"logging" in keys
or "profiling" in keys
or "diagnostics" in keys):
79 if "momentum_solver" in keys
or "poisson_solver" in keys
or "operation_mode" in keys:
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.
91 source = REPO_ROOT /
"examples" / template_name
92 destination = temporary_root / template_name
93 result =
run_cli([
"init", template_name,
"--dest", str(destination)])
95 fail(f
"picurv init {template_name}", result)
97 "config",
"config/studies",
"inputs",
"assets/objects",
"assets/sets",
"runs",
"studies"
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")
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")
110 for source_path
in source.rglob(
"*"):
111 if not source_path.is_file():
113 relative = source_path.relative_to(source)
114 if relative.as_posix() == RUNTIME_EXECUTION_EXAMPLE:
116 if source_path.suffix.lower()
in {
".yml",
".yaml"}:
118 stem = source_path.stem
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,
125 if not any(candidate.is_file()
for candidate
in candidates):
126 raise RuntimeError(f
"picurv init {template_name} did not relocate YAML {relative}")
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}")
137 @brief Run the starter-content inventory, composition, and initializer audit.
138 @return Process status code.
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)}")
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.")
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.")
159 for bundle
in contract[
"case_bundles"]:
161 for bundle
in contract[
"config_role_bundles"]:
163 for bundle
in contract[
"study_bundles"]:
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"]:
171 print(
"Starter template, example, and configuration audit passed.")
175if __name__ ==
"__main__":
177 raise SystemExit(
main())
178 except (RuntimeError, subprocess.TimeoutExpired)
as error:
179 print(f
"Starter-content audit failed: {error}", file=sys.stderr)
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.