Skip to content

Commit d0cc2dc

Browse files
authored
Merge pull request #43 from PyAutoLabs/feature/howto-smoke-all-tutorials
test: run every script in smoke, not a 10-of-15 allowlist
2 parents 65e8fbd + f0611bc commit d0cc2dc

4 files changed

Lines changed: 54 additions & 109 deletions

File tree

.github/scripts/run_smoke.py

Lines changed: 49 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,74 @@
11
"""
2-
Run the workspace smoke test suite.
3-
4-
Reads `smoke_tests.txt` from the workspace root and `config/build/profile_smoke.yaml`
5-
for per-script env var overrides, then runs each listed script with the
6-
appropriate environment. Continues through failures and exits non-zero
7-
if any script failed.
8-
9-
The env resolution itself is NOT implemented here: it is PyAutoHands's
10-
`autohands/env_config.py`, imported below. This file used to carry a copy, and
11-
the copy had already drifted (its `load_env_config` hardcoded
12-
`config/build/profile_smoke.yaml`, so the PR gate was structurally unable to read
13-
the release profile — the seed incident's failure mode 4/7). One resolver
14-
means the PR gate and the release runner cannot disagree about what a script's
15-
environment is. See PyAutoHands docs/env_profile_redesign.md §5 (#161 step 2).
16-
17-
Mirrors the logic of the `/smoke-test` skill so CI and local runs stay
18-
in sync.
2+
Run the workspace smoke test suite: every script under `scripts/`, minus the
3+
exclusions in `config/build/no_run.yaml`.
4+
5+
Coverage is **opt-out**. A new tutorial is smoke-tested the moment it is added;
6+
excluding one is a deliberate, documented entry in `config/build/no_run.yaml` —
7+
the same file the notebook runner already honours, so scripts and notebooks can
8+
no longer disagree about what is skipped.
9+
10+
This replaces the former `smoke_tests.txt` allowlist, under which a script was
11+
tested only if someone remembered to add it. That design left HowToGalaxy
12+
testing 4 of its 26 scripts, and a public teaching notebook stayed broken in
13+
three places because no job had ever executed it (HowToGalaxy #58).
14+
15+
Nothing about discovery, exclusion or environment resolution is implemented
16+
here. This is a thin shim over PyAutoHands' `autohands/run_python.py` — the same
17+
entry point PyAutoHeart's workspace-validation uses for its `run_scripts` job —
18+
so the PR gate and the validation runner cannot drift apart. That runner
19+
provides:
20+
21+
* recursive discovery, ordering `simulator*` first and then `start_here.py`,
22+
which is what tutorials depending on simulated datasets need
23+
* `should_skip()` against `config/build/no_run.yaml`
24+
* per-script env from `config/build/profile_smoke.yaml`
25+
26+
Mirrors the `/smoke-test` skill so CI and local runs stay in sync.
1927
"""
2028

2129
from __future__ import annotations
2230

31+
import os
2332
import subprocess
2433
import sys
25-
import time
2634
from pathlib import Path
2735

28-
2936
WORKSPACE = Path(__file__).resolve().parents[2]
30-
SMOKE_FILE = WORKSPACE / "smoke_tests.txt"
31-
ENV_VARS_FILE = WORKSPACE / "config" / "build" / "profile_smoke.yaml"
32-
SCRIPTS_DIR = WORKSPACE / "scripts"
37+
PROJECT = "howtofit"
3338

3439
# CI puts PyAutoHands/autohands on PYTHONPATH (PyAutoHeart's reusable
3540
# smoke-tests.yml clones it alongside the dependency chain); for local runs,
3641
# fall back to the sibling checkout.
3742
try:
38-
from env_config import build_env_for_script, load_env_config
43+
import build_util
3944
except ImportError: # pragma: no cover - local-run fallback
4045
sys.path.insert(0, str(WORKSPACE.parent / "PyAutoHands" / "autohands"))
41-
from env_config import build_env_for_script, load_env_config
42-
43-
44-
def load_smoke_scripts() -> list[str]:
45-
scripts: list[str] = []
46-
for line in SMOKE_FILE.read_text().splitlines():
47-
line = line.strip()
48-
if not line or line.startswith("#"):
49-
continue
50-
scripts.append(line)
51-
return scripts
52-
46+
import build_util
5347

