Files
2026-07-21 14:22:42 -04:00

171 lines
5.3 KiB
Plaintext

---
title: "Testing"
description: "Validate Zener modules with test benches and circuit graphs"
---
# Testing
Zener tests validate module connectivity, component properties, and circuit
topology. Define tests in `.zen` files and run them with `pcb test`.
## Define a test bench
`TestBench` evaluates a module for one or more input cases and runs each check
function against the result:
```python
MyCircuit = Module("./my_circuit.zen")
def verify_power_connections(module, inputs):
connections = module.nets.get("VCC", [])
check(len(connections) >= 2, "VCC must have at least two connections")
def verify_ground(module, inputs):
check("GND" in module.nets, "GND net is missing")
TestBench(
name = "PowerTest",
module = MyCircuit,
test_cases = {
"default": {},
},
checks = [verify_power_connections, verify_ground],
)
```
This example assumes that `MyCircuit` has no required inputs. Supply each
module input in the case dictionary when the module requires arguments.
| Parameter | Description |
|---|---|
| `name` | Test bench identifier. |
| `module` | Module loader created with `Module()`. |
| `test_cases` | Nonempty map from case names to module input dictionaries. |
| `checks` | Functions to run for each evaluated case. |
Each check receives the evaluated module and the active case's input dictionary.
Call `check(condition, message)` or `error(message)` to fail a test. An unhandled
evaluation error also fails the test. `pcb test` reports failures with their
source locations.
## Inspect the evaluated module
The evaluated module exposes its nets and components:
| Expression | Result |
|---|---|
| `module.nets` | Map from net names to connected component-port tuples. |
| `module.components` | Map from hierarchical paths to components. |
| `module["U1"]` | Direct child component or module. |
| `module["Power.U1"]` | Descendant selected by hierarchical path. |
Component values expose `name`, `type`, `pins`, `properties`, sourcing fields,
and component-specific properties such as `resistance` when present.
```python
def verify_feedback_resistor(module, inputs):
resistor = module["Feedback.R1"]
check(resistor.type == "resistor", "Feedback.R1 must be a resistor")
check(resistor.resistance.matches("10k"), "Feedback.R1 must be 10 kohm")
```
## Search circuit paths
`module.graph()` returns the circuit graph. Use `graph.paths()` to find simple
paths between component pins or public module nets:
```python
def verify_power_path(module, inputs):
graph = module.graph()
paths = graph.paths(
start = ("Regulator", "VIN"),
end = "GND_GND",
max_depth = 5,
)
check(len(paths) > 0, "No path exists from VIN to ground")
```
`start` and `end` accept a `(component, pin)` tuple or the name of a public net.
`max_depth` limits the number of traversed components and defaults to 10.
Each returned path provides:
- `ports`: traversed `(component, pin)` tuples
- `components`: traversed component values
- `nets`: traversed net names
## Match components in a path
`count`, `any`, `all`, and `none` accept a function that validates one
component. The matcher succeeds when it returns without an error:
```python
def is_resistor(component):
check(component.type == "resistor", "component is not a resistor")
resistor_count = path.count(is_resistor)
path.any(is_resistor)
path.all(is_resistor)
path.none(is_resistor)
```
`any`, `all`, and `none` fail when their condition is not satisfied.
## Match a component sequence
`path.matches()` validates the complete ordered component sequence. Each matcher
receives the path and the current component index, then returns the number of
components it consumed.
```python
def resistor(expected_value=None):
def matcher(path, cursor):
check(cursor < len(path.components), "expected a resistor at end of path")
component = path.components[cursor]
check(component.type == "resistor", component.name + " is not a resistor")
if expected_value != None:
check(component.resistance.matches(expected_value), "unexpected resistance")
return 1
return matcher
def capacitor(expected_value=None):
def matcher(path, cursor):
check(cursor < len(path.components), "expected a capacitor at end of path")
component = path.components[cursor]
check(component.type == "capacitor", component.name + " is not a capacitor")
if expected_value != None:
check(component.capacitance.matches(expected_value), "unexpected capacitance")
return 1
return matcher
```
Use the matchers in a topology check:
```python
def verify_filter(module, inputs):
paths = module.graph().paths(start=("OpAmp", "OUT"), end="GND_GND")
check(len(paths) > 0, "filter path is missing")
paths[0].matches(
resistor("1k"),
capacitor("100nF"),
resistor("10k"),
)
```
Matcher helpers are not prelude symbols. Define them in the test file or load
them from a project-local helper module.
Pass `suppress_errors=True` when a failed sequence is an expected search result.
The method then returns `False` instead of failing the test:
```python
matching_paths = [path for path in paths if path.matches(
resistor(),
capacitor(),
suppress_errors = True,
)]
check(len(matching_paths) > 0, "RC path is missing")
```