diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d20921328..68706f190 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -227,12 +227,13 @@ jobs: group: yield timeout_minutes: 20 cargo_test_args: "" - # Both files share this one crate (issue #923, Phase 3 of the + # All three files share this one crate (issue #923, Phase 3 of the # statistical/yield epic #710, added `klt yield-sensitivity` to - # the same `klt_yield_native` extension `klt yield` uses). - test_paths: tests/test_yield.py tests/test_yield_sensitivity.py + # the same `klt_yield_native` extension `klt yield` uses; issue + # #2675 added the `klt yield-campaign` native end-to-end tests). + test_paths: tests/test_yield.py tests/test_yield_campaign.py tests/test_yield_sensitivity.py extension_module: klt_yield_native - no_skip_message: "klt_yield_native did not install -- tests/test_yield.py's and tests/test_yield_sensitivity.py's statistics tiers would have skipped" + no_skip_message: "klt_yield_native did not install -- tests/test_yield.py's, tests/test_yield_campaign.py's and tests/test_yield_sensitivity.py's statistics tiers would have skipped" defaults: run: working-directory: ${{ matrix.dir }} diff --git a/tests/test_yield_campaign.py b/tests/test_yield_campaign.py index 6496d6c55..f004c9843 100644 --- a/tests/test_yield_campaign.py +++ b/tests/test_yield_campaign.py @@ -22,11 +22,13 @@ import importlib.util import json import os +import shutil from pathlib import Path import pytest from helpers.subprocess_fakes import fake_completed +from klayout_tools import _paths as paths_module from klayout_tools import sim, yield_campaign from klayout_tools.yield_analysis import _read_samples from klayout_tools.yield_campaign import CampaignError, run_campaign @@ -77,7 +79,28 @@ def _base_spec(**overrides) -> dict: return spec +def _stub_ngspice_discovery(monkeypatch) -> None: + """Issue #2675: `sim.py` resolves the `ngspice` binary (issue #2423) + *before* dispatching any corner, i.e. before the `subprocess.run` stubs + below take effect. Stub resolution to the bare name (mirrors + `test_sim.py`'s `_bare_name_ngspice_resolution`) and drop any host + `$KLT_NGSPICE_BINARY`, so these tests do not need a real install. + Production discovery is untouched; only called by tests that also stub + `subprocess.run`.""" + real_which = shutil.which + + def fake_which(cmd, *args, **kwargs): + if cmd == "ngspice": + return cmd + return real_which(cmd, *args, **kwargs) + + monkeypatch.delenv("KLT_NGSPICE_BINARY", raising=False) + monkeypatch.setattr(paths_module.shutil, "which", fake_which) + + def _stub_subprocess_run(monkeypatch, *, log_text: str) -> None: + _stub_ngspice_discovery(monkeypatch) + def fake_run(cmd, capture_output, text, timeout, cwd=None): log_path = cmd[cmd.index("-o") + 1] with open(log_path, "w", encoding="utf-8") as handle: @@ -636,6 +659,7 @@ def _stub_subprocess_run_per_sample(monkeypatch, *, values: list[str]) -> None: """Like `_stub_subprocess_run`, but emits a different measured value per ngspice invocation, so a single campaign can mix trustworthy and implausible draws.""" + _stub_ngspice_discovery(monkeypatch) calls = {"n": 0} def fake_run(cmd, capture_output, text, timeout, cwd=None): @@ -722,9 +746,8 @@ def test_run_campaign_hands_yield_a_netlist_that_unwraps_to_a_string( ): """The regression, over the **real** `run_sim` -> `_read_samples` seam rather than a hand-built fixture -- only the native statistics core is - stubbed, so this runs everywhere (`test_yield_campaign.py` is not in - CI's `klt_yield_native` matrix leg, which is why the `requires_native` - end-to-end below cannot be the only cover for this). + stubbed, so this runs everywhere (the `requires_native` + end-to-end below also runs in CI's `klt_yield_native` matrix leg). `run_campaign` dumps `run_sim`'s live report verbatim to `sample-set.json` and hands the path straight to `run_yield`, so