54-
def load_cfg() -> dict | None:
55-
"""Parsed env profile, or None when the workspace has none.
56-
57-
None flows through build_env_for_script -> None -> subprocess inherits the
58-
parent environment, which is what the old local copy's empty-config path
59-
did by hand.
60-
"""
61-
if not ENV_VARS_FILE.exists():
62-
return None
63-
return load_env_config(ENV_VARS_FILE)
64-
65-
66-
def run_one(script_rel: str, cfg: dict | None) -> tuple[str, int, float, str]:
67-
env = build_env_for_script(Path(script_rel), cfg)
68-
script_path = SCRIPTS_DIR / script_rel
69-
t0 = time.time()
70-
result = subprocess.run(
71-
[sys.executable, str(script_path)],
72-
cwd=str(WORKSPACE),
73-
env=env,
74-
capture_output=True,
75-
text=True,
76-
)
77-
elapsed = time.time() - t0
78-
output = result.stdout + result.stderr
79-
return script_rel, result.returncode, elapsed, output
48+
AUTOHANDS = Path(build_util.__file__).resolve().parent
8049

8150

8251
def main() -> int:
83-
if not SMOKE_FILE.exists():
84-
print(f"ERROR: no smoke_tests.txt at {SMOKE_FILE}", file=sys.stderr)
85-
return 1
86-
scripts = load_smoke_scripts()
87-
if not scripts:
88-
print("No smoke test scripts listed.")
89-
return 0
90-
cfg = load_cfg()
91-
92-
print(f"Running {len(scripts)} smoke test script(s) from {SMOKE_FILE.name}\n")
93-
failures: list[tuple[str, int, str]] = []
94-
for script_rel in scripts:
95-
print(f"::group::{script_rel}")
96-
name, rc, elapsed, output = run_one(script_rel, cfg)
97-
print(output, end="")
98-
status = "PASS" if rc == 0 else f"FAIL (exit {rc})"
99-
print(f"\n[{status}] {name}{elapsed:.1f}s")
100-
print("::endgroup::")
101-
if rc != 0:
102-
failures.append((name, rc, output))
52+
env = os.environ.copy()
53+
env["PYTHONPATH"] = os.pathsep.join(
54+
p for p in (str(AUTOHANDS), env.get("PYTHONPATH", "")) if p
55+
)
10356

104-
total = len(scripts)
105-
passed = total - len(failures)
106-
print(f"\n=== Smoke test summary: {passed}/{total} passed ===")
107-
for name, rc, _ in failures:
108-
print(f" FAIL {name} (exit {rc})")
109-
return 0 if not failures else 1
57+
# --report-dir is REQUIRED, not cosmetic. run_python.py only propagates
58+
# failures (`sys.exit(1)`) when a report was built; without it the suite
59+
# runs to completion and always exits 0 — a vacuously green gate. It also
60+
# switches execute_script from "abort on the first failure" to "record and
61+
# continue", which is the behaviour the old runner had.
62+
cmd = [
63+
sys.executable,
64+
str(AUTOHANDS / "run_python.py"),
65+
PROJECT,
66+
"scripts",
67+
"--report-dir",
68+
str(WORKSPACE / "test-results"),
69+
]
70+
# run_python.py resolves config/build/ relative to the cwd.
71+
return subprocess.run(cmd, cwd=str(WORKSPACE), env=env).returncode
11072

11173

11274
if __name__ == "__main__":

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ dataset/
1010
notebooks/plot/
1111
test_report.md
1212
test_results/
13+
14+
# Structured smoke/validation reports (run_smoke.py --report-dir)
15+
test-results/

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ and the generic `af.Model` / `af.Collection` API.
3535
## Testing
3636

3737
On CI, every PR is gated on Python **3.12 and 3.13** by `smoke_tests.yml` (runs
38-
`python .github/scripts/run_smoke.py`, driven by `smoke_tests.txt` + `config/build/profile_smoke.yaml`
38+
`python .github/scripts/run_smoke.py`, which runs **every** script under `scripts/` except the
39+
exclusions in `config/build/no_run.yaml`, with per-script env from `config/build/profile_smoke.yaml`
3940
the definition of green), `navigator_check.yml` (PyAutoHands's reusable navigator-catalogue check;
4041
see *Notebooks vs Scripts*), and `url_check.yml` (link checking). The smoke and navigator jobs check
4142
out **PyAutoHands** as a sibling and run the PyAuto* libraries from the **same-named branch** of each

smoke_tests.txt

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)