0
mirror of https://github.com/bbenchoff/OrthoRoute.git synced 2026-08-22 14:19:15 +00:00
Files
OrthoRoute/tests/README.md

433 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# OrthoRoute Test Suite
All test files live in this directory. No test-related documentation exists outside it.
```
tests/
├── conftest.py # Shared fixtures (board, router, routing_result, log)
├── unit/ # Fast headless tests (~2 s, no KiCad required)
│ ├── test_domain_models.py
│ ├── test_logging_utils.py
│ ├── test_performance_utils.py
│ └── test_routing_engine.py
└── regression/ # Full-pipeline tests (require KiCad run first)
├── run_headless_routing.py # Full headless backplane routing + golden comparison
├── test_backplane.py
├── golden_board.json # Board signature — update if .kicad_pcb changes
└── golden_metrics.json # Routing baselines — update after algorithm improvements
```
---
## Quick start
```powershell
# Unit tests only (fast, no KiCad required)
python -m pytest tests/unit/ -v
# Unit tests with coverage report
python -m pytest tests/unit/ -v --cov=orthoroute --cov-report=term-missing
# Full regression suite (needs logs/latest.log from a KiCad run)
python -m pytest tests/regression/ -v
# Full headless backplane route + timeout/log completion check + golden compare
python tests/regression/run_headless_routing.py
# Log-only validation against timeout/completion (no reroute)
python tests/regression/run_headless_routing.py --log-only
# Everything
python -m pytest tests/ -v
```
---
## Unit tests — current coverage
**Result:** 26 passed — no failures, no skips
**Last verified:** 2026-04-05
| File under test | Test file | Tests | Notes |
|---|---|---|---|
| `domain/models/board.py` | `test_domain_models.py` | 12 | `Coordinate`, `Bounds`, `Pad`, `Net`, `Board` |
| `shared/utils/logging_utils.py` | `test_logging_utils.py` | 3 | `init_logging()` — console/file handler levels |
| `shared/utils/performance_utils.py` | `test_performance_utils.py` | 3 | `@profile_time` — return value, no-op, debug emit |
| `algorithms/manhattan/unified_pathfinder.py` | `test_routing_engine.py` | 8 | `PathFinderConfig`, `UnifiedPathFinder` init, GPU, pipeline API |
### test_domain_models.py — 12 tests
| Class | Test | What it checks |
|---|---|---|
| `TestCoordinate` | `test_creation` | `Coordinate(x, y)` constructs |
| `TestCoordinate` | `test_immutable` | Frozen dataclass rejects mutation |
| `TestCoordinate` | `test_equality` | Value equality on identical coords |
| `TestBounds` | `test_width_height` | `Bounds.width` / `.height` computed properties |
| `TestPad` | `test_minimal_pad` | `Pad(id, component_id, net_id, position, size)` |
| `TestPad` | `test_pad_defaults` | Optional fields are None by default |
| `TestNet` | `test_net_has_pads` | Pads attached via `Net.pads` list |
| `TestNet` | `test_empty_net_not_routable` | `Net.is_routable``False` with < 2 pads |
| `TestNet` | `test_net_with_two_pads_is_routable` | `Net.is_routable` `True` with 2 pads |
| `TestBoard` | `test_board_holds_nets` | `Board.nets` list populated |
| `TestBoard` | `test_board_net_count` | `len(board.nets)` correct |
| `TestBoard` | `test_board_add_net` | Nets can be appended post-construction |
### test_logging_utils.py — 3 tests
| Class | Test | What it checks |
|---|---|---|
| `TestInitLogging` | `test_normal_mode_console_level` | Console handler level is `ERROR` (not WARNING) |
| `TestInitLogging` | `test_debug_mode_file_level` | `ORTHO_DEBUG=1` sets file handler to `DEBUG` |
| `TestInitLogging` | `test_no_debug_console_filters_warnings` | Normal mode console suppresses WARNING |
### test_performance_utils.py — 3 tests
| Class | Test | What it checks |
|---|---|---|
| `TestProfileTime` | `test_returns_correct_value` | Decorated function return value preserved |
| `TestProfileTime` | `test_noop_without_debug` | No log emitted when `ORTHO_DEBUG` unset |
| `TestProfileTime` | `test_emits_profile_log_with_debug` | `[PROFILE]` WARNING log emitted when `ORTHO_DEBUG=1` |
### test_routing_engine.py — 8 tests
| Class | Test | Marker | What it checks |
|---|---|---|---|
| `TestPathFinderConfig` | `test_default_config_instantiation` | | `PathFinderConfig()` creates without error |
| `TestPathFinderConfig` | `test_config_has_hotset_cap` | | `hotset_cap` present and > 0 |
| `TestUnifiedPathFinderInit` | `test_instantiation_cpu_mode` | — | CPU-mode router constructs |
| `TestUnifiedPathFinderInit` | `test_instantiation_gpu_mode` | `requires_gpu` | GPU-mode router constructs when CUDA available |
| `TestUnifiedPathFinderInit` | `test_cuda_dijkstra_importable` | `requires_gpu` | `CUDADijkstra` kernel class importable |
| `TestUnifiedPathFinderInit` | `test_has_required_pipeline_methods` | — | `initialize_graph`, `map_all_pads`, `route_multiple_nets` callable |
| `TestRouteMultipleNetsReturnShape` | `test_empty_request_returns_minimal_dict` | — | Empty input → returns `{}` (no crash) |
| `TestRouteMultipleNetsReturnShape` | `test_required_keys_are_defined` | — | Non-empty result includes all 15 required keys |
### What is NOT covered by unit tests
These areas belong in `tests/regression/` or require a running KiCad process:
| Area | Why excluded |
|---|---|
| `infrastructure/kicad/rich_kicad_interface.py` | Requires KiCad IPC process |
| `infrastructure/kicad/file_parser.py` | Covered by dedicated parser tests and headless regression runner |
| `presentation/pipeline.py` — full pipeline run | Requires a board with pads |
| `algorithms/manhattan/unified_pathfinder.py``_pathfinder_negotiation` | ~6 k-line monolith; full run too slow for unit suite |
| `presentation/gui/main_window.py` | Requires PyQt6 display |
### GPU test behaviour
Two tests are gated with `@requires_gpu` (auto-skipped when no CUDA device is present):
```python
requires_gpu = pytest.mark.skipif(not _gpu_available(), reason="No CUDA GPU available")
```
Current platform: **NVIDIA T1200 Laptop GPU, 4 GB, CuPy 14.0.1** — both GPU tests run and pass.
### Adding new unit tests
1. Create `tests/unit/test_<module>.py`.
2. One test class per public API — no full routing runs.
3. Guard GPU/KiCad imports with `pytest.importorskip` or a `requires_x` marker.
---
## Regression tests — overview
Board under test: **TestBackplane.kicad_pcb** — 18 copper layers, 1,604 pads, 512 routable nets.
### Test groups
| Group | Class(es) | Trigger | Current status |
|---|---|---|---|
| **A** — Log health | `TestLogHealth`, `TestIterationMetrics` | `log_content` fixture (needs `logs/latest.log`) | Runs when a routing log exists |
| **A2** — Board load | `TestBoardLoad` | `log_content` fixture | Skipped (no log file headlessly) |
| **B** — Routing quality | `TestRoutingQuality` | `routing_result` fixture (board must load with ≥ 10 pads) | Runs with KiCad or headless parser load |
| **C** — Write-back | `TestWriteBack` | `routing_result` + KiCad IPC | Skipped (KiCad IPC not available headlessly) |
Groups B and C activate automatically when run inside a KiCad session (IPC adapter) or when the file parser successfully loads the board.
#### Why Groups A/A2 skip headlessly
The `log_path` fixture calls `pytest.skip()` when `logs/latest.log` doesn't exist.
Run OrthoRoute via KiCad with `ORTHO_DEBUG=1` first — then re-run the regression suite.
#### Why Group C still skips headlessly
Write-back tests require an active KiCad IPC session. Headless routing validates
algorithmic convergence but does not apply tracks/vias to a live KiCad board.
### Pass/fail policy
**Hard fail**`AssertionError` → build broken, must fix before merging.
**Soft warn**`warnings.warn(UserWarning)` → visible in output, never blocks CI.
| Test | Hard fail? | What it checks |
|---|---|---|
| `test_gpu_mode_detected` | soft warn | GPU preferred; warns if CPU used |
| `test_gpu_mode_matches_available_hardware` | soft warn | GPU=YES ↔ CUDA kernels compiled |
| `test_all_nets_routed` | **YES** | `nets_routed == total_nets` |
| `test_convergence` | **YES** | `converged == True` |
| `test_result_has_required_key[*]` | **YES** | All 15 return-dict keys present |
| `test_iteration_budget` | soft warn | Iterations ≤ `active_metrics.iterations_max` |
| `test_total_time` | soft warn | Total time ≤ mode-specific threshold |
| `test_overuse_final` | soft warn | `overuse_final == 0` |
| `test_iter_stability` | soft warn | No iteration >3× mode avg limit |
| `test_no_barrel_conflicts` | soft warn | `barrel_conflicts == 0` |
| `test_no_excluded_nets` | soft warn | `excluded_nets == 0` |
### Golden files
| File | Purpose | When to update |
|---|---|---|
| `regression/golden_board.json` | Board signature (pads, layers, lattice size, …) | When `TestBackplane.kicad_pcb` changes |
| `regression/golden_metrics.json` | Routing baseline thresholds | After an intentional algorithm improvement |
### How to run a full regression
```powershell
# 1. Route the board via KiCad with debug logging
$env:ORTHO_DEBUG = '1'
# launch KiCad, open TestBoards/TestBackplane.kicad_pcb, run the plugin
# 2. Run all regression groups
python -m pytest tests/regression/ -v
# 3. Headless CI — only unit tests run without KiCad
python -m pytest tests/unit/ -v
```
---
## Regression test metrics
Track routing quality results over time.
`iteration_metrics` values come from the `[ITER N]` log lines written by `_pathfinder_negotiation()`.
### Baselines (`golden_metrics.json`)
The file contains separate `"gpu"` and `"cpu"` blocks. The `active_metrics` conftest fixture auto-selects the right block by reading `PathFinder loaded (GPU=YES/NO)` from the log.
#### GPU baseline — NVIDIA T1200 Laptop (4 GB)
| Metric | Threshold | Type |
|---|---|---|
| `nets_routed` | 512 / 512 | HARD FAIL if < 100% |
| `converged` | `true` | HARD FAIL if false |
| `iterations_max` | 60 | SOFT WARN if exceeded |
| `total_time_s_max` | 900 s | SOFT WARN if exceeded |
| `iter_avg_time_s_max` | 25 s/iter | SOFT WARN if exceeded |
| `iter_1_time_s_max` | 120 s | SOFT WARN (first iter = 512 nets × ~100 ms/net) |
| `overuse_final_max` | 0 | SOFT WARN |
#### CPU-only baseline (--cpu-only flag)
| Metric | Threshold | Type |
|---|---|---|
| `nets_routed` | 512 / 512 | HARD FAIL if < 100% |
| `converged` | `true` | HARD FAIL if false |
| `iterations_max` | 120 | SOFT WARN if exceeded |
| `total_time_s_max` | 7200 s | SOFT WARN if exceeded |
| `iter_avg_time_s_max` | 120 s/iter | SOFT WARN if exceeded |
| `overuse_final_max` | 0 | SOFT WARN |
> CPU baseline values are estimates — update `golden_metrics.json["cpu"]` after first CPU run:
> ```powershell
> python main.py cli TestBoards/TestBackplane.kicad_pcb --cpu-only
> ```
Board signature: 1,604 pads · 512/1,088 routable nets · 446,472 lattice nodes · 14,281,664 edges · 5,315 existing tracks · 3,267 existing vias.
### Progress log
Add a row after each meaningful algorithm change or full KiCad run.
#### 2026-04-05 — First live KiCad run (IPC connected, GPU)
| Metric | Value | vs. baseline |
|---|---|---|
| Compute mode | GPU (T1200, 4 GB) | |
| IPC adapter | kipy IPC (pynng) | |
| Nets routed | in progress | |
| Converged | in progress | |
| Iterations | in progress | |
| Total time | in progress | |
| Avg iter time | ~0.085 s/net × 512 = ~43 s/iter | |
| Overuse (final) | in progress | |
| Existing tracks | 5,315 | (golden) |
| Existing vias | 3,267 | (golden) |
> First successful IPC connection + board load + GPU routing run.
> Board: 73×97 mm, ρ=0.161 (SPARSE), 446,472 nodes, 14,281,664 edges.
> GPU sort: 14.3M edges in 1.2 s (11.9M edges/sec).
> Update table when routing completes.
#### 2026-04-03 — Baseline run (RTX Turing GPU)
| Metric | Value | vs. baseline |
|---|---|---|
| Nets routed | 512 / 512 | PASS |
| Converged | yes | PASS |
| Iterations | | |
| Total time | | |
| Avg iter time | | |
| Overuse (final) | 0 | |
| Existing tracks | 5,315 | (golden) |
| Existing vias | 3,267 | (golden) |
> Source: `debug_output/run_20260403_184510/` log.
> This is the reference run used to populate `golden_board.json` and `golden_metrics.json`.
<!-- TEMPLATE copy for each new run
#### YYYY-MM-DD — [Description of change]
| Metric | Value | vs. baseline |
|---|---|---|
| Nets routed | ? / 512 | / / |
| Converged | yes / no | / |
| Iterations | ? | / |
| Total time | ? s | / |
| Avg iter time | ? s/iter | / |
| Overuse (final) | ? | / |
| Tracks delta | +? | / |
| Vias delta | +? | / |
> Notes: what changed, GPU / CPU, any skipped groups.
-->
### Updating baselines
```powershell
# Edit tests/regression/golden_metrics.json with new thresholds
# Edit tests/regression/golden_board.json if the board file changed
# Add a row to the Progress log above
```
`*_max` fields are soft-warn upper bounds.
`nets_routed` and `converged` are hard-fail values — do not lower them without explicit approval.
#### Original tolerance rationale
| Metric | Tolerance | Rationale |
|---|---|---|
| `nets_routed` | exact match | Must not regress routing success |
| `overuse_edges` | must be 0 | Must not regress convergence |
| `iterations` | ±10% | PathFinder is non-deterministic |
| `total_time_s` | ≤ 110% of golden | Must not be more than 10% slower |
> The current implementation uses `*_max` thresholds in `golden_metrics.json` rather than percentage tolerances. The table above is the original design intent — useful when deciding how tight to set thresholds.
#### Future: `--export-metrics` CLI flag
```powershell
python main.py cli TestBoards/TestBackplane.kicad_pcb --export-metrics tests/regression/golden_metrics.json
```
Would emit routing result as JSON directly from the routing run. **Not yet implemented.**
---
## TODO — unit tests still needed
Priority order: top items cover logic most likely to silently regress.
### 🔴 HIGH — routing algorithm core
| Module | Class / function | Test ideas |
|---|---|---|
| `unified_pathfinder.py` | `Lattice3D` / `_build_lattice` | `node_count == Nx × Ny × Nz`; no diagonal adjacency; H/V layer discipline alternates |
| `unified_pathfinder.py` | CSR construction | `indptr[-1] == len(indices)`; no self-loops; every node degree ≥ 2 |
| `unified_pathfinder.py` | `EdgeAccountant.commit_path` / `clear_path` | Usage increments on commit; decrements on clear; `verify_present_matches_canonical` passes |
| `unified_pathfinder.py` | via pooling — `via_col_use` | Column usage increments correctly; capacity limit enforced; idempotent at cap |
**Skeleton — lattice:**
```python
# tests/unit/test_lattice.py
def test_lattice_node_count():
lat = Lattice3D(bounds=(0, 0, 10, 10), pitch=1.0, layers=6)
assert lat.num_nodes == lat.x_steps * lat.y_steps * 6
def test_layer_directions_alternate():
lat = Lattice3D(bounds=(0, 0, 10, 10), pitch=1.0, layers=4)
assert lat.get_legal_axis(0) == 'v' # F.Cu = vertical
assert lat.get_legal_axis(1) == 'h' # In1 = horizontal
```
**Skeleton — via pooling:**
```python
# tests/unit/test_via_pooling.py
def test_via_column_accounting():
pf = UnifiedPathFinder(config=PathFinderConfig(), use_gpu=False)
pf._increment_via_column_use(x=5, y=10)
assert pf.via_col_use[5, 10] == 1
```
### 🟡 MEDIUM — infrastructure adapters
| Module | What to test |
|---|---|
| `infrastructure/kicad/file_parser.py` | `load_board()` on a small synthetic `.kicad_pcb` returns correct pad/net counts |
| `infrastructure/kicad/rich_kicad_interface.py` | nm→mm coordinate conversion; layer name normalisation (`BL_F_Cu``F.Cu`); keepout dict schema keys |
| `algorithms/manhattan/pad_escape_planner.py` | Escape via within board bounds; DRC clearance from neighbouring pads; retry on conflict |
### 🟡 MEDIUM — domain services
| Module | What to test |
|---|---|
| `domain/services/drc_checker.py` (if exists) | Clearance violation detected; keepout region blocks routing |
| `algorithms/manhattan/parameter_derivation.py` | Derived pitch / clearance values in valid range for known inputs |
### 🟢 LOW — CLI integration
| What to test |
|---|
| `main.py cli TestBoards/TestBackplane.kicad_pcb` exits 0 and emits routing summary (subprocess smoke test) |
| Future `--export-metrics <path>` flag writes valid JSON |
---
## TODO — regression improvements still needed
### 🔴 HIGH — make Groups B/C run headlessly
| Item | Detail |
|---|---|
| Replace `KiCadFileParser` with a fixture-level mock board | Build a `Board` domain object directly from the known golden values (1,604 pads, 512 nets) so Groups B/C run without KiCad IPC or a real file parse |
| Add `--export-metrics` flag to `main.py` | Emit routing result as JSON; allows CI to ingest metrics without parsing log files |
**Mock board skeleton:**
```python
# tests/conftest.py (alternative board_object path)
def _make_synthetic_board():
"""Build a Board from golden_board.json values — no file I/O needed."""
from orthoroute.domain.models.board import Board, Net, Pad, Coordinate
board = Board(id="golden", name="TestBackplane")
# populate 512 routable nets with 2 pads each from stored positions
# (positions can be loaded from a compact fixture CSV)
return board
```
### 🟡 MEDIUM — expand Group A log checks
| Item | Detail |
|---|---|
| Assert `[STEP5]` deterministic lattice line present | Detects silent fallback to wrong lattice size |
| Assert IPC adapter chosen (not SWIG or file fallback) | `test_ipc_adapter_used` is currently a soft warn; make HARD FAIL once IPC is stable |
| Assert `[PREFLIGHT]` and `[LATTICE]` lines both appear | Confirms pipeline steps 2 and 4 ran; catches short-circuits |
| Assert per-iteration `nets_routed` is monotonically non-decreasing | Detects net loss between iterations |
### 🟡 MEDIUM — expand Group B routing quality checks
| Item | Detail |
|---|---|
| Track/via delta exact counts (not just `> 0`) | Record `tracks_after tracks_before`; warn if < `tracks_delta_min` |
| Per-net failure list in result | `failed_nets` should be empty; log net names for diagnosis if not |
| `iteration_metrics` list non-empty | Confirms `_pathfinder_negotiation` populated the per-iter dict |
| `iteration_metrics[-1].nets_routed == total_nets` | Final iteration must have all nets placed |
### 🟢 LOW — CI integration
| Item | Detail |
|---|---|
| GitHub Actions workflow running `tests/unit/` on every push | No KiCad required; fast domain/utils regression feedback |
| Scheduled weekly run against KiCad via self-hosted runner | Requires KiCad 9+ and `ORTHO_DEBUG=1`; posts metrics to PR comment |
| Threshold auto-tightening script | After 3 consecutive runs within X% of golden, auto-reduce `*_max` values by 5% |