Repository navigation
Releases: m-mcgowan/embedded-test-runner
Release list
v0.3.4
Added
- Two gates now stand between a release commit and its tag, because v0.3.3
was tagged on a red build.scripts/release.shfirst installs the package
into an empty virtualenv and runs the suite there, before anything is
committed — a working checkout cannot tell a declared dependency from one
that merely happens to be installed, which is exactly howpyserialwent
undeclared. It then pushes the release commit and waits for every workflow
run on that exact SHA to conclude successfully before creating the tag, so a
tag on a red build is structurally impossible rather than merely
discouraged. If CI fails,mainis left carrying an untagged commit, which
is an ordinary state to fix forward from.--skip-venvand--skip-ciexist
for emergencies.
Fixed
- CI could not collect the acceptance tests. Dropping CI's
--ignore=tests/acceptancein 0.3.3 exposed thatpyserialwas never
declared anywhere:tests/acceptance/helpers.pyimports it at module scope,
so collection died withModuleNotFoundError: No module named 'serial'even
though those tests skip themselves without a--port. It went unnoticed
locally because PlatformIO's environment already provides the module. Now
declared in thedevextra, and verified by running CI's exact command in a
clean virtualenv rather than the working one.
v0.3.3
Fixed
-
RESUME_AFTERdiscarded the run's accumulatedETST:ARGS. They were
folded intoRUN,RUN_ALLandRUN:, but aRESUME_AFTER:command
matched neither branch and fell through to the passthrough, silently
dropping them. Every deep-sleep test forces a resume cycle, so on any suite
that sleeps, the first cycle honoured the run's filters and environment and
every cycle after it ran without them. The failure runs in the dangerous
direction: coverage quietly changes while the run still exits green, because
a test that skips counts as a test that passed. Measured on ESP32-S3
hardware, a 19-cycle suite applied--envon the first cycle only, and all
five environment-gated cases then skipped while the suite reported
218 ran / 218 passed. Filters travel the same path, so a focused--tsrun
over-reported for the same reason.The folding logic is extracted to
include/etst/doctest/command_args.hso it
can be covered natively rather than fixed in place — it had no native
coverage precisely because it was tangled with Arduino dependencies. Ten
cases now exercise the real function; three of them failed before the fix. -
A plain
pytestrun could not collect this project's own test suite.
Two separate defects, both hidden by CI's
--ignore=tests/integration --ignore=tests/acceptance. First,tests/and
tests/acceptance/both hold aconftest.py, and pytest imports each under
the bare module nameconftest, so the seven unit-test modules doing
from conftest import MockProjectConfiggot whichever loaded first —
acceptance, alphabetically — and failed to import. The shared mocks moved to
tests/pio_mocks.py, an ordinary importable module;conftest.pykeeps only
theinstall_pio_mocks()side effect that must happen at collection time.
Second,pytest_addoptionfor--port/--baudsat in the acceptance
conftest, and pytest only calls that hook on initial conftests, so a
whole-tree run never declared the options and every acceptance fixture died
onValueError: no option named 'port'. The declaration moved to
tests/conftest.pyand droppedrequired=True, which pytest had been
applying to the entire session rather than the directory that asked for it.
Acceptance tests now skip without a--portinstead of breaking the run,
CI runspytest tests/ -vwith no ignore flags, and
tests/test_suite_hygiene.pyfails if plain collection ever breaks again.
Added
scripts/release.sh --publish <version>— finishes a release left local
by--no-push: pushesmainand the tag, then creates the GitHub release
from the CHANGELOG section for that version.--no-pushpreviously exited
with no way back into the script, so the publish half had to be retyped by
hand — which is how v0.3.2's GitHub release was actually created. The pushes
are no-ops if the tag went out by hand first, and the run refuses if the
GitHub release already exists.
v0.3.2
Fixed
-
RESUME_AFTERsilently dropped tests (high severity — sessions could
skip whole suites and still exitPASSED).apply_resume_after()computed
the resume point as an index over all registered tests, then passed it to
doctest'sfirstoption, which is an index over tests that pass filters
(doctest compares it againstnumTestCasesPassingFilters). Any
doctest::skip()-attributed or filtered-out test before the resume point
made the two spaces diverge, sofirstwas set too high and silently
excluded that many tests after the resume point. The error compounded
across sleep/wake cycles; when the inflated index passed the end of the
list, a cycle ran nothing at all. Observed in the field: 148 of 186 tests
ran, 8 of 35 suites never started, result reportedPASSED.Resume is now implemented by force-setting
m_skipon the already-completed
prefix instead of usingfirst. This removes the index arithmetic entirely,
lets doctest's own filter chain do the counting, and composes correctly with
filters and env requirements applied after the resume is processed. -
Under-run is now a failure, not a pass. The runner compares tests
actually executed against the runnable count the device reported on its
first cycle, and reports anERROREDcase (runner/under_run) when tests
went missing. The summary line also shows the shortfall
(N ran of M expected | K NEVER RAN). A run that cannot account for its
own tests no longer looks green. -
ETST:COUNTSrun/skipwere wrong during resume cycles. The count
applied a manualskip_countadjustment that double-counted once resume
skips became visible tocount_passing_filters(), and silently did nothing
in therun == skip_countcase. The adjustment is removed; the reported
counts now reflect what will actually run. -
etst::require_envbroke on include order.env.hreferred to
doctest::detail::TestCaseunqualified from insidenamespace etst, which
resolves toetst::doctest— the runner's own namespace — as soon as any
header declaring it is included first, failing with
'etst::doctest::detail' has not been declared. Now spelled::doctest::.
Latent untilresume.hwas introduced; it would have bitten any consumer
that included a runner header beforeenv.h.
Changed
scripts/release.shreleases one repo at a time. It previously released
embedded-bridge and pio-test-runner in lockstep under a single version, which
the two repos had already outgrown (bridge at v0.2.0, runner at v0.3.1) and
which hard-failed whenever one repo had an empty[Unreleased]section. Each
repo now carries its own version and is released on its own;--repo <dir>
picks the target. The dependency is still enforced, but as a check rather
than a chain: releasing pio-test-runner stops if embedded-bridge has commits
past its own last release tag, and says nothing when it does not. Also adds
--no-push, which does the local bump/commit/tag and prints the publish
commands instead of pushing.
Added
-
etst/doctest/resume.h— the resume-point selection (sorted_registry(),
select_resume_after()) extracted out ofrunner.hinto a header carrying no
Arduino or ESP dependency, so the native C++ harness can test the real code
instead of a hand-mirrored copy.runner.hkeepsapply_resume_after()
unchanged as the logging wrapper, so consumers need no changes. -
Native regression coverage for resume selection
(tests/test_doctest_internals.cpp, six cases, run in CI). Fixture cases
placedoctest::skip()-attributed tests deliberately before the resume
point — the exact condition under which the two index spaces diverged — and
assert that tests after the resume point still run, that the completed prefix
is skipped exactly, that already-skipped tests after the point are untouched,
that an unknown resume point reports-1and mutates nothing, and that
sorted_registry()holds its (file, line) wire ordering. A seventh case
guards the fixture itself, failing loudly if a future edit stops reproducing
the divergence.Verified discriminating: rebuilding the harness against a shadow
resume.h
carrying the oldfirst-based selection fails exactly one case
(tests after the resume point still run despite skipped tests before it)
by exactly two tests — one per skip-attributed fixture case.
v0.3.1 — declare embedded-bridge dependency
Fixed
- Dependency declarations: declare
embedded-bridgeexplicitly in bothpyproject.toml(PEP 508 git URL) andlibrary.json(PlatformIOdependenciesarray). v0.3.0 left these implicit, which worked for the in-tree pip-install consumers because they installembedded-bridgefrom git themselves before importingetst, but DOA for the canonicallib_deps-only path documented inexamples/test_custom_runner.py.
After the fix, both:
pip install embedded-test-runner @ git+https://github.com/m-mcgowan/embedded-test-runner.git@v0.3.1(fresh venv, no other deps), and- PlatformIO
lib_deps = https://github.com/m-mcgowan/embedded-test-runner.git#v0.3.1
resolve embedded-bridge automatically.
Verified
- Fresh-venv pip install: pulls embedded-bridge from git,
from etst.runner import EmbeddedTestRunnersucceeds. - Fresh-PIO-project
pio pkg install: both libs land in.pio/libdeps/<env>/, the example shim glob matches, runner imports cleanly.
v0.3.0 — receiver plugin API + rename
Added
- Receiver plugin API: packages can declare a receiver class under the
embedded_test_runner.receiversentry-point group and have it auto-attached to the runner's router. See README "Receiver plugins" for the contract. Enablespio-gcovand similar packages to attach per-partition capture receivers without per-project Python. - Partition lifecycle hooks:
on_partition_start()/on_partition_complete()onEmbeddedTestRunner. Default implementations forward to plugin receivers that opt in. Subclasses overriding either hook should callsuper()to preserve plugin notification. - Test environment variables via
ETST:ARGS/--env K=V: host forwardsETST_ENV_*host env vars andpio test -a --env K=Varguments to the firmware before eachRUN. Newetst::env(key)/etst::env_int(key, default)lookups in<etst/env.h>, plus arequire_env("KEY")doctest decorator that skips a test when the named env var is missing.
Changed
- Renamed: pio-test-runner → embedded-test-runner, pio_test_runner → etst, ptr_doctest → etst::doctest. Include path:
<etst/...>. Clean break — no backward-compat aliases. - Config struct replaces weak hooks:
ptr_board_init,ptr_after_cycle,ptr_configure_contextremoved. Useetst::configandetst::doctest::config. - Listener renamed:
PtrTestListener→EtstDoctestListener. - doctest_runner.h moved:
<pio_test_runner/doctest_runner.h>→<etst/doctest/runner.h>.
Fixed
open_serialretry default restored:_open_serialno longer passesretries=1, which had overridden the 5-retry default and turned transient sub-second port unavailability after USB-CDC re-enumeration into a hard partition failure on back-to-backpio testruns.- PIO doctest parser integration: forward blank lines to PIO's
DoctestTestCaseParserso it can commit accumulatedTEST CASE:header tokens; collapse parser-emitted subcase entries toTEST_CASEgranularity in_ensure_test_results. Together these eliminate phantom empty-name cases and subcase-iteration double-counting in PIO's outer summary. - Example shim libdeps glob:
examples/test_custom_runner.pyandtests/integration/test/test_custom_runner.pynow glob*/embedded-test-runner/src(matched the post-renamelibrary.jsonname).
Migration
The PTR→ETST rename is a clean break with no backward-compat aliases. Consumers must update:
- C++ includes:
<pio_test_runner/...>→<etst/...> - C++ namespaces:
ptr_doctest::→etst::doctest:: - C++ weak hooks:
ptr_board_init/ptr_after_cycle/ptr_configure_context→ assign toetst::config/etst::doctest::configstruct fields - Python imports:
from pio_test_runner.runner import …→from etst.runner import … - Protocol prefix in any host-side parsers:
PTR:→ETST:(already breaking in 0.2.0 — re-noting for completeness) - Environment variables:
PTR_*→ETST_*(old names still accepted with deprecation warning) library.jsonname in libdeps globs:pio-test-runner→embedded-test-runner(this affects copies of the example shim that pre-date 0.3.0 — re-copyexamples/test_custom_runner.py)
v0.2.0
Breaking Changes
- Protocol prefix renamed
PTR:→ETST:(Embedded Test). Firmware must be rebuilt. - Protocol tags renamed:
ETST:CASE:START(wasTEST:START),ETST:COUNTS(wasTESTS). - Environment variables renamed
PTR_*→ETST_*(shorter:ETST_CASE,ETST_SUITE, etc.). OldPTR_*names still work with deprecation warning. - Default post-test action changed from
sleeptowait(device stays online).
Fixed
- Phase 2 resume:
RUN: --wake --tc "exact name"instead ofRUN: *name*— fixes tokenizer splitting multi-word names, fixes wildcard substring collisions. is_test_wake(): protocol-based--wakeflag replaces RTC memory — no sleep memory consumed.- Skip count reporting:
ETST:COUNTSnow reflects all filter types, not just RESUME_AFTER. - Hang detection in line callback mode: detects silent hangs when PIO owns serial.
Added
- Runtime skip control (
--unskip-tc,--skip-tc,--no-skip) - Full doctest CLI passthrough via
applyCommandLine() ETST:BUSY/ETST:RESTARTprotocol messages- Per-test timeout annotations and test count reporting
- Deep sleep test documentation with complete example
- Hang detection documentation (global timeout, per-test, busy signal)
- Aggregate run summary across sleep/wake cycles
- Roadmap: framework abstraction, multi-phase tests, platform expansion
See CHANGELOG.md for full details.
v0.1.0
First release. PlatformIO test runner with crash detection, deep sleep
orchestration, and auto-install from GitHub.
Added
- EmbeddedTestRunner — PlatformIO test runner plugin with crash
detection, disconnect handling, and framework-agnostic result parsing
via embedded-bridge receivers - PTR protocol — bidirectional test protocol with CRC-8 checksums,
repeated READY handshake, and host→device CRC validation - Ready/run protocol — READY/RUN/DONE state machine for device
readiness and test execution - RESUME_AFTER deep sleep orchestration — multi-cycle support for
tests that survive deep sleep, with automatic RESTART and resume - Disconnect handler — manages disconnect/reconnect windows for
devices that sleep, reset, or crash - Test result receiver — parses device output for test results
- Robust doctest parser — fixes PIO doctest parser crash on
lines ending with ':' - Timing tracker — per-test duration measurement with slow test
reporting - Firmware headers —
doctest_runner.h,test_runner.h, and
protocol.hfor on-device test integration with CRC-8 - Auto-install shim — example
test_custom_runner.pythat
pip-installs pio-test-runner and embedded-bridge from GitHub on first
use (opt out viaPIO_TEST_RUNNER_NO_AUTO_INSTALL=1) - On-device integration tests — ESP32-S3 test project exercising
protocol handshake, memory tracking, timing, disconnect, and deep
sleep with two sleep cycles - Release script —
scripts/release.shautomates version bump,
changelog dating, tagging, pushing, and GitHub release for both repos - Design documentation
Fixed
- Pass exception to PIO TestCase for ERRORED status
- Teardown hang detection
- Drain stale serial bytes before waiting for runner commands
- Wait for device ack before closing serial after RESTART
- Strip leading garbage bytes before CRC validation on device
- Retry command when device re-sends READY after CRC failure