Hardware Test Teardown After an Exception: Return Outputs to an Agreed Safe State
Design hardware-test cleanup as a sequence of owned actions with explicit completion evidence, then exercise exceptions at each setup and test boundary. Preserve the original failure and every cleanup failure. If the required output state cannot be established, quarantine the fixture instead of returning it to the next test as though teardown succeeded.
The concrete software example uses Python 3.12.14 contextlib.ExitStack with in-memory fake outputs. Five exception paths were executed. No real power output, relay or instrument was controlled. Software teardown is not a safety-rated protection mechanism and cannot replace the qualified procedures or independent protection required for hazardous equipment.
Define the agreed end state
For each controlled output, specify the required state after normal completion, test failure, setup failure and communication loss. “Turn everything off” is not a universal answer. Some equipment must maintain cooling, release a load in a controlled order or preserve a state while an authorized operator intervenes.
Record who owns each resource and which shared functions must remain available. A worker cleaning up its test must not disconnect another lane’s supply or reset an instrument still owned by another operation.
Define the evidence that establishes completion: supported readback, independent observation or another qualified mechanism. A command returning successfully may only establish acceptance, not that an output reached its required state.

Pair every side effect with a recovery obligation
Before enabling a stimulus or changing a route, identify the corresponding cleanup action and its dependency order. Keep setup steps small enough that a failure can be associated with the resources already acquired or modified.
Register cleanup promptly when the relevant obligation exists. If an operation can partially change hardware and then raise an exception, a callback registered only after successful return leaves a gap. Use an operation-specific recovery wrapper or a safely pre-established cleanup obligation, and keep uncertain state explicit.
Do not invent a generic reversal for every command. Some operations are irreversible or require reconciliation rather than undo. The test plan must identify them before execution and prevent routine exception handling from attempting an unsafe compensating action.
Use the cleanup stack within its actual contract
Python 3.12.14’s ExitStack documentation describes callbacks executed in reverse registration order when the stack is closed or its with block exits. It also states that garbage collection does not implicitly invoke those callbacks. Keep cleanup attached to an explicit lifetime.
Reverse order is useful when later setup depends on earlier setup, but it is not automatically the right electrical sequence. Register or group actions according to the reviewed hardware dependency graph, not merely the order in which convenient API calls happened.
The same documentation distinguishes callbacks and context-manager exit handlers, including exception suppression behavior. Review whether a wrapper can suppress or replace the original exception. An apparently successful test after a swallowed failure is a reporting defect, not successful recovery.
Retain primary and cleanup errors separately
Keep a primary test/setup outcome and a list of cleanup outcomes. Attempt the remaining independent cleanup actions when one fails, where the approved procedure permits continuation. Do not let the first cleanup exception prevent every later action without an explicit reason.
A dependent cleanup may need to stop if a prerequisite is unresolved. For example, changing a route after failing to remove a stimulus could be prohibited. Express that dependency in the cleanup policy and report the actions intentionally withheld.
The final status should distinguish “test failed, cleanup confirmed” from “test failed, hardware state unresolved.” Both preserve the test failure, but only the former may meet the fixture’s reuse conditions.
Read the executed fake-output campaign
The model’s harmless baseline was power=off and route=idle. Setup changed power first, registered its cleanup, then changed the route and registered route cleanup. These strings are synthetic states, not a recommendation for any physical instrument.
| Injected model path | Observed cleanup | Final model state |
|---|---|---|
| Normal completion. | Route cleanup, then power cleanup. | Baseline restored. |
| Exception before setup. | No callbacks, because no modeled side effect occurred. | Baseline unchanged. |
| Exception after power setup. | Power cleanup only. | Baseline restored. |
| Test exception after both setup steps. | Route cleanup, then power cleanup. | Baseline restored; primary failure retained. |
| Test exception plus power-cleanup failure. | Route cleanup completes; power cleanup records an error. | Route idle, power unknown, fixture quarantined. |
All five paths met their assertions. The last retained both the primary test exception and the cleanup error. The fake did not model partially completed setup, a blocked callback, process termination or an instrument that acknowledges a command without applying it.
Bound cleanup without pretending it always completes
Assign supported deadlines to cleanup operations and an overall recovery budget. A cleanup routine that waits forever can occupy the fixture indefinitely and hide the original failure. A deadline expiring should produce unresolved state and escalation, not a fabricated baseline.
Python exception unwinding only helps while the process executes the relevant cleanup path. A crash, forced termination, machine power loss or broken transport can bypass it. A separate supervisor or recovery authority needs a qualified way to fence stale access and establish the hardware state before reuse.
Do not use a destructor or interpreter shutdown as the sole cleanup mechanism. Likewise, closing a communication handle is not proof that an autonomous instrument stopped generating its output.
Qualify with safe substitutes before hardware
Start with fake controllers that record every action and can fail at each boundary. Assert ordering, ownership, preserved errors and quarantine behavior. Add a fake that refuses readback so the report cannot confuse a requested state with an established one.
Move to physical equipment only under an approved procedure with safe loads, known limits and appropriate independent protection. Verify the actual outputs and the next test’s baseline. Do not deliberately strand hazardous outputs merely to demonstrate that a Python finally block runs.
The deliverable is a cleanup dependency checklist, exception campaign and unresolved-state handoff. It makes recoverable software failures orderly while keeping the limits of software control visible.