diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index fc49f03067..d6df559571 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -44,6 +44,42 @@ from esphome.writer import ( ) +@pytest.fixture(autouse=True) +def _isolate_platformio_paths(tmp_path_factory: pytest.TempPathFactory) -> Any: + """Sandbox PlatformIO path lookups so tests never touch ~/.platformio. + + `clean_all` and `clean_build` both query ProjectConfig for paths like + `cache_dir` and `core_dir` and `rmtree` anything that exists. By + default `core_dir` resolves to ~/.platformio, which is global state + shared across pytest-xdist workers — multiple workers can each pass + `is_dir()` and then race inside `shutil.rmtree`, producing + FileNotFoundError flakes (and trashing developers' local PIO state + when the suite is run outside CI). + + 14 of the 18 `clean_*` tests in this file invoke `clean_all` / + `clean_build` without installing their own ProjectConfig mock, so + making the fixture autouse is simpler than tagging each test + individually. + + Patch ProjectConfig.get_instance to point every PIO dir at a unique + tmp directory that doesn't actually exist on disk — `is_dir()` + returns False, so the rmtree loop is skipped entirely. Tests that + want to verify the PIO-cleanup branch (e.g. test_clean_all, + test_clean_all_partial_exists) install their own inner patch which + stacks on top of this one and wins for the duration of their block. + """ + pio_root = tmp_path_factory.mktemp("isolated_pio") / "nonexistent" + mock_cfg = MagicMock() + mock_cfg.get.side_effect = lambda section, option: ( + str(pio_root / option) if section == "platformio" else "" + ) + with patch( + "platformio.project.config.ProjectConfig.get_instance", + return_value=mock_cfg, + ): + yield + + @pytest.fixture def mock_copy_src_tree(): """Mock copy_src_tree to avoid side effects during tests."""