From c177dd354ab198dea7f9e99e34c6aa9fb2f249f6 Mon Sep 17 00:00:00 2001 From: James Smith Date: Thu, 11 Jun 2026 16:44:04 +0100 Subject: [PATCH] test: add shared fake_process fixture for complete Popen mocking Co-Authored-By: Claude Fable 5 --- tests/conftest.py | 25 +++++++++++++++++++++++++ tests/test_conftest_fixtures.py | 24 ++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tests/test_conftest_fixtures.py diff --git a/tests/conftest.py b/tests/conftest.py index 4c566b7..6a72b6f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -138,3 +138,28 @@ def test_db(tmp_path): conn.execute("PRAGMA journal_mode = WAL") yield conn conn.close() + + +@pytest.fixture +def fake_process(): + """Factory for complete subprocess.Popen replacements. + + Hand-rolled Popen mocks keep missing two things: __enter__ (subprocess.run + wraps Popen in a context manager) and a communicate() tuple. Use this + factory instead of building MagicMock processes inline. + """ + + def _make(returncode=0, stdout="", stderr="", running=True, pid=12345): + proc = MagicMock() + proc.poll.return_value = None if running else returncode + proc.returncode = returncode + proc.pid = pid + proc.wait.return_value = returncode + proc.communicate.return_value = (stdout, stderr) + proc.stdout.read.return_value = stdout + proc.stderr.read.return_value = stderr + proc.stdin = MagicMock() + proc.__enter__.return_value = proc + return proc + + return _make diff --git a/tests/test_conftest_fixtures.py b/tests/test_conftest_fixtures.py new file mode 100644 index 0000000..5b4e7fe --- /dev/null +++ b/tests/test_conftest_fixtures.py @@ -0,0 +1,24 @@ +"""Tests for shared conftest fixtures.""" + +import subprocess +from unittest.mock import patch + + +class TestFakeProcess: + def test_works_with_subprocess_run(self, fake_process): + """subprocess.run() must unpack communicate() and enter the context manager.""" + proc = fake_process(stdout="hello", stderr="", returncode=0) + with patch("subprocess.Popen", return_value=proc): + result = subprocess.run(["anything"], capture_output=True, text=True, timeout=5) + assert result.stdout == "hello" + + def test_running_process_defaults(self, fake_process): + proc = fake_process() + assert proc.poll() is None # still running + assert proc.pid == 12345 + assert proc.communicate() == ("", "") + + def test_exited_process(self, fake_process): + proc = fake_process(running=False, returncode=1, stderr=b"device busy") + assert proc.poll() == 1 + assert proc.stderr.read() == b"device busy"