8FS-503 / FS-504 regression coverage — the ``include_venv`` flag on
9:func:`oct.scaffold.run_scaffold` must:
11* create ``.venv/`` via :mod:`venv` when True (the default),
12* populate :attr:`ScaffoldResult.venv_path`,
13* leave ``venv_path`` as ``None`` when False,
14* skip (not overwrite) an existing ``.venv/``,
15* downgrade :class:`OSError` from the builder to a warning message,
16* make the venv discoverable to :func:`check_venv_health` — i.e. the
17 scaffolded project is then "venv-found" even before activation.
19The tests stub :class:`venv.EnvBuilder` so the suite never shells out to
20``ensurepip`` — we are testing the *contract*, not the stdlib.
30- ``include_venv=True`` → ``.venv/`` created; ``venv_path`` set.
31- ``include_venv=False`` → no ``.venv/``; ``venv_path is None``.
32- Pre-existing ``.venv/`` is recorded in ``skipped`` with a message.
35from __future__
import annotations
37from pathlib
import Path
46 """Drop-in replacement for :class:`venv.EnvBuilder` used in tests.
48 Creates the target directory (mirroring the stdlib side-effect) without
49 installing pip — cheap, deterministic, safe on Windows.
52 instances: list[
"_FakeEnvBuilder"] = []
54 def __init__(self, *, with_pip: bool =
True, clear: bool =
False,
55 upgrade_deps: bool =
False) ->
None:
60 _FakeEnvBuilder.instances.append(self)
62 def create(self, env_dir: str) ->
None:
63 Path(env_dir).mkdir(parents=
True, exist_ok=
True)
64 (Path(env_dir) /
"pyvenv.cfg").write_text(
"home = /fake\n", encoding=
"utf-8")
68@pytest.fixture(autouse=True)
70 """Swap in the fake EnvBuilder for every test in this module."""
71 _FakeEnvBuilder.instances = []
72 monkeypatch.setattr(_scaffold_mod.venv,
"EnvBuilder", _FakeEnvBuilder)
77 """FS-503: default include_venv=True materialises .venv/ at the project root."""
78 result = run_scaffold(
"demo", tmp_path, include_venv=
True)
79 project = tmp_path /
"demo"
80 venv_dir = project /
".venv"
81 assert venv_dir.is_dir()
82 assert (venv_dir /
"pyvenv.cfg").is_file()
83 assert result.venv_path == venv_dir
84 assert venv_dir
in result.created
85 assert len(_FakeEnvBuilder.instances) == 1
89 """FS-503: include_venv=False produces no .venv/ and venv_path stays None."""
90 result = run_scaffold(
"demo", tmp_path, include_venv=
False)
91 project = tmp_path /
"demo"
92 assert not (project /
".venv").exists()
93 assert result.venv_path
is None
94 assert _FakeEnvBuilder.instances == []
98 """FS-503: include_venv=True combined with dry_run=True must not touch disk."""
99 result = run_scaffold(
"demo", tmp_path, dry_run=
True, include_venv=
True)
100 project = tmp_path /
"demo"
101 assert not project.exists()
102 assert result.venv_path
is None
103 assert _FakeEnvBuilder.instances == []
104 assert any(
".venv" in m
and "DRY RUN" in m
for m
in result.messages)
108 """FS-503: a second run over an existing tree records .venv/ in skipped."""
109 first = run_scaffold(
"demo", tmp_path, include_venv=
True)
110 assert first.venv_path
is not None
111 second = run_scaffold(
"demo", tmp_path, include_venv=
True)
112 project = tmp_path /
"demo"
113 assert (project /
".venv")
in second.skipped
115 assert len(_FakeEnvBuilder.instances) == 1
116 assert any(
"already exists" in m
for m
in second.messages)
120 tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
122 """FS-503: OSError from EnvBuilder must not abort scaffolding."""
124 class _ExplodingBuilder:
125 def __init__(self, **_: object) ->
None:
128 def create(self, env_dir: str) ->
None:
129 raise OSError(
"disk full")
131 monkeypatch.setattr(_scaffold_mod.venv,
"EnvBuilder", _ExplodingBuilder)
132 result = run_scaffold(
"demo", tmp_path, include_venv=
True)
133 project = tmp_path /
"demo"
135 assert (project /
".option_c" /
"octrc.json").is_file()
136 assert result.venv_path
is None
137 assert any(
"venv creation failed" in m
for m
in result.messages)
141 """Smoke: include_venv flag preserves the ScaffoldResult contract."""
142 result = run_scaffold(
"demo", tmp_path, include_venv=
True)
143 assert isinstance(result, ScaffoldResult)
144 assert isinstance(result.created, list)
145 assert isinstance(result.skipped, list)
146 assert isinstance(result.messages, list)
None create(self, str env_dir)
None __init__(self, *, bool with_pip=True, bool clear=False, bool upgrade_deps=False)
test_include_venv_false_skips_venv(Path tmp_path)
test_venv_creation_failure_downgrades_to_warning(Path tmp_path, pytest.MonkeyPatch monkeypatch)
test_dry_run_true_never_creates_venv(Path tmp_path)
test_result_is_scaffoldresult(Path tmp_path)
_stub_envbuilder(pytest.MonkeyPatch monkeypatch)
test_preexisting_venv_is_skipped(Path tmp_path)
test_include_venv_true_creates_venv(Path tmp_path)