Skip to content

Commit 7e259c5

Browse files
committed
test: isolate HOME and silence expected output in test suite
Point HOME at a private temp dir per suite run so lpb never writes token / last-project / last-image / state-defaults into the real ~/.localpibox, and test_remove (which relies on default cfg dirs) can no longer prompt against real data. _OutputCapture now also redirects the root logger's handler streams (lpb.py err/warn write via logger too, not just sys.stderr), so expected error/message noise no longer leaks into CI logs. No real podman/docker/curl is invoked: images_pull is patched and container ops go through mock_podman.
1 parent 971d946 commit 7e259c5

1 file changed

Lines changed: 70 additions & 20 deletions

File tree

scripts/test_lpb.py

Lines changed: 70 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import ast
1414
import builtins
1515
import importlib.util
16+
import logging
1617
import os
1718
import re
1819
import shutil
@@ -151,12 +152,52 @@ def reset_mock():
151152
_module_counter = 0
152153
_subprocess_orig = None
153154
_shutil_orig = None
155+
_ISOLATED_HOME = tempfile.mkdtemp(prefix="lpb_test_home_")
156+
157+
158+
class _OutputCapture:
159+
"""Redirect stdout+stderr to a sink while active (suppresses expected noise).
160+
161+
Also redirects the root logger's handler streams — lpb.py's err()/warn()
162+
write through BOTH print(..., file=sys.stderr) and logger.error, and the
163+
logger handler captured the original sys.stderr at import time, so it
164+
would bypass a plain sys.stderr swap.
165+
"""
166+
167+
def __init__(self):
168+
self.out = []
169+
self._old = None
170+
self._old_streams = None
171+
172+
def write(self, s):
173+
self.out.append(s)
174+
175+
def flush(self):
176+
pass
177+
178+
def __enter__(self):
179+
self._old = (sys.stdout, sys.stderr)
180+
sys.stdout, sys.stderr = self, self
181+
self._old_streams = [h.stream for h in logging.getLogger().handlers]
182+
for h in logging.getLogger().handlers:
183+
h.stream = self
184+
return self
185+
186+
def __exit__(self, *exc):
187+
sys.stdout, sys.stderr = self._old
188+
for h, stream in zip(logging.getLogger().handlers, self._old_streams):
189+
h.stream = stream
190+
return False
191+
154192

155193
def make_module(lpb_path: str | None = None):
156194
"""Import lpb.py with all mocks in place. Each call gets unique module name.
157195
158196
lpb_path: override which lpb source file to import (used by regression
159197
mutation tests to load a deliberately-broken copy).
198+
199+
HOME is redirected to a private temp dir, so the module never touches the
200+
real ~/.localpibox (token / last-project / last-image / state defaults).
160201
"""
161202
global _module_counter, _subprocess_orig, _shutil_orig
162203
_module_counter += 1
@@ -169,10 +210,12 @@ def make_module(lpb_path: str | None = None):
169210
subprocess.run = mock_run
170211
shutil.which = mock_which
171212

172-
# Remove LPB_* env vars so tests get clean defaults
213+
# Remove LPB_* env vars so tests get clean defaults, and point HOME at a
214+
# private temp dir so the module's default paths are isolated per run.
173215
for key in list(os.environ.keys()):
174216
if key.startswith("LPB_"):
175217
os.environ.pop(key, None)
218+
os.environ["HOME"] = _ISOLATED_HOME
176219

177220
# Use unique name to bypass sys.modules cache
178221
mod_name = f"lpb_test_{_module_counter}_{id(make_module)}"
@@ -243,7 +286,8 @@ def test_remove():
243286
mod = make_module()
244287
mod.parse_cli(["--remove"])
245288
mod.apply_overrides()
246-
mod.cmd_remove()
289+
with _OutputCapture():
290+
mod.cmd_remove()
247291
assert not MOCK_STATE["exists"], "container should be removed"
248292
print(" PASS\n")
249293

@@ -278,26 +322,28 @@ def test_logs_missing():
278322
mod = make_module()
279323
mod.parse_cli(["--logs"])
280324
mod.apply_overrides()
281-
try:
282-
mod.cmd_logs()
283-
except SystemExit:
284-
pass
325+
with _OutputCapture():
326+
try:
327+
mod.cmd_logs()
328+
except SystemExit:
329+
pass
285330
print(" PASS\n")
286331

287332

288333
def test_unknown_flag():
289334
print("TEST: --foo-bar → error")
290335
reset_mock()
291336
mod = make_module()
292-
try:
293-
mod.parse_cli(["--foo-bar"])
294-
mod.apply_overrides()
295-
mod.cmd_run()
296-
assert False, "should have raised an error"
297-
except (SystemExit, mod.DevstackError) as e:
298-
# DevstackError (new) or SystemExit(code=1) (legacy path)
299-
if isinstance(e, SystemExit):
300-
assert e.code == 1
337+
with _OutputCapture():
338+
try:
339+
mod.parse_cli(["--foo-bar"])
340+
mod.apply_overrides()
341+
mod.cmd_run()
342+
assert False, "should have raised an error"
343+
except (SystemExit, mod.DevstackError) as e:
344+
# DevstackError (new) or SystemExit(code=1) (legacy path)
345+
if isinstance(e, SystemExit):
346+
assert e.code == 1
301347
print(" PASS\n")
302348

303349

@@ -571,7 +617,8 @@ def test_cmd_remove_with_dirs():
571617
orig_input = builtins.input
572618
builtins.input = lambda prompt="": "y"
573619
try:
574-
mod.cmd_remove()
620+
with _OutputCapture():
621+
mod.cmd_remove()
575622
finally:
576623
builtins.input = orig_input
577624
assert not MOCK_STATE["exists"], "container should be removed"
@@ -596,7 +643,8 @@ def test_cmd_remove_abort():
596643
orig_input = builtins.input
597644
builtins.input = lambda prompt="": "n"
598645
try:
599-
mod.cmd_remove()
646+
with _OutputCapture():
647+
mod.cmd_remove()
600648
finally:
601649
builtins.input = orig_input
602650
assert os.path.exists(state), "state dir must survive an aborted remove"
@@ -620,9 +668,10 @@ def test_cmd_update_runs():
620668
mod.ContainerClient.images_pull = lambda self, name: pulled.append(name) or 0
621669
mod.parse_cli(["--update"])
622670
mod.apply_overrides()
623-
mod.cmd_update()
671+
with _OutputCapture():
672+
mod.cmd_update()
624673
assert pulled, f"expected image pulls, got {pulled}"
625-
print(f" pulled: {pulled}")
674+
print(f" pulled: {pulled} (via mocked images_pull)")
626675
print(" PASS\n")
627676

628677

@@ -649,7 +698,8 @@ def spy_containers_run(*a, **kw):
649698
return ("cid123", "cid123", "", 0)
650699

651700
mod.ContainerClient.containers_run = spy_containers_run
652-
mod.cmd_run()
701+
with _OutputCapture():
702+
mod.cmd_run()
653703
assert mod.cfg.project_name == "tmp"
654704
env_vars = captured.get("env", [])
655705
env_str = "\n".join(env_vars)

0 commit comments

Comments
 (0)