Design Spec: Test Isolation¶
Document Control¶
| Field | Value |
|---|---|
| Status | Implemented |
| Command surface | None (test infrastructure) |
| Primary area | Testing, safety |
| Related specs | docs/design/active-transmit-safety.md, docs/design/uds-active-workflows.md |
| Issues | #531 (found during the evaluation tracked in #532) |
Goal¶
Running pytest must not read, write, or transmit anything belonging to the
person running it. A contributor should get that guarantee by default,
without opting in, and a test that breaks it should fail immediately and say
which test it was.
User-Facing Motivation¶
Before this, the suite depended on the machine it ran on, in three ways that were each demonstrated rather than theorised:
- It wrote to real application state. A single full run left two fuzz
campaign directories in the operator's real
~/.canarchy/findings, from twofuzz guidedtests that ran an active campaign without passing--findings-dir. Thirty-eight had accumulated on the evaluation machine. - It read real configuration. With a
~/.canarchy/config.tomlsettinginterface = "udp_multicast"and adefault_interface, nine tests failed and python-can loggedUdpMulticastBus was not properly shut down— the suite had opened a real multicast socket. - Its default pointed at hardware.
transport_backend_config()defaults to thepython-canbackend onsocketcan. On a machine with a live interface, a test that reaches a bus with no configuration at all would open the real one.
The third point is why configuration isolation alone is insufficient, and the first is why a bus guard alone is insufficient. Both are needed.
The deeper problem is diagnostic: when the suite's result depends on the developer's configuration, a failure means "something is different about this machine" rather than "the code is wrong", and a pass means nothing about anyone else's machine.
Requirements¶
| ID | Type | Requirement |
|---|---|---|
REQ-TISO-01 |
Ubiquitous | The test suite shall resolve Path.home() to a per-test temporary directory by default, so configuration, caches, session state, plugin state, metadata overrides, and fuzz findings never resolve to the operator's own. |
REQ-TISO-02 |
Ubiquitous | The test suite shall remove every CANARCHY_* environment variable from a test's environment by default. |
REQ-TISO-03 |
Ubiquitous | The system shall redirect state paths that captured Path.home() at import time, which the HOME redirection alone does not move. |
REQ-TISO-04 |
Unwanted behaviour | If a test opens a real CAN interface through python_can.Bus, the system shall fail that test immediately with a message naming the test and the requested interface. |
REQ-TISO-05 |
Optional feature | Where python-can's in-process virtual interface is requested, the system shall permit it, since it opens no socket and reaches no hardware. |
REQ-TISO-06 |
Optional feature | Where a test is marked integration, the system shall skip isolation and the bus guard for it. |
REQ-TISO-07 |
Ubiquitous | The system shall exclude integration-marked tests from the default run and require explicit selection with -m integration. |
REQ-TISO-08 |
Ubiquitous | Isolation shall not conceal configuration-sensitive product behaviour: tests covering configuration precedence shall continue to set a configured default interface through their own explicit fixtures. |
REQ-TISO-09 |
Ubiquitous | The suite shall produce the same result whether or not the operator has a ~/.canarchy/config.toml. |
Design¶
Two autouse fixtures in tests/conftest.py, both active by default.
isolate_canarchy_state¶
Redirects HOME and USERPROFILE to a per-test temporary directory
(REQ-TISO-01) and deletes every CANARCHY_* variable (REQ-TISO-02).
The redirection is done through the environment, not by patching
Path.home. Path.home() resolves ~ via HOME on POSIX and USERPROFILE
on Windows, so setting them moves in-process code and subprocess-based
tests such as the stdio MCP server, which inherit the environment. Patching
Path.home to a fixed value would instead override a test that sets up its
own HOME — tests/test_session.py::workspace does exactly that — silently
pointing the code under test somewhere the test never wrote to.
A handful of modules bind Path.home() into module-level constants at import
time, before any fixture runs, so the environment redirection does not move
them. These are re-pointed explicitly (REQ-TISO-03):
| Module | Constant |
|---|---|
canarchy.dbc_cache |
_CACHE_ROOT, _CONFIG_PATH |
canarchy.skills_cache |
_CACHE_ROOT, _CONFIG_PATH |
canarchy.j1587_metadata |
_PID_OVERRIDES_DEFAULT |
canarchy.j1939_metadata |
_SPN_OVERRIDES_DEFAULT |
canarchy.j2497_metadata |
_MID_OVERRIDES_DEFAULT |
forbid_real_can_bus¶
python_can.Bus(...) in PythonCanBackend._open_bus is the single place a
real interface is opened, so the guard wraps exactly that (REQ-TISO-04).
The virtual interface is allowed through (REQ-TISO-05): it is an
in-process message queue with no socket and no hardware, the CAN equivalent
of the localhost server the web tests bind. Every other interface —
socketcan, udp_multicast, pcan, vector, kvaser — reaches a real bus
or the network and is refused.
Tests that legitimately exercise the python-can backend patch
PythonCanBackend._open_bus and never reach the guard, so it costs them
nothing.
What isolation deliberately does not do¶
REQ-TISO-08 is the constraint that shapes the rest. Making the suite green
by hiding configuration-sensitivity would be worse than the original state:
530 — where a configured default interface turned a reference catalog lookup¶
into unacknowledged active bus probing — was found because a test inherited real configuration and failed.
So the uds services regressions from #530 continue to set a configured
default interface, through their own _load_user_config patches and
patch.dict(os.environ, ...), rather than inheriting whatever the developer
happens to have. Isolation makes that coverage deliberate and identical on
every machine; it does not remove it. This is verified by reintroducing the
530 defect and confirming the regressions still fail under isolation¶
(TEST-TISO-14).
Error Cases¶
| Condition | Behaviour |
|---|---|
A test opens a non-virtual bus |
AssertionError naming the test node id, the channel, and the interface, plus the four ways to fix it |
A test is marked integration |
Isolation and the guard both stand down; the test is deselected unless -m integration is passed |
| python-can is not installed | The guard stands down; there is no bus to open |
Out Of Scope¶
- Sandboxing filesystem access outside
~/.canarchy— tests legitimately write totmp_pathand read repository fixtures. - Blocking all network access. The web tests bind a localhost server on purpose; the guard targets CAN interfaces specifically, which is where the operator-visible risk is.
- Retrofitting every existing test onto shared fixtures. Tests that already isolate correctly are left alone.