mirror of
https://github.com/diodeinc/pcb.git
synced 2026-09-14 20:17:19 +08:00
171 lines
5.3 KiB
Plaintext
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")
|
|
```
|