Skip to content

Releases: m-mcgowan/embedded-test-runner

v0.3.4

Choose a tag to compare

@m-mcgowan m-mcgowan released this 11 Sep 21:51

Added

  • Two gates now stand between a release commit and its tag, because v0.3.3
    was tagged on a red build. scripts/release.sh first 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 how pyserial went
    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, main is left carrying an untagged commit, which
    is an ordinary state to fix forward from. --skip-venv and --skip-ci exist
    for emergencies.

Fixed

  • CI could not collect the acceptance tests. Dropping CI's
    --ignore=tests/acceptance in 0.3.3 exposed that pyserial was never
    declared anywhere: tests/acceptance/helpers.py imports it at module scope,
    so collection died with ModuleNotFoundError: 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 the dev extra, and verified by running CI's exact command in a
    clean virtualenv rather than the working one.

v0.3.3

Choose a tag to compare

@m-mcgowan m-mcgowan released this 11 Sep 21:33

Fixed

  • RESUME_AFTER discarded the run's accumulated ETST:ARGS. They were
    folded into RUN, RUN_ALL and RUN:, but a RESUME_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 --env on 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 --ts run
    over-reported for the same reason.

    The folding logic is extracted to include/etst/doctest/command_args.h so 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 pytest run 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 a conftest.py, and pytest imports each under
    the bare module name conftest, so the seven unit-test modules doing
    from conftest import MockProjectConfig got whichever loaded first —
    acceptance, alphabetically — and failed to import. The shared mocks moved to
    tests/pio_mocks.py, an ordinary importable module; conftest.py keeps only
    the install_pio_mocks() side effect that must happen at collection time.
    Second, pytest_addoption for --port/--baud sat 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
    on ValueError: no option named 'port'. The declaration moved to
    tests/conftest.py and dropped required=True, which pytest had been
    applying to the entire session rather than the directory that asked for it.
    Acceptance tests now skip without a --port instead of breaking the run,
    CI runs pytest tests/ -v with no ignore flags, and
    tests/test_suite_hygiene.py fails if plain collection ever breaks again.

Added

  • scripts/release.sh --publish <version> — finishes a release left local
    by --no-push: pushes main and the tag, then creates the GitHub release
    from the CHANGELOG section for that version. --no-push previously 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

Choose a tag to compare

@m-mcgowan m-mcgowan released this 02 Sep 04:15

Fixed

  • RESUME_AFTER silently dropped tests (high severity — sessions could
    skip whole suites and still exit PASSED). apply_resume_after() computed
    the resume point as an index over all registered tests, then passed it to
    doctest's first option, which is an index over tests that pass filters
    (doctest compares it against numTestCasesPassingFilters). Any
    doctest::skip()-attributed or filtered-out test before the resume point
    made the two spaces diverge, so first was 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 reported PASSED.

    Resume is now implemented by force-setting m_skip on the already-completed
    prefix instead of using first. 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 an ERRORED case (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:COUNTS run/skip were wrong during resume cycles. The count
    applied a manual skip_count adjustment that double-counted once resume
    skips became visible to count_passing_filters(), and silently did nothing
    in the run == skip_count case. The adjustment is removed; the reported
    counts now reflect what will actually run.

  • etst::require_env broke on include order. env.h referred to
    doctest::detail::TestCase unqualified from inside namespace etst, which
    resolves to etst::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 until resume.h was introduced; it would have bitten any consumer
    that included a runner header before env.h.

Changed

  • scripts/release.sh releases 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 of runner.h into 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.h keeps apply_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
    place doctest::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 -1 and 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 old first-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

Choose a tag to compare

@m-mcgowan m-mcgowan released this 05 May 05:46

Fixed

  • Dependency declarations: declare embedded-bridge explicitly in both pyproject.toml (PEP 508 git URL) and library.json (PlatformIO dependencies array). v0.3.0 left these implicit, which worked for the in-tree pip-install consumers because they install embedded-bridge from git themselves before importing etst, but DOA for the canonical lib_deps-only path documented in examples/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 EmbeddedTestRunner succeeds.
  • 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

Choose a tag to compare

@m-mcgowan m-mcgowan released this 05 May 04:50

Added

  • Receiver plugin API: packages can declare a receiver class under the embedded_test_runner.receivers entry-point group and have it auto-attached to the runner's router. See README "Receiver plugins" for the contract. Enables pio-gcov and similar packages to attach per-partition capture receivers without per-project Python.
  • Partition lifecycle hooks: on_partition_start() / on_partition_complete() on EmbeddedTestRunner. Default implementations forward to plugin receivers that opt in. Subclasses overriding either hook should call super() to preserve plugin notification.
  • Test environment variables via ETST:ARGS / --env K=V: host forwards ETST_ENV_* host env vars and pio test -a --env K=V arguments to the firmware before each RUN. New etst::env(key) / etst::env_int(key, default) lookups in <etst/env.h>, plus a require_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_context removed. Use etst::config and etst::doctest::config.
  • Listener renamed: PtrTestListener → EtstDoctestListener.
  • doctest_runner.h moved: <pio_test_runner/doctest_runner.h> → <etst/doctest/runner.h>.

Fixed

  • open_serial retry default restored: _open_serial no longer passes retries=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-back pio test runs.
  • PIO doctest parser integration: forward blank lines to PIO's DoctestTestCaseParser so it can commit accumulated TEST CASE: header tokens; collapse parser-emitted subcase entries to TEST_CASE granularity 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.py and tests/integration/test/test_custom_runner.py now glob */embedded-test-runner/src (matched the post-rename library.json name).

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 to etst::config / etst::doctest::config struct 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.json name in libdeps globs: pio-test-runner → embedded-test-runner (this affects copies of the example shim that pre-date 0.3.0 — re-copy examples/test_custom_runner.py)

v0.2.0

Choose a tag to compare

@m-mcgowan m-mcgowan released this 09 Apr 08:00

Breaking Changes

  • Protocol prefix renamed PTR: → ETST: (Embedded Test). Firmware must be rebuilt.
  • Protocol tags renamed: ETST:CASE:START (was TEST:START), ETST:COUNTS (was TESTS).
  • Environment variables renamed PTR_* → ETST_* (shorter: ETST_CASE, ETST_SUITE, etc.). Old PTR_* names still work with deprecation warning.
  • Default post-test action changed from sleep to wait (device stays online).

Fixed

  • Phase 2 resume: RUN: --wake --tc "exact name" instead of RUN: *name* — fixes tokenizer splitting multi-word names, fixes wildcard substring collisions.
  • is_test_wake(): protocol-based --wake flag replaces RTC memory — no sleep memory consumed.
  • Skip count reporting: ETST:COUNTS now 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:RESTART protocol 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

Choose a tag to compare

@m-mcgowan m-mcgowan released this 17 Mar 08:50

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.h for on-device test integration with CRC-8
  • Auto-install shim — example test_custom_runner.py that
    pip-installs pio-test-runner and embedded-bridge from GitHub on first
    use (opt out via PIO_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.sh automates 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