Pytest CT Framework
Scope
The pytest area runs continuous tests (CT) that combine a test case,
a fixture composition, DUT interfaces, and optional measurement or control equipment.
Every pytest CT execution resolves to one of two modes: mock or hil.
| Area | Current coverage |
|---|---|
| Communication | USB, JTAG, and Network , UART transport |
| Timing | UART baudrate error and jitter |
| Available tool implementations | USB, UART, JTAG, Network; FPGA, Saleae, Digilent |
| Result capture | Status, duration, configuration, metrics, statistics, source revision, logs |
| Runner | pytest only; unittest does not use the fixture-mode contract |
Architecture
flowchart TD
A[pytest CT test case] --> B[ct marker]
A --> C[fixture_NNN]
B --> D[Fixture mode selection]
C --> E[Test interface]
C --> F[Test equipment]
E --> G[DUT or loopback]
F --> G
A --> H[ct_result recorder]
D --> H
H --> I[Result JSON and test log]
| Layer | Location | Responsibility |
|---|---|---|
| Test case | test_envs/tests/pytest/test_cases/ |
Scenario, assertions, CT marker, metrics/statistics |
| Fixture composition | test_envs/tests/pytest/fixtures/ |
Tool selection, connection lifecycle, supported modes |
| DUT interface | test_envs/tests/pytest/test_interfaces/ |
connect, disconnect, read, write, execute |
| Test equipment | test_envs/tests/pytest/test_equipments/ |
Measurement or DUT control |
| Framework hooks | test_envs/tests/pytest/conftest.py |
Registry, validation, selection, result/log generation |
Repository Structure
test_envs/tests/
├── pytest/ # CT execution mode: mock or hil
│ ├── test_cases/
│ │ ├── test_fixture_001_uart_timing.py
│ │ ├── test_fixture_002_usb_loopback.py
│ │ └── test_fixture_003_network_loopback.py
│ ├── fixtures/
│ │ ├── fixture_001_uart_saleae.py
│ │ ├── fixture_002_usb_digilent.py
│ │ ├── fixture_003_network.py
│ │ ├── fixture_004_jtag_fpga.py
│ │ └── fixture_005_full_hil.py
│ ├── test_equipments/
│ │ ├── hil_base.py
│ │ ├── fpga/{mock,hil}/
│ │ ├── saleae/{mock,hil}/
│ │ └── digilent/{mock,hil}/
│ ├── test_interfaces/
│ │ ├── base.py
│ │ ├── hil_base.py
│ │ ├── usb/{mock,hil}/
│ │ ├── uart/{mock,hil}/
│ │ ├── jtag/{mock,hil}/
│ │ └── network/{mock,hil}/
│ └── conftest.py
└── unittest/
├── ct_framework/
│ └── python/
├── python/
├── c_cpp/
├── firmware/
└── common/
Identifiers and Test Cases
| Identifier | Rule | Example |
|---|---|---|
| TEST ID | Unique, non-empty marker value without spaces | CT-UART-001 |
| Fixture ID | Test filename number and fixture metadata must agree | FIXTURE-001 |
| Fixture argument | Must use the same three-digit number | fixture_001 |
| Execution ID | Generated for every result | YYYYMMDD_HHMMSS_ffffff |
Current executable CT mapping
| Fixture | Composition | Test case | TEST ID | Category | Default mode |
|---|---|---|---|---|---|
FIXTURE-001 |
UART + Saleae | test_fixture_001_uart_timing.py |
CT-UART-001 |
timing |
mock |
FIXTURE-002 |
USB + Digilent | test_fixture_002_usb_loopback.py |
CT-USB-001 |
communication |
mock |
FIXTURE-003 |
Network | test_fixture_003_network_loopback.py |
CT-NETWORK-001 |
communication |
mock |
FIXTURE-004 and FIXTURE-005 are registered metadata/fixture scaffolds,
but there are no matching test_fixture_004_*.py or test_fixture_005_*.py CT test cases.
CT Marker Contract
@pytest.mark.ct(
test_id="CT-UART-001",
category="timing",
fixture_id="FIXTURE-001",
fixture_mode="mock",
test_prompt="",
)
| Field | Required | Owner | Purpose |
|---|---|---|---|
test_id |
Yes | Test case | Unique TEST ID and --test-id selection |
category |
Yes | Test case | Result classification |
fixture_id |
Yes | Test case | Fixture metadata mapping |
fixture_mode |
Yes | Test case | Default mock or hil mode |
test_prompt |
No | Test case | Local LLM prompt override; an empty value uses the configured default |
description |
No | Test case | Result description; the function name is the fallback |
Collection performs the following validation for every CT item:
- Required marker fields exist.
test_idis a non-empty string and is not duplicated.- A file named
test_fixture_<NNN>_*.pydeclaresfixture_id="FIXTURE-<NNN>". - The test function requests
fixture_<NNN>. - The fixture ID exists in the registry.
fixture_modeismockorhil, and that mode is enabled inFIXTURE_META.
An invalid contract raises a pytest collection/usage error before test execution.
Fixture Registry and Metadata
fixture_registry() imports every fixture_*.py module in the fixtures package and reads its FIXTURE_META mapping.
Registry validation therefore applies even to fixture scaffolds that do not yet have test cases.
FIXTURE_META = {
"fixture_id": "FIXTURE-001",
"interfaces": ["UART"],
"equipments": ["Saleae"],
"modes": {
"mock": {"enabled": True},
"hil": {"enabled": True},
},
}
| Field | Validation |
|---|---|
fixture_id |
Unique, non-empty str |
interfaces |
list[str]; an empty list is allowed |
equipments |
list[str]; an empty list is allowed |
modes |
Mapping containing both mock and hil |
modes.<mode>.enabled |
bool |
Registered fixture catalog
| Fixture | Interfaces | Equipment | Mock declared | HIL declared | Current integration state |
|---|---|---|---|---|---|
FIXTURE-001 |
UART | Saleae | Yes | Yes | Mock executable; HIL fixture explicitly fails |
FIXTURE-002 |
USB | Digilent | Yes | Yes | Mock executable; HIL fixture explicitly fails |
FIXTURE-003 |
Network | None | Yes | Yes | Mock executable; HIL fixture explicitly fails |
FIXTURE-004 |
JTAG | FPGA | Yes | No | Mock component fixtures only; no CT test case |
FIXTURE-005 |
None | None | No | Yes | CICT_HIL=1 gate scaffold; no CT test case |
enabled: true declares that a mode may be selected. It does not prove that the fixture has been wired to physical hardware. The current repository has no end-to-end executable HIL CT test case.
Fixture-Based Test Case Composition
A CT Test Case does not create hardware or communication objects directly. It requests one numbered fixture, and that fixture composes the required interfaces and equipment for the selected test topology.
Test Case
├── @pytest.mark.ct
│ ├── TEST ID
│ ├── Fixture ID
│ └── Default mock or hil mode
├── fixture_<NNN>
│ ├── Interface: DUT communication and data transfer
│ ├── Equipment: measurement, capture, or target control
│ └── connect → yield → disconnect lifecycle
└── ct_result
├── metrics: interface and protocol measurements
└── statistics: equipment observations
| Layer | Responsibility | Examples |
|---|---|---|
| Test Case | Defines stimulus, assertions, TEST ID, category, and result values | UART timing, USB loopback, Network packet loopback |
| Fixture | Defines the reusable test topology and resource lifecycle | UART + Saleae, USB + Digilent |
| Interface | Communicates with the DUT or protocol endpoint | UART, USB, JTAG, Network |
| Equipment | Measures, captures, programs, or controls the test environment | Saleae, Digilent, FPGA |
| Result recorder | Separates interface metrics from equipment statistics | ct_result.metrics, ct_result.statistics |
Current Fixture and Test Case Behavior
| Fixture | Test Case status | Interface action | Equipment action | Recorded result |
|---|---|---|---|---|
FIXTURE-001 |
Executable as CT-UART-001 |
UART writes and reads a timing probe | Saleae measures UART timing | Baudrate error and jitter metrics; Saleae statistics |
FIXTURE-002 |
Executable as CT-USB-001 |
USB performs a 256-byte loopback | Digilent measures the USB transfer | Bytes, packets, endpoint, voltage, and integrity data |
FIXTURE-003 |
Executable as CT-NETWORK-001 |
Network performs a packet loopback | No equipment is currently composed | Bytes, packet count, latency, host, and port |
FIXTURE-004 |
Extension fixture; no CT Test Case | JTAG mock connection and transfer | FPGA connection and programmed-image tracking | Available for a future FPGA/JTAG Test Case |
FIXTURE-005 |
HIL gate; no CT Test Case | No interface | No equipment | Skips unless CICT_HIL=1 |
The numbered contract connects the files and runtime objects: test_fixture_001_*.py must declare FIXTURE-001 and request fixture_001. Collection stops before execution if the number, marker metadata, fixture argument, or registry entry does not agree.
Equipment Extension
FPGA, Saleae, and Digilent are equipment adapters. Each equipment type owns separate mock/ and hil/ implementations so a fixture can keep the same Test Case contract while changing the runtime backend.
test_equipments/
├── fpga/{mock,hil}/
├── saleae/{mock,hil}/
├── digilent/{mock,hil}/
└── wireshark/{mock,hil}/ # Planned extension
Wireshark should be added as Network capture equipment, not as a replacement for NetworkInterface. The Network interface continues to send and receive DUT traffic; the Wireshark equipment adapter starts capture, stops capture, and converts packet observations into fixture statistics.
A future Wireshark CT extension can use this structure:
test_envs/tests/pytest/
├── test_equipments/wireshark/{mock,hil}/
├── fixtures/fixture_006_network_wireshark.py
└── test_cases/test_fixture_006_network_capture.py
The extension must add Wireshark to FIXTURE_META.equipments, declare both mode flags, keep capture cleanup in finally, and record capture statistics through ct_result.statistics. HIL availability must fail or skip explicitly and must never fall back silently to mock behavior.
Mock and HIL Mode Selection
pytest CT
├── mock # Simulation or loopback implementation
└── hil # Physical hardware implementation
| Priority | Source | Values |
|---|---|---|
| 1 | CLI override | --fixture-mode=mock, --fixture-mode=hil |
| 2 | CT marker default | fixture_mode="mock", fixture_mode="hil" |
--fixture-mode=marker is the CLI default, but marker is not an execution mode.
It means “use the mock or hil value declared by the CT marker.” effective_fixture_mode() resolves the final mode and verifies that it is enabled by the fixture metadata.
none is also not a pytest execution mode.
It is used only for result fields such as equipment_mode when a fixture has no equipment.
| Result field | Value source |
|---|---|
test_mode |
Effective fixture mode |
interface_mode |
Effective fixture mode |
equipment_mode |
Effective fixture mode, or none when the equipment list is empty |
interfaces / equipments |
FIXTURE_META snapshot |
modes |
FIXTURE_META.modes snapshot |
Mock behavior
| Tool | Current behavior |
|---|---|
| UART | Connected-state checks, byte loopback, baudrate transmission history |
| USB | Connected-state checks, byte loopback, endpoint and packet-count history |
| Network | Host/port validation, byte loopback, packet and latency history |
| JTAG | Connected-state checks and byte loopback |
| Saleae | UART sample statistics, baudrate error, jitter |
| Digilent | USB bytes, packets, packet size, bus voltage, integrity error |
| FPGA | Connected-state check and programmed-image tracking |
HIL adapters and gate
HILTransportInterface delegates connection and I/O to injected handlers and rejects reads/writes while disconnected. HILEquipmentController similarly delegates equipment connect/disconnect operations. The UART, USB, JTAG, Network, Saleae, Digilent, and FPGA HIL classes currently inherit these generic adapters without device-specific logic.
A completed HIL fixture must:
- Enable
modes.hil.enabledonly when HIL selection is intended. - Instantiate the HIL interface/equipment with real handlers.
- Connect every configured interface and equipment before yielding.
- Disconnect resources in a
finallyblock. - Fail or skip explicitly if hardware or configuration is unavailable.
- Never fall back silently to a mock implementation.
full_hil is an independent gate scaffold and skips unless CICT_HIL=1; it is not currently connected to a fixture_005 CT test case.
Interface Lifecycle
All DUT transports implement this contract:
connect()
disconnect()
read(size=-1)
write(data)
execute(command)
TestInterface also supports a context manager. Its default execute() encodes a command, writes it, and returns one read. Current composed fixtures connect tools before yield and disconnect them in reverse order in finally.
Result Recording
Tests receive ct_result: CTResultRecorder and add measurement data explicitly:
ct_result.metrics.update(measurement.metrics())
ct_result.statistics.update(measurement.statistics())
After the test call, the fixture creates a normalized ResultRecord.
| Result section | Contents |
|---|---|
test_case |
ID, status, category, duration, description, environment |
test_configs |
CT marker fields except test_id and description |
fixture_configs |
Fixture ID, effective modes, tools, mode declarations |
test_src |
Git commit and branch |
test_result |
Execution ID, timestamp, metrics, statistics, log name |
Status is PASS, FAIL, or SKIP from the pytest call report. The environment is github_local_runner when GITHUB_ACTIONS=true, otherwise local; runner name comes from RUNNER_NAME or defaults to local.
The generated test log contains these sections:
[TEST]
[STDOUT]
[STDERR]
[EQUIPMENT]
[INTERFACE]
Statistics are written to EQUIPMENT; metrics are written to INTERFACE.
Commands
-
pytest.ini
[pytest] testpaths = test_envs/tests python_files = test_*.py addopts = -ra markers = ct: continuous/integration test hardware: test requiring real hardware
-
Powershell
Python Command Examples in venv
All configured tests under pytest.ini testpaths
.\.venv\Scripts\python.exe -m pytest
pytest CT area only
.\.venv\Scripts\python.exe -m pytest test_envs/tests/pytest -m ct -s
.\.venv\Scripts\python.exe -m pytest test_envs/tests/pytest/test_cases --fixture-mode=marker
Force Mock mode
.\.venv\Scripts\python.exe -m pytest test_envs/tests/pytest/test_cases --fixture-mode=mock
Select one TEST ID
VSCode -> Task
VSCode -> Testing
.\.venv\Scripts\python.exe -m pytest test_envs/tests/pytest/test_cases --test-id CT-UART-001 --fixture-mode=mock
For the current three CT test cases,
forcing --fixture-mode=hil fails with an explicit “HIL implementation is required” message.
VS Code
| Item | Value |
|---|---|
| Testing path | test_envs/tests/pytest |
| Debug configuration | Not currently configured in launch.json |
| All-test task | TEST CASE: ALL |
| TEST ID task | TEST CASE: TEST ID |
| Mode picker | marker, mock, hil |
Report
Pytest and unittest connect to the same Markdown-first Report pipeline. Pytest adds Local LLM analysis and conditional escalation before the shared Markdown is generated.
Pytest result JSON + test log
↓
test_envs.tools.test_result
↓
Local LLM analysis + escalation decision
↓
test_envs.tools.mkdocs_reporter
↓
Canonical Markdown
┌──────┴──────┐
↓ ↓
MkDocs Results Pandoc Report
| Report stage | Source or tool | Output |
|---|---|---|
| Test evidence | test_reports/results/pytest/test_cases/<test-id>/ |
<execution-id>_result.json and <execution-id>_test.log |
| Test environment metadata | Top-level test_envs in result JSON |
OS, unique test server name, host environment, and request source; rendered in the Markdown Test Summary |
| Analysis | test_envs.tools.local_llm |
Analysis stored in result JSON and Local LLM log |
| Report coordination | test_envs.tools.test_result |
Processes the latest or every pending result |
| Canonical Markdown | test_envs.tools.mkdocs_reporter |
test_reports/markdown/pytest/test_cases/<test-id>/<execution-id>_result.md |
| MkDocs publication | test_envs.tools.mkdocs_reporter |
docs/tests/pytest/<test-id>.md and execution pages |
| Pandoc conversion | test_envs.tools.pandoc_reporter |
test_reports/pandocs/pytest/test_cases/<test-id>/<execution-id>_result.<format>; DOCX uses test_reports/pandocs/reference.docx |
Generate pending Markdown and publish MkDocs result pages:
.\.venv\Scripts\python.exe -m test_envs.tools.test_result --pending --docs
Convert the latest Markdown to HTML:
.\.venv\Scripts\python.exe -m test_envs.tools.pandoc_reporter --latest --format html
Convert the latest Markdown to DOCX:
.\.venv\Scripts\python.exe -m test_envs.tools.pandoc_reporter --latest --format docx
The same commands are available from VS Code through Run and Debug-Report and Tasks-Report. Pytest result pages are published under Pytest Results.