Files
devx/tests/unit/test_check_test_isolation.py
T
emil d8ceb6c8a1
Post-merge / detect-and-configure (push) Successful in 17s
Post-merge / release-and-maintain (push) Successful in 1m9s
DEVX-140: feat: make check_test_isolation configurable via pyproject.toml
2026-07-14 16:29:31 +00:00

1782 lines
63 KiB
Python

"""Unit tests for devx.tools.check_test_isolation."""
from __future__ import annotations
import ast
import subprocess
import textwrap
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from click.testing import CliRunner
from devx.tools.check_test_isolation import (
HEAVY_MODULE_IMPORTS,
HELPER_INTERNAL_CALLS,
IO_INTERNAL_CALLS,
KNOWN_IO_FUNCTIONS,
KNOWN_SUBPROCESS_HELPERS,
CallGraph,
_extract_patch_targets,
_is_integration_test,
_load_test_isolation_config,
_SubprocessAudit,
analyze_file,
analyze_test_files,
cli,
find_test_files,
)
def _write_test_file(tmp_path: Path, content: str) -> Path:
"""Write content to a test file and return the path."""
file = tmp_path / "test_example.py"
file.write_text(textwrap.dedent(content))
return file
class TestFindTestFiles:
def test_finds_test_files_in_directory(self, tmp_path: Path) -> None:
(tmp_path / "test_foo.py").touch()
(tmp_path / "test_bar.py").touch()
(tmp_path / "helper.py").touch()
result = find_test_files(tmp_path)
assert len(result) == 2
assert all(f.name.startswith("test_") for f in result)
def test_single_file(self, tmp_path: Path) -> None:
file = tmp_path / "test_single.py"
file.touch()
result = find_test_files(file)
assert result == [file]
def test_non_python_file(self, tmp_path: Path) -> None:
file = tmp_path / "test_readme.md"
file.touch()
result = find_test_files(file)
assert result == []
class TestAnalyzeFile:
def test_clean_file_no_violations(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
from unittest.mock import patch, MagicMock
class TestExample:
@patch("mymodule.subprocess.run")
def test_with_patch(self, mock_run: MagicMock) -> None:
mymodule.do_thing()
""",
)
violations = analyze_file(file)
assert violations == []
def test_unpatched_subprocess_run(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
import subprocess
class TestExample:
def test_direct_subprocess(self) -> None:
subprocess.run(["echo", "hi"])
""",
)
violations = analyze_file(file)
assert len(violations) == 1
assert violations[0].category == "unpatched-subprocess"
assert "subprocess.run" in violations[0].message
def test_patched_subprocess_no_violation(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
from unittest.mock import patch, MagicMock
import subprocess
class TestExample:
@patch("subprocess.run")
def test_patched(self, mock_run: MagicMock) -> None:
subprocess.run(["echo", "hi"])
""",
)
violations = analyze_file(file)
assert violations == []
def test_unpatched_time_sleep(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
import time
class TestExample:
def test_with_sleep(self) -> None:
time.sleep(5)
""",
)
violations = analyze_file(file)
assert len(violations) == 1
assert violations[0].category == "unpatched-sleep"
def test_patched_time_sleep_no_violation(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
from unittest.mock import patch, MagicMock
import time
class TestExample:
@patch("time.sleep")
def test_patched_sleep(self, mock_sleep: MagicMock) -> None:
time.sleep(5)
""",
)
violations = analyze_file(file)
assert violations == []
def test_unpatched_known_helper(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
from mymodule import update_doc_versions
class TestExample:
def test_calls_helper(self) -> None:
update_doc_versions("1.0.0")
""",
)
violations = analyze_file(file)
assert len(violations) == 1
assert violations[0].category == "unpatched-helper"
assert "update_doc_versions" in violations[0].message
def test_patched_helper_no_violation(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
from unittest.mock import patch, MagicMock
from mymodule import update_doc_versions
class TestExample:
@patch("mymodule.update_doc_versions")
def test_patched_helper(self, mock: MagicMock) -> None:
update_doc_versions("1.0.0")
""",
)
violations = analyze_file(file)
assert violations == []
def test_helper_safe_when_subprocess_patched(self, tmp_path: Path) -> None:
"""update_doc_versions is safe if subprocess.run is patched."""
file = _write_test_file(
tmp_path,
"""
from unittest.mock import patch, MagicMock
from mymodule import update_doc_versions
class TestExample:
@patch("subprocess.run")
def test_subprocess_patched(self, mock: MagicMock) -> None:
update_doc_versions("1.0.0")
""",
)
violations = analyze_file(file)
assert violations == []
def test_helper_safe_when_internal_dep_patched(self, tmp_path: Path) -> None:
"""run_tests is safe if run_cmd is patched (run_tests calls run_cmd)."""
file = _write_test_file(
tmp_path,
"""
from unittest.mock import patch, MagicMock
from mymodule import run_tests
class TestExample:
@patch("mymodule.run_cmd")
def test_run_cmd_patched(self, mock: MagicMock) -> None:
run_tests()
""",
)
violations = analyze_file(file)
assert violations == []
def test_excessive_iterations(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
class TestExample:
def test_many_iterations(self) -> None:
for _ in range(500):
assert True
""",
)
violations = analyze_file(file)
assert len(violations) == 1
assert violations[0].category == "excessive-iterations"
assert "500" in violations[0].message
def test_acceptable_iterations(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
class TestExample:
def test_few_iterations(self) -> None:
for _ in range(50):
assert True
""",
)
violations = analyze_file(file)
assert violations == []
def test_range_with_start_stop(self, tmp_path: Path) -> None:
"""range(0, 500) should also be flagged."""
file = _write_test_file(
tmp_path,
"""
class TestExample:
def test_range_start_stop(self) -> None:
for _ in range(0, 500):
assert True
""",
)
violations = analyze_file(file)
assert len(violations) == 1
assert violations[0].category == "excessive-iterations"
def test_subprocess_check_output(self, tmp_path: Path) -> None:
"""subprocess.check_output should also be flagged."""
file = _write_test_file(
tmp_path,
"""
import subprocess
class TestExample:
def test_check_output(self) -> None:
result = subprocess.check_output(["echo", "hi"])
""",
)
violations = analyze_file(file)
assert len(violations) == 1
assert violations[0].category == "unpatched-subprocess"
def test_subprocess_popen(self, tmp_path: Path) -> None:
"""subprocess.Popen should also be flagged."""
file = _write_test_file(
tmp_path,
"""
import subprocess
class TestExample:
def test_popen(self) -> None:
p = subprocess.Popen(["echo", "hi"])
""",
)
violations = analyze_file(file)
assert len(violations) == 1
assert violations[0].category == "unpatched-subprocess"
def test_attribute_style_patch(self, tmp_path: Path) -> None:
"""mock.patch.object style should be recognized."""
file = _write_test_file(
tmp_path,
"""
from unittest.mock import mock
import subprocess
class TestExample:
@mock.patch("subprocess.run")
def test_attr_patch(self, mock_run) -> None:
subprocess.run(["echo"])
""",
)
violations = analyze_file(file)
assert violations == []
def test_subprocess_check_call(self, tmp_path: Path) -> None:
"""subprocess.check_call should also be flagged."""
file = _write_test_file(
tmp_path,
"""
import subprocess
class TestExample:
def test_check_call(self) -> None:
subprocess.check_call(["echo", "hi"])
""",
)
violations = analyze_file(file)
assert len(violations) == 1
assert violations[0].category == "unpatched-subprocess"
def test_subprocess_call(self, tmp_path: Path) -> None:
"""subprocess.call should also be flagged."""
file = _write_test_file(
tmp_path,
"""
import subprocess
class TestExample:
def test_call(self) -> None:
subprocess.call(["echo", "hi"])
""",
)
violations = analyze_file(file)
assert len(violations) == 1
assert violations[0].category == "unpatched-subprocess"
def test_non_subprocess_attribute_not_flagged(self, tmp_path: Path) -> None:
"""subprocess.something_else should not be flagged."""
file = _write_test_file(
tmp_path,
"""
import subprocess
class TestExample:
def test_other(self) -> None:
x = subprocess.PIPE
""",
)
violations = analyze_file(file)
assert violations == []
def test_async_test_function(self, tmp_path: Path) -> None:
"""Async test functions should be analyzed too."""
file = _write_test_file(
tmp_path,
"""
import subprocess
class TestExample:
async def test_async(self) -> None:
subprocess.run(["echo"])
""",
)
violations = analyze_file(file)
assert len(violations) == 1
assert violations[0].category == "unpatched-subprocess"
def test_call_with_no_name(self, tmp_path: Path) -> None:
"""Calls with complex expressions (e.g. lambda) should not crash."""
file = _write_test_file(
tmp_path,
"""
class TestExample:
def test_lambda_call(self) -> None:
(lambda: None)()
""",
)
violations = analyze_file(file)
assert violations == []
def test_range_with_no_args(self, tmp_path: Path) -> None:
"""range() with no args should not crash."""
file = _write_test_file(
tmp_path,
"""
class TestExample:
def test_empty_range(self) -> None:
for _ in range():
pass
""",
)
violations = analyze_file(file)
assert violations == []
def test_range_with_non_constant_stop(self, tmp_path: Path) -> None:
"""range(0, variable) should not be flagged (can't determine count)."""
file = _write_test_file(
tmp_path,
"""
class TestExample:
def test_variable_range(self) -> None:
n = 100
for _ in range(0, n):
pass
""",
)
violations = analyze_file(file)
assert violations == []
def test_range_with_non_constant_start(self, tmp_path: Path) -> None:
"""range(variable, 500) should be flagged with stop value."""
file = _write_test_file(
tmp_path,
"""
class TestExample:
def test_variable_start(self) -> None:
s = 0
for _ in range(s, 500):
pass
""",
)
violations = analyze_file(file)
assert len(violations) == 1
assert violations[0].category == "excessive-iterations"
def test_for_loop_with_non_range_call(self, tmp_path: Path) -> None:
"""for loop with a non-range call should not crash."""
file = _write_test_file(
tmp_path,
"""
class TestExample:
def test_iter_func(self) -> None:
for _ in list([1, 2, 3]):
pass
""",
)
violations = analyze_file(file)
assert violations == []
def test_for_loop_with_list(self, tmp_path: Path) -> None:
"""for loop with a list literal should not crash."""
file = _write_test_file(
tmp_path,
"""
class TestExample:
def test_iter_list(self) -> None:
for _ in [1, 2, 3]:
pass
""",
)
violations = analyze_file(file)
assert violations == []
def test_range_with_single_non_int_arg(self, tmp_path: Path) -> None:
"""range(variable) should not crash or flag."""
file = _write_test_file(
tmp_path,
"""
class TestExample:
def test_range_var(self) -> None:
n = 50
for _ in range(n):
pass
""",
)
violations = analyze_file(file)
assert violations == []
def test_range_with_three_args(self, tmp_path: Path) -> None:
"""range(0, 500, 1) should be flagged (3 args, stop=500)."""
file = _write_test_file(
tmp_path,
"""
class TestExample:
def test_range_step(self) -> None:
for _ in range(0, 500, 1):
pass
""",
)
violations = analyze_file(file)
assert len(violations) == 1
assert violations[0].category == "excessive-iterations"
def test_non_test_function_not_analyzed(self, tmp_path: Path) -> None:
"""Non-test functions should not be analyzed."""
file = _write_test_file(
tmp_path,
"""
import subprocess
def helper_function() -> None:
subprocess.run(["echo", "hi"])
class TestExample:
def test_uses_helper(self) -> None:
helper_function()
""",
)
violations = analyze_file(file)
# helper_function is not a test, so no violation for its subprocess call
# test_uses_helper calls helper_function, not subprocess directly
assert violations == []
def test_class_level_patch_satisfies_check(self, tmp_path: Path) -> None:
"""@patch on the class should satisfy the check for all methods."""
file = _write_test_file(
tmp_path,
"""
from unittest.mock import patch, MagicMock
import subprocess
@patch("subprocess.run")
class TestExample:
def test_method_a(self, mock: MagicMock) -> None:
subprocess.run(["echo", "a"])
def test_method_b(self, mock: MagicMock) -> None:
subprocess.run(["echo", "b"])
""",
)
violations = analyze_file(file)
assert violations == []
def test_syntax_error_returns_violation(self, tmp_path: Path) -> None:
file = tmp_path / "test_broken.py"
file.write_text("def test(:\n pass\n")
violations = analyze_file(file)
assert len(violations) == 1
assert violations[0].category == "syntax-error"
def test_heavy_module_import_at_module_level(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
import pandas
def test_foo() -> None:
assert True
""",
)
violations = analyze_file(file)
assert len(violations) == 1
assert violations[0].category == "heavy-module-import"
assert "pandas" in violations[0].message
def test_heavy_import_inside_function_ok(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
def test_foo() -> None:
import pandas
assert True
""",
)
violations = analyze_file(file)
assert violations == []
def test_heavy_import_from_at_module_level(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
from matplotlib import pyplot as plt
def test_foo() -> None:
assert True
""",
)
violations = analyze_file(file)
assert len(violations) == 1
assert violations[0].category == "heavy-module-import"
def test_reload_without_cleanup_odd_count(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
import importlib
import devx.config as cfg
def test_reload_no_cleanup() -> None:
importlib.reload(cfg)
assert cfg.TASK_PREFIX == "CUSTOM"
""",
)
violations = analyze_file(file)
reload_violations = [v for v in violations if v.category == "reload-without-cleanup"]
assert len(reload_violations) == 1
assert "1 time(s)" in reload_violations[0].message
def test_reload_with_cleanup_even_count_ok(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
import importlib
import devx.config as cfg
def test_reload_with_cleanup() -> None:
importlib.reload(cfg)
assert cfg.TASK_PREFIX == "CUSTOM"
importlib.reload(cfg)
""",
)
violations = analyze_file(file)
reload_violations = [v for v in violations if v.category == "reload-without-cleanup"]
assert reload_violations == []
def test_reload_attribute_access_detected(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
import importlib
import devx.config
def test_reload_attr() -> None:
importlib.reload(devx.config)
""",
)
violations = analyze_file(file)
reload_violations = [v for v in violations if v.category == "reload-without-cleanup"]
assert len(reload_violations) == 1
assert "config" in reload_violations[0].message
class TestAnalyzeTestFiles:
def test_multiple_files(self, tmp_path: Path) -> None:
_write_test_file(
tmp_path,
"""
import subprocess
class TestA:
def test_a(self) -> None:
subprocess.run(["echo"])
""",
)
file2 = tmp_path / "test_other.py"
file2.write_text(
textwrap.dedent("""
import time
class TestB:
def test_b(self) -> None:
time.sleep(1)
""")
)
violations = analyze_test_files(tmp_path)
assert len(violations) == 2
categories = {v.category for v in violations}
assert "unpatched-subprocess" in categories
assert "unpatched-sleep" in categories
def test_category_filter(self, tmp_path: Path) -> None:
_write_test_file(
tmp_path,
"""
import subprocess
class TestA:
def test_a(self) -> None:
subprocess.run(["echo"])
""",
)
file2 = tmp_path / "test_other.py"
file2.write_text(
textwrap.dedent("""
import time
class TestB:
def test_b(self) -> None:
time.sleep(1)
""")
)
violations = analyze_test_files(tmp_path, categories={"unpatched-sleep"})
assert len(violations) == 1
assert violations[0].category == "unpatched-sleep"
class TestKnownHelpers:
def test_all_helpers_have_internal_calls(self) -> None:
"""Every known helper should have its internal calls documented."""
for helper in KNOWN_SUBPROCESS_HELPERS:
assert helper in HELPER_INTERNAL_CALLS, f"Missing HELPER_INTERNAL_CALLS entry for {helper}"
def test_run_tests_internal_calls_include_run_cmd(self) -> None:
assert "run_cmd" in HELPER_INTERNAL_CALLS["run_tests"]
def test_update_doc_versions_internal_calls_include_subprocess(self) -> None:
assert "subprocess" in HELPER_INTERNAL_CALLS["update_doc_versions"]
class TestIOFunctionChecks:
"""Tests for unpatched I/O function detection."""
def test_unpatched_get_pat_violation(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
from mymodule import get_pat
class TestExample:
def test_calls_get_pat(self) -> None:
result = get_pat("staging")
""",
)
violations = analyze_file(file)
assert len(violations) == 1
assert violations[0].category == "unpatched-io"
assert "get_pat" in violations[0].message
def test_patched_get_pat_no_violation(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
from unittest.mock import patch
from mymodule import get_pat
class TestExample:
@patch("mymodule.get_pat", return_value="pat")
def test_patched(self, mock) -> None:
result = get_pat("staging")
""",
)
violations = analyze_file(file)
assert violations == []
def test_unpatched_load_secrets_violation(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
from mymodule import load_secrets
class TestExample:
def test_calls_load_secrets(self) -> None:
result = load_secrets("staging")
""",
)
violations = analyze_file(file)
assert len(violations) == 1
assert violations[0].category == "unpatched-io"
assert "load_secrets" in violations[0].message
def test_patched_load_secrets_no_violation(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
from unittest.mock import patch
from mymodule import load_secrets
class TestExample:
@patch("mymodule.load_secrets", return_value={})
def test_patched(self, mock) -> None:
result = load_secrets("staging")
""",
)
violations = analyze_file(file)
assert violations == []
def test_unpatched_requests_get_violation(self, tmp_path: Path) -> None:
file = _write_test_file(
tmp_path,
"""
import requests
class TestExample:
def test_calls_requests(self) -> None:
resp = requests.get("https://example.com")
""",
)
violations = analyze_file(file)
assert len(violations) == 1
assert violations[0].category == "unpatched-io"
assert "requests.get" in violations[0].message or "get" in violations[0].message
def test_integration_marker_skips_subprocess(self, tmp_path: Path) -> None:
"""@pytest.mark.integration tests should not be flagged for subprocess.run."""
file = _write_test_file(
tmp_path,
"""
import subprocess
import pytest
@pytest.mark.integration
def test_real_subprocess():
subprocess.run(["echo", "hello"])
""",
)
violations = analyze_file(file)
assert violations == []
def test_integration_marker_skips_sleep(self, tmp_path: Path) -> None:
"""@pytest.mark.integration tests should not be flagged for time.sleep."""
file = _write_test_file(
tmp_path,
"""
import time
import pytest
@pytest.mark.integration
def test_real_sleep():
time.sleep(1)
""",
)
violations = analyze_file(file)
assert violations == []
def test_integration_marker_with_args_skips(self, tmp_path: Path) -> None:
"""@pytest.mark.integration(...) with args should also be skipped."""
file = _write_test_file(
tmp_path,
"""
import subprocess
import pytest
@pytest.mark.integration(scope="module")
def test_real_subprocess():
subprocess.run(["echo", "hello"])
""",
)
violations = analyze_file(file)
assert violations == []
def test_integration_directory_skipped(self, tmp_path: Path) -> None:
"""Files in integration/ directories should be skipped entirely."""
integration_dir = tmp_path / "integration"
integration_dir.mkdir()
file = integration_dir / "test_real_io.py"
file.write_text("import subprocess\ndef test_real_subprocess():\n subprocess.run(['echo', 'hello'])\n")
violations = analyze_file(file)
assert violations == []
class TestCli:
"""Tests for the standalone CLI interface."""
def test_clean_directory_exits_zero(self, tmp_path: Path) -> None:
_write_test_file(
tmp_path,
"""
from unittest.mock import patch, MagicMock
class TestExample:
@patch("subprocess.run")
def test_ok(self, mock: MagicMock) -> None:
pass
""",
)
runner = CliRunner()
result = runner.invoke(cli, ["--test-path", str(tmp_path)])
assert result.exit_code == 0
assert "no violations" in result.output
def test_violations_exit_nonzero(self, tmp_path: Path) -> None:
_write_test_file(
tmp_path,
"""
import subprocess
class TestExample:
def test_bad(self) -> None:
subprocess.run(["echo"])
""",
)
runner = CliRunner()
result = runner.invoke(cli, ["--test-path", str(tmp_path)])
assert result.exit_code == 1
assert "FAILED" in result.output
assert "unpatched-subprocess" in result.output
def test_always_strict(self, tmp_path: Path) -> None:
"""CLI is always strict — no --strict flag needed."""
_write_test_file(
tmp_path,
"""
import subprocess
class TestExample:
def test_bad(self) -> None:
subprocess.run(["echo"])
""",
)
runner = CliRunner()
result = runner.invoke(cli, ["--test-path", str(tmp_path)])
assert result.exit_code == 1
def test_category_filter(self, tmp_path: Path) -> None:
_write_test_file(
tmp_path,
"""
import subprocess, time
class TestExample:
def test_bad(self) -> None:
subprocess.run(["echo"])
time.sleep(1)
""",
)
runner = CliRunner()
result = runner.invoke(cli, ["--test-path", str(tmp_path), "--categories", "unpatched-sleep"])
assert result.exit_code == 1
assert "unpatched-sleep" in result.output
assert "unpatched-subprocess" not in result.output
def test_max_loop_iterations_option(self, tmp_path: Path) -> None:
_write_test_file(
tmp_path,
"""
class TestExample:
def test_loop(self) -> None:
for _ in range(10):
assert True
""",
)
runner = CliRunner()
# With max=5, 10 iterations is a violation
result = runner.invoke(cli, ["--test-path", str(tmp_path), "--max-loop-iterations", "5"])
assert result.exit_code == 1
assert "excessive-iterations" in result.output
def test_no_test_files(self, tmp_path: Path) -> None:
runner = CliRunner()
result = runner.invoke(cli, ["--test-path", str(tmp_path)])
assert result.exit_code == 0
assert "no violations" in result.output
class TestPytestPlugin:
"""Tests for the pytest plugin hooks.
These hooks are marked with pragma: no cover because they're loaded
by pytest before coverage instrumentation starts. We test them via
direct calls to verify correctness.
"""
def test_pytest_addoption_registers_options(self) -> None:
"""Verify that pytest_addoption registers the expected options."""
from unittest.mock import MagicMock
from devx.tools.check_test_isolation import pytest_addoption
parser = MagicMock()
pytest_addoption(parser)
addoption_calls = parser.addoption.call_args_list
assert len(addoption_calls) >= 2
def test_pytest_collection_finish_noop_when_disabled(self) -> None:
"""Plugin should skip analysis when --no-test-isolation is set."""
from unittest.mock import MagicMock
from devx.tools.check_test_isolation import pytest_collection_finish
session = MagicMock()
session.config.getoption.side_effect = lambda opt: opt == "--no-test-isolation"
pytest_collection_finish(session)
def test_pytest_collection_finish_no_violations(self) -> None:
"""Plugin should not emit warnings when there are no violations."""
from unittest.mock import MagicMock
from devx.tools.check_test_isolation import pytest_collection_finish
session = MagicMock()
session.config.getoption.side_effect = lambda opt: False
session.items = []
pytest_collection_finish(session)
def test_pytest_collection_finish_with_violation(self, tmp_path: Path) -> None:
"""Plugin should fail when hard violations are found (always strict)."""
from unittest.mock import MagicMock
import pytest
from devx.tools.check_test_isolation import pytest_collection_finish
test_file = _write_test_file(
tmp_path,
"""
import subprocess
class TestExample:
def test_bad(self) -> None:
subprocess.run(["echo"])
""",
)
session = MagicMock()
session.config.getoption.side_effect = lambda opt: {
"--no-test-isolation": False,
"--test-isolation-max-loop": 100,
}.get(opt, False)
item = MagicMock()
item.fspath = str(test_file)
session.items = [item]
with pytest.raises(pytest.fail.Exception, match="Test isolation"):
pytest_collection_finish(session)
def test_pytest_collection_finish_advisory_only(self, tmp_path: Path) -> None:
"""Transitive-subprocess advisories should warn, not fail."""
import warnings
from unittest.mock import MagicMock
from devx.tools.check_test_isolation import pytest_collection_finish
# Create a src/ directory with a module that calls subprocess.run
# so the call graph can detect transitive subprocess calls.
src_dir = tmp_path / "src" / "mypkg"
src_dir.mkdir(parents=True)
(src_dir / "__init__.py").write_text("")
(src_dir / "cli.py").write_text("import subprocess\ndef main():\n subprocess.run(['echo'])\n")
test_file = _write_test_file(
tmp_path,
"""
from click.testing import CliRunner
from mypkg.cli import main
class TestExample:
def test_advisory(self) -> None:
runner = CliRunner()
result = runner.invoke(main, [])
""",
)
session = MagicMock()
session.config.getoption.side_effect = lambda opt: {
"--no-test-isolation": False,
"--test-isolation-max-loop": 100,
}.get(opt, False)
item = MagicMock()
item.fspath = str(test_file)
session.items = [item]
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
pytest_collection_finish(session)
# Should only emit advisory warnings, not fail
assert any("advisory" in str(warning.message).lower() for warning in w)
class TestSubprocessAudit:
"""Tests for the _SubprocessAudit runtime wrapper (lines 157-195)."""
def test_ensure_installed_wraps_subprocess(self) -> None:
audit = _SubprocessAudit()
original_run = subprocess.run
try:
audit._ensure_installed()
assert audit._installed is True
assert "run" in audit._originals
# The subprocess.run should now be a wrapper, not the original
assert subprocess.run is not original_run
# Calling _ensure_installed again is a no-op (cached return)
audit._ensure_installed()
finally:
# Restore originals
for name, orig in audit._originals.items():
setattr(subprocess, name, orig)
def test_make_wrapper_records_calls_when_active(self) -> None:
audit = _SubprocessAudit()
mock_original = MagicMock(return_value="result")
wrapper = audit._make_wrapper("run", mock_original)
audit.start_test()
result = wrapper(["echo", "hi"], capture_output=True)
calls = audit.stop_test()
assert result == "result"
run_calls = [c for c in calls if c[0] == "run"]
assert len(run_calls) == 1
assert "echo" in run_calls[0][1]
mock_original.assert_called_once_with(["echo", "hi"], capture_output=True)
def test_make_wrapper_records_list_cmd_truncation(self) -> None:
"""Long command lists should be truncated to first 4 elements."""
audit = _SubprocessAudit()
mock_original = MagicMock(return_value="result")
wrapper = audit._make_wrapper("run", mock_original)
audit.start_test()
wrapper(["echo", "1", "2", "3", "4", "5", "6"], capture_output=True)
calls = audit.stop_test()
run_calls = [c for c in calls if c[0] == "run"]
assert len(run_calls) == 1
assert "..." in run_calls[0][1]
def test_make_wrapper_records_string_cmd(self) -> None:
"""A string command (not list) should be recorded as-is."""
audit = _SubprocessAudit()
mock_original = MagicMock(return_value="result")
wrapper = audit._make_wrapper("run", mock_original)
audit.start_test()
wrapper("echo hi", shell=True, capture_output=True)
calls = audit.stop_test()
run_calls = [c for c in calls if c[0] == "run"]
assert len(run_calls) == 1
assert "echo hi" in run_calls[0][1]
def test_calls_not_recorded_when_inactive(self) -> None:
"""When audit is not active, calls should not be recorded."""
audit = _SubprocessAudit()
mock_original = MagicMock(return_value="result")
wrapper = audit._make_wrapper("run", mock_original)
# Don't call start_test — audit inactive
wrapper(["echo", "hi"], capture_output=True)
# stop_test returns empty since no calls recorded
calls = audit.stop_test()
assert not calls
mock_original.assert_called_once_with(["echo", "hi"], capture_output=True)
def test_start_then_stop_returns_calls(self) -> None:
"""start_test initializes calls list, stop_test returns and clears it."""
audit = _SubprocessAudit()
mock_original = MagicMock(return_value="result")
wrapper = audit._make_wrapper("run", mock_original)
audit.start_test()
wrapper(["echo"], capture_output=True)
calls = audit.stop_test()
assert len(calls) == 1
# After stop, calls is cleared (None or empty)
calls2 = audit.stop_test()
assert not calls2
def test_ensure_installed_skips_missing_funcs(self) -> None:
"""If a subprocess func is missing (None), it should be skipped (line 162)."""
audit = _SubprocessAudit()
saved = subprocess.check_output
try:
# Temporarily make check_output "missing" (None)
subprocess.check_output = None # type: ignore[assignment]
audit._ensure_installed()
# check_output should NOT be in originals (skipped)
assert "check_output" not in audit._originals
# run should still be wrapped
assert "run" in audit._originals
finally:
subprocess.check_output = saved # type: ignore[assignment]
for name, orig in audit._originals.items():
setattr(subprocess, name, orig)
class TestExtractPatchTargets:
"""Tests for _extract_patch_targets (lines 263-291)."""
def _parse_func(self, source: str) -> ast.FunctionDef:
tree = ast.parse(textwrap.dedent(source))
return tree.body[0] # type: ignore[return-value]
def test_patch_object_extracted(self) -> None:
"""patch.object(module, "name") should extract the short name."""
node = self._parse_func(
"""
def test_foo():
with patch.object(mymodule, "subprocess"):
mymodule.do_thing()
"""
)
targets = _extract_patch_targets(node)
assert "subprocess" in targets
def test_patch_object_with_module_alias(self) -> None:
"""patch.object with a module alias Name as first arg."""
node = self._parse_func(
"""
def test_foo():
with patch.object(subprocess, "run"):
subprocess.run(["echo"])
"""
)
targets = _extract_patch_targets(node)
assert "run" in targets
def test_with_patch_context_manager_extracted(self) -> None:
"""with patch("module.func") in function body should be extracted."""
node = self._parse_func(
"""
def test_foo():
with patch("mymodule.subprocess.run"):
mymodule.do_thing()
"""
)
targets = _extract_patch_targets(node)
assert "mymodule.subprocess.run" in targets
assert "run" in targets
def test_with_multiple_patch_context_managers(self) -> None:
"""with patch("a"), patch("b") should extract both."""
node = self._parse_func(
"""
def test_foo():
with patch("mod.a"), patch("mod.b"):
pass
"""
)
targets = _extract_patch_targets(node)
assert "mod.a" in targets
assert "mod.b" in targets
assert "a" in targets
assert "b" in targets
def test_patch_object_non_string_second_arg_ignored(self) -> None:
"""patch.object with non-string 2nd arg should not crash."""
node = self._parse_func(
"""
def test_foo():
with patch.object(mymodule, some_var):
pass
"""
)
targets = _extract_patch_targets(node)
assert targets == set()
class TestCallGraph:
"""Tests for CallGraph building (lines 409, 419-420, 449, 470)."""
def _make_src(self, tmp_path: Path, files: dict[str, str]) -> Path:
src = tmp_path / "src"
src.mkdir()
for rel, content in files.items():
f = src / rel
f.parent.mkdir(parents=True, exist_ok=True)
f.write_text(textwrap.dedent(content))
return src
def test_ensure_built_cached(self, tmp_path: Path) -> None:
"""_ensure_built should only build once (cached return)."""
src = self._make_src(tmp_path, {"pkg/__init__.py": "", "pkg/mod.py": "def foo():\n pass\n"})
cg = CallGraph(src)
cg._ensure_built()
assert cg._built is True
nodes_before = dict(cg._nodes)
# Second call should be a no-op
cg._ensure_built()
assert cg._nodes == nodes_before
def test_build_skips_syntax_error(self, tmp_path: Path) -> None:
"""Files with syntax errors should be skipped, not crash."""
src = self._make_src(
tmp_path,
{
"pkg/__init__.py": "",
"pkg/broken.py": "def test(:\n pass\n",
"pkg/good.py": "def foo():\n pass\n",
},
)
cg = CallGraph(src)
cg._ensure_built()
# good.py's foo should be registered, broken.py skipped
assert any("foo" in k for k in cg._nodes)
def test_build_skips_unicode_decode_error(self, tmp_path: Path) -> None:
"""Files with invalid UTF-8 should be skipped."""
src = tmp_path / "src"
src.mkdir()
(src / "pkg").mkdir()
(src / "pkg" / "__init__.py").write_text("")
(src / "pkg" / "binary.py").write_bytes(b"\xff\xfe\x00\xbad bytes")
(src / "pkg" / "good.py").write_text("def foo():\n pass\n")
cg = CallGraph(src)
cg._ensure_built()
assert any("foo" in k for k in cg._nodes)
def test_scan_node_skips_classdef(self, tmp_path: Path) -> None:
"""Methods inside classes should NOT be registered."""
src = self._make_src(
tmp_path,
{
"pkg/__init__.py": "",
"pkg/mod.py": """
class MyClass:
def my_method(self):
subprocess.run(["echo"])
def top_level():
pass
""",
},
)
cg = CallGraph(src)
cg._ensure_built()
# top_level should be registered
assert "pkg.mod.top_level" in cg._nodes
# my_method should NOT be registered (class body skipped)
assert "pkg.mod.my_method" not in cg._nodes
assert "my_method" not in cg._by_short
def test_register_function_records_io_calls(self, tmp_path: Path) -> None:
"""KNOWN_IO_FUNCTIONS calls should be recorded in io_calls."""
src = self._make_src(
tmp_path,
{
"pkg/__init__.py": "",
"pkg/mod.py": """
def foo():
get_pat("staging")
load_secrets("prod")
""",
},
)
cg = CallGraph(src)
cg._ensure_built()
node = cg._nodes["pkg.mod.foo"]
assert "get_pat" in node.io_calls
assert "load_secrets" in node.io_calls
def test_register_function_records_subprocess_calls(self, tmp_path: Path) -> None:
"""subprocess.run calls should be recorded in subprocess_calls."""
src = self._make_src(
tmp_path,
{
"pkg/__init__.py": "",
"pkg/mod.py": """
import subprocess
def foo():
subprocess.run(["echo"])
""",
},
)
cg = CallGraph(src)
cg._ensure_built()
node = cg._nodes["pkg.mod.foo"]
assert "subprocess.run" in node.subprocess_calls
class TestFindReachableDangerous:
"""Tests for find_reachable_dangerous (lines 516-580)."""
def _make_src(self, tmp_path: Path, files: dict[str, str]) -> Path:
src = tmp_path / "src"
src.mkdir()
for rel, content in files.items():
f = src / rel
f.parent.mkdir(parents=True, exist_ok=True)
f.write_text(textwrap.dedent(content))
return src
def test_import_map_resolution(self, tmp_path: Path) -> None:
"""import_map should resolve target to a precise full name."""
src = self._make_src(
tmp_path,
{
"pkg/__init__.py": "",
"pkg/mod.py": """
import subprocess
def main():
subprocess.run(["echo"])
""",
},
)
cg = CallGraph(src)
dangerous = cg.find_reachable_dangerous("main", set(), import_map={"main": "pkg.mod.main"})
assert len(dangerous) == 1
assert "subprocess" in dangerous[0][1]
def test_import_map_falls_back_to_short_name(self, tmp_path: Path) -> None:
"""If import_map value not in nodes, fall back to short name (line 516)."""
src = self._make_src(
tmp_path,
{
"pkg/__init__.py": "",
"pkg/mod.py": """
import subprocess
def main():
subprocess.run(["echo"])
""",
},
)
cg = CallGraph(src)
# import_map points to a non-existent full name → fallback to by_short
dangerous = cg.find_reachable_dangerous("main", set(), import_map={"main": "nonexistent.pkg.main"})
assert len(dangerous) == 1
def test_no_candidates_returns_empty(self, tmp_path: Path) -> None:
"""If no candidates found, return empty list (line 526)."""
src = self._make_src(tmp_path, {"pkg/__init__.py": ""})
cg = CallGraph(src)
dangerous = cg.find_reachable_dangerous("nonexistent", set())
assert dangerous == []
def test_fully_qualified_name_candidate(self, tmp_path: Path) -> None:
"""A fully-qualified target_name in nodes should be used directly (line 519)."""
src = self._make_src(
tmp_path,
{
"pkg/__init__.py": "",
"pkg/mod.py": """
import subprocess
def main():
subprocess.run(["echo"])
""",
},
)
cg = CallGraph(src)
dangerous = cg.find_reachable_dangerous("pkg.mod.main", set())
assert len(dangerous) == 1
def test_short_name_fallback(self, tmp_path: Path) -> None:
"""target_name not in nodes falls back to short name (line 522)."""
src = self._make_src(
tmp_path,
{
"pkg/__init__.py": "",
"pkg/mod.py": """
import subprocess
def main():
subprocess.run(["echo"])
""",
},
)
cg = CallGraph(src)
# "pkg.main" is not a full name in nodes, so it falls back to "main"
dangerous = cg.find_reachable_dangerous("pkg.main", set())
assert len(dangerous) == 1
def test_visited_prevents_infinite_loop(self, tmp_path: Path) -> None:
"""Visited set prevents infinite loops (line 535)."""
src = self._make_src(
tmp_path,
{
"pkg/__init__.py": "",
"pkg/mod.py": """
def a():
b()
def b():
a()
subprocess.run(["echo"])
""",
},
)
cg = CallGraph(src)
dangerous = cg.find_reachable_dangerous("a", set())
assert len(dangerous) == 1
def test_depth_limit_stops_traversal(self, tmp_path: Path) -> None:
"""max_depth should stop traversal (line 534)."""
src = self._make_src(
tmp_path,
{
"pkg/__init__.py": "",
"pkg/mod.py": """
def a():
b()
def b():
c()
def c():
subprocess.run(["echo"])
""",
},
)
cg = CallGraph(src)
# With max_depth=0, only the direct node is visited
dangerous = cg.find_reachable_dangerous("a", set(), max_depth=0)
assert dangerous == []
def test_node_not_found_continues(self, tmp_path: Path) -> None:
"""If a queued node isn't in _nodes, continue (line 540)."""
src = self._make_src(tmp_path, {"pkg/__init__.py": ""})
cg = CallGraph(src)
# Manually inject a candidate that doesn't exist in nodes
cg._by_short["ghost"] = ["pkg.mod.ghost"]
dangerous = cg.find_reachable_dangerous("ghost", set())
assert dangerous == []
def test_io_calls_checked(self, tmp_path: Path) -> None:
"""IO calls should be reported as dangerous (lines 551-554)."""
src = self._make_src(
tmp_path,
{
"pkg/__init__.py": "",
"pkg/mod.py": """
def main():
get_pat("staging")
""",
},
)
cg = CallGraph(src)
dangerous = cg.find_reachable_dangerous("main", set())
assert len(dangerous) == 1
assert "PAT" in dangerous[0][1] or "get_pat" in str(dangerous)
def test_io_calls_patched_skipped(self, tmp_path: Path) -> None:
"""Patched IO calls should not be reported."""
src = self._make_src(
tmp_path,
{
"pkg/__init__.py": "",
"pkg/mod.py": """
def main():
get_pat("staging")
""",
},
)
cg = CallGraph(src)
dangerous = cg.find_reachable_dangerous("main", {"get_pat"})
assert dangerous == []
def test_patched_helper_skipped_in_enqueue(self, tmp_path: Path) -> None:
"""A patched helper should not be enqueued (lines 559-560)."""
src = self._make_src(
tmp_path,
{
"pkg/__init__.py": "",
"pkg/mod.py": """
def main():
run_cmd()
def run_cmd():
subprocess.run(["echo"])
""",
},
)
cg = CallGraph(src)
# run_cmd is patched → should not traverse into it
dangerous = cg.find_reachable_dangerous("main", {"run_cmd"})
assert dangerous == []
def test_same_module_resolution(self, tmp_path: Path) -> None:
"""Calls within the same module should prefer same-module resolution (lines 566-567)."""
src = self._make_src(
tmp_path,
{
"pkg/__init__.py": "",
"pkg/mod.py": """
def main():
helper()
def helper():
subprocess.run(["echo"])
""",
},
)
cg = CallGraph(src)
dangerous = cg.find_reachable_dangerous("main", set())
assert len(dangerous) == 1
def test_short_name_single_match_resolution(self, tmp_path: Path) -> None:
"""A single global match by short name should be resolved (lines 571-572)."""
src = self._make_src(
tmp_path,
{
"pkg/__init__.py": "",
"pkg/mod.py": """
def main():
helper()
""",
"pkg/other.py": """
import subprocess
def helper():
subprocess.run(["echo"])
""",
},
)
cg = CallGraph(src)
dangerous = cg.find_reachable_dangerous("main", set())
assert len(dangerous) == 1
def test_is_patched_endswith(self, tmp_path: Path) -> None:
"""_is_patched should match patches ending with .short (line 580)."""
src = self._make_src(
tmp_path,
{
"pkg/__init__.py": "",
"pkg/mod.py": """
import subprocess
def main():
subprocess.run(["echo"])
""",
},
)
cg = CallGraph(src)
# "devx.ci.release.subprocess.run" ends with ".run"
dangerous = cg.find_reachable_dangerous("main", {"devx.ci.release.subprocess.run"})
assert dangerous == []
def test_is_patched_full_name_match(self) -> None:
"""_is_patched should match exact full name."""
assert CallGraph._is_patched("subprocess.run", "run", {"subprocess.run"}) is True
def test_is_patched_short_name_match(self) -> None:
"""_is_patched should match short name in patches."""
assert CallGraph._is_patched("subprocess.run", "run", {"run"}) is True
def test_is_patched_no_match(self) -> None:
"""_is_patched should return False when not patched."""
assert CallGraph._is_patched("subprocess.run", "run", {"other"}) is False
def test_is_patched_endswith_no_false_positive(self) -> None:
"""endswith should not match substrings (e.g. 'run' vs 'run_cmd')."""
assert CallGraph._is_patched("mod.run_cmd", "run_cmd", {"mod.run"}) is False
class TestVisitCallAttributeTarget:
"""Tests for visit_Call with ast.Attribute target (lines 851-862)."""
def test_invoke_with_module_func_attribute(self, tmp_path: Path) -> None:
"""runner.invoke(module.func) should resolve via import_map."""
src = tmp_path / "src"
src.mkdir()
(src / "pkg").mkdir()
(src / "pkg" / "__init__.py").write_text("")
(src / "pkg" / "cli.py").write_text("import subprocess\ndef main():\n subprocess.run(['echo'])\n")
test_file = tmp_path / "test_example.py"
test_file.write_text(
textwrap.dedent(
"""
from click.testing import CliRunner
import pkg.cli as cli_mod
class TestExample:
def test_invoke(self) -> None:
runner = CliRunner()
result = runner.invoke(cli_mod.main, [])
"""
)
)
cg = CallGraph(src)
violations = analyze_file(test_file, call_graph=cg)
transitive = [v for v in violations if v.category == "transitive-subprocess"]
assert len(transitive) == 1
def test_invoke_with_attribute_no_import_map(self, tmp_path: Path) -> None:
"""runner.invoke(mod.func) where mod not in import_map uses attr only (line 860)."""
src = tmp_path / "src"
src.mkdir()
(src / "pkg").mkdir()
(src / "pkg" / "__init__.py").write_text("")
(src / "pkg" / "cli.py").write_text("import subprocess\ndef main():\n subprocess.run(['echo'])\n")
test_file = tmp_path / "test_example.py"
test_file.write_text(
textwrap.dedent(
"""
from click.testing import CliRunner
class TestExample:
def test_invoke(self) -> None:
runner = CliRunner()
# unknown_mod not imported, so falls back to attr name
result = runner.invoke(unknown_mod.main, [])
"""
)
)
cg = CallGraph(src)
violations = analyze_file(test_file, call_graph=cg)
transitive = [v for v in violations if v.category == "transitive-subprocess"]
assert len(transitive) == 1
def test_invoke_with_attribute_non_name_value(self, tmp_path: Path) -> None:
"""runner.invoke(get_obj().func) — target.value is not a Name (line 862)."""
src = tmp_path / "src"
src.mkdir()
(src / "pkg").mkdir()
(src / "pkg" / "__init__.py").write_text("")
(src / "pkg" / "cli.py").write_text("import subprocess\ndef main():\n subprocess.run(['echo'])\n")
test_file = tmp_path / "test_example.py"
test_file.write_text(
textwrap.dedent(
"""
from click.testing import CliRunner
class TestExample:
def test_invoke(self) -> None:
runner = CliRunner()
result = runner.invoke(CliRunner().main, [])
"""
)
)
cg = CallGraph(src)
violations = analyze_file(test_file, call_graph=cg)
transitive = [v for v in violations if v.category == "transitive-subprocess"]
assert len(transitive) == 1
class TestIsIntegrationTest:
"""Tests for _is_integration_test (lines 1076-1080)."""
def test_marker_based_integration(self) -> None:
"""A test item with 'integration' in keywords should be detected."""
item = MagicMock()
item.keywords = {"integration", "test_foo"}
item.fspath = "tests/unit/test_foo.py"
assert _is_integration_test(item) is True
def test_path_based_integration(self) -> None:
"""A test item in an integration/ directory should be detected."""
item = MagicMock()
item.keywords = {"test_foo"}
item.fspath = "tests/integration/test_foo.py"
assert _is_integration_test(item) is True
def test_not_integration_test(self) -> None:
"""A regular test item should not be detected as integration."""
item = MagicMock()
item.keywords = {"test_foo"}
item.fspath = "tests/unit/test_foo.py"
assert _is_integration_test(item) is False
def test_no_keywords_attr(self) -> None:
"""An item without keywords attr should use fspath only."""
item = MagicMock()
item.keywords = {}
item.fspath = "tests/unit/test_foo.py"
assert _is_integration_test(item) is False
class TestLoadTestIsolationConfig:
"""Tests for _load_test_isolation_config — project-specific rule merging."""
def test_merges_io_functions(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Project-specific io_functions are added to KNOWN_IO_FUNCTIONS."""
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text(
'[tool.devx.check_test_isolation]\nio_functions = { "my_custom_io" = "reads from disk" }\n'
)
monkeypatch.chdir(tmp_path)
_load_test_isolation_config()
assert "my_custom_io" in KNOWN_IO_FUNCTIONS
assert KNOWN_IO_FUNCTIONS["my_custom_io"] == "reads from disk"
def test_merges_subprocess_helpers(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Project-specific subprocess_helpers are added."""
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text(
'[tool.devx.check_test_isolation]\nsubprocess_helpers = { "my_sp_helper" = "calls subprocess.run" }\n'
)
monkeypatch.chdir(tmp_path)
_load_test_isolation_config()
assert "my_sp_helper" in KNOWN_SUBPROCESS_HELPERS
def test_merges_helper_internal_calls(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Project-specific helper_internal_calls are merged."""
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text(
'[tool.devx.check_test_isolation]\nhelper_internal_calls = { "my_helper" = ["subprocess", "run_cmd"] }\n'
)
monkeypatch.chdir(tmp_path)
_load_test_isolation_config()
assert "my_helper" in HELPER_INTERNAL_CALLS
assert HELPER_INTERNAL_CALLS["my_helper"] == {"subprocess", "run_cmd"}
def test_merges_io_internal_calls(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Project-specific io_internal_calls are merged."""
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text(
'[tool.devx.check_test_isolation]\nio_internal_calls = { "my_io_func" = ["open", "yaml"] }\n'
)
monkeypatch.chdir(tmp_path)
_load_test_isolation_config()
assert "my_io_func" in IO_INTERNAL_CALLS
assert IO_INTERNAL_CALLS["my_io_func"] == {"open", "yaml"}
def test_merges_heavy_module_imports(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Project-specific heavy_module_imports are merged."""
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text('[tool.devx.check_test_isolation]\nheavy_module_imports = { "mymodule" = 150.0 }\n')
monkeypatch.chdir(tmp_path)
_load_test_isolation_config()
assert "mymodule" in HEAVY_MODULE_IMPORTS
assert HEAVY_MODULE_IMPORTS["mymodule"] == 150.0
def test_no_config_section_is_noop(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Missing [tool.devx.check_test_isolation] section is a no-op."""
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text('[tool.devx]\nother_key = "value"\n')
monkeypatch.chdir(tmp_path)
before_io = dict(KNOWN_IO_FUNCTIONS)
_load_test_isolation_config()
assert before_io == KNOWN_IO_FUNCTIONS
def test_no_pyproject_is_noop(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""No pyproject.toml at all is a no-op."""
monkeypatch.chdir(tmp_path)
before = dict(KNOWN_SUBPROCESS_HELPERS)
_load_test_isolation_config()
assert before == KNOWN_SUBPROCESS_HELPERS
def test_non_dict_config_is_noop(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""A non-dict check_test_isolation section is a no-op."""
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text('[tool.devx]\ncheck_test_isolation = "not_a_dict"\n')
monkeypatch.chdir(tmp_path)
before = dict(HEAVY_MODULE_IMPORTS)
_load_test_isolation_config()
assert before == HEAVY_MODULE_IMPORTS
def test_invalid_entry_types_are_skipped(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Entries with wrong types (non-str values) are silently skipped."""
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text(
"[tool.devx.check_test_isolation]\n"
'io_functions = { "good_func" = "desc", "bad_func" = 123 }\n'
'heavy_module_imports = { "good_mod" = 100.0, "bad_mod" = "fast" }\n'
)
monkeypatch.chdir(tmp_path)
_load_test_isolation_config()
assert "good_func" in KNOWN_IO_FUNCTIONS
assert "bad_func" not in KNOWN_IO_FUNCTIONS
assert "good_mod" in HEAVY_MODULE_IMPORTS
assert "bad_mod" not in HEAVY_MODULE_IMPORTS
def test_extends_without_replacing_defaults(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Project config adds to defaults without removing them."""
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text('[tool.devx.check_test_isolation]\nio_functions = { "project_func" = "project I/O" }\n')
monkeypatch.chdir(tmp_path)
_load_test_isolation_config()
# Default entries still present
assert "get_pat" in KNOWN_IO_FUNCTIONS
# Project entry added
assert "project_func" in KNOWN_IO_FUNCTIONS