Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

HILT hero image

HILT

Python HIL testing with a pit of success: composable test components, guardrails that make a bad test hard to write, and an engine that won't let an unhandled failure pass silently.

HILT (Hardware-In-the-Loop Testing) is a test framework for Python. A test is a component and a rack is a tree of them, built from the same unit you test one bench DUT with. You write what you claim about the hardware; HILT enforces the verdict, records the evidence as the test runs, and gives the floor a terminal Inspector for reading it, so a technician can triage a failure at 2am without calling you.

The act of executing the test is the act of producing irrefutable evidence

Why

Most test scripts have one way to fail: something raises. A bad unit, a bug in the test, and a database timeout all raise, and all show up as the same red FAIL. A failure that doesn't raise doesn't show up at all; it passes. In the former case the technician on shift guesses or escalates and the line waits for an engineer; in the latter, a bad unit ships to the customer.

HILT closes both gaps. A test passes only by returning an explicit verdict, so a failure that never raises cannot pass by default. And every failure it records is sorted by cause before anyone reads it: bad unit, bad bench, or bad test.

That is what the pit of success is for. The guardrails push the engineer to name each failure mode while the test is being written, and the engine holds that line at run time, so the sorting is done before the suite ever reaches the floor. The same component composes from one bench DUT to a rack, so it is done once. Every guardrail and every line of the engine is measured against one bar:

A Floor Technician can correctly categorize a failure and start the right recovery protocol within 60 seconds of opening the Inspector, without messaging the engineer who wrote the test.

Try it

Requires Python 3.14 and uv. HILT is not on PyPI yet; install from a clone.

make install
uv run hilt run examples/hello_world.py
uv run hilt inspect

A suite is a class, a claim is a method, and a verdict is explicit:

from hilt import PASS, TestComponent, TestFailure, TestUtils, Verdict


class HelloWorld(TestComponent):
    def __init__(self, *, name: str, greeting: str = "hello world") -> None:
        super().__init__(name=name)
        self._greeting = greeting

    def build_suite(self) -> TestComponent:
        return self.test("say hello", self._say_hello)

    async def _say_hello(self, utils: TestUtils) -> Verdict:
        if self._greeting:  # the claim, stated as a condition
            utils.logger.info(self._greeting)  # lands in this node's timeline
            return PASS  # the only way to pass
        raise TestFailure("nothing to say", "a greeting", "(empty)")

The Getting started guide continues from here: the examples in teaching order, running against real devices over SSH, and deploying to a station.

Three outcomes, never merged

No pass by default. "Reached the end without throwing" proves only that none of the failures you thought of happened. A HILT test passes only by returning the PASS sentinel, which can't be forged from a string, a truthy value, or parsed config. Declare the readings a pass must carry with @evidence(...), and a pass without them doesn't count.

Every node ends in exactly one of three states, and each sends the floor down a different path:

State What happened What the floor does
Pass The test returned PASS, with its declared evidence Ship it
Fail The hardware failed a modeled check (TestFailure), or the test raised an exception it didn't model. The second case is recorded as Fail with a loud contract-violation banner and bug: true Check the hardware, or hand the flagged bug to the engineer
Aborted No verdict exists: the harness or integration layer broke, the operator stopped the rack, a time budget expired, or the test returned without PASS. That last case is also flagged bug: true Fix the harness or the test. Don't condemn the unit

Two developer mistakes, two different states, on purpose. An unmodeled exception carries information about what went wrong, so it is recorded as a Fail and marked as a bug. A method that falls off the end produces nothing, and HILT will not forge a verdict from nothing, so the node is Aborted and marked as a bug. Both are loud. Neither takes down the sibling DUTs running beside it.

What you get

Evidence is a side effect of running. Each run writes a timestamped directory with a versioned result.json, a human timeline.log and machine timeline.jsonl per node, and point-in-time artifacts stitched onto that timeline. Entries are flushed as they are written, so an interrupted run keeps its chronology up to the moment it stopped. With NTP-synced stations, that timeline holds up in a customer dispute.

One building block, bench to rack. TestComponent is the only unit. A leaf check, an ordered sequence(), a concurrent parallel(), a whole device, and a full rack are the same type, composed. No level knows what sits above it, so the test tree mirrors the hardware tree.

Guardrails your team can't bypass. Rules are enforced at runtime (one child boundary owns the verdict, the failure taxonomy, and lifecycle order, and cleanup always runs), at import time (a structurally broken suite refuses to load), and by static analysis (hilt-check flags an ungated PASS). Nothing that protects the hardware or the verdict relies on the linter alone.

Safe aborts. When the integration layer fails mid-run or the operator hits Ctrl-C, HILT closes every in-flight transport across the tree: SSH sessions destroyed, subprocesses killed. A 30-minute firmware flash doesn't keep writing to a board after the run is called off. Start and finish hooks stay paired, so your MES never gets orphan rows.

Less plumbing. Call utils.use_shell() in configure() and every descendant's utils.exec runs over that SSH hop, jump hosts included. Serial consoles and HTTP/CoAP device APIs route the same way. wait_for() requires a looking_for= description that becomes the timeout message the technician reads. Time budgets (fail_after= / abort_after=) attach to any node, and a child can only tighten its parent's budget.

The Inspector

hilt inspect opens any saved run in a terminal UI built for the person at the station. The Inspect tab replays the evidence. The Re-Run tab re-runs any branch or leaf of the tree with the exact production code, without touching the production record.

  [ Inspect ]   Re-Run
  ---------------------------------------------------------
    rack  Aborted
    └─ servers  Aborted  ←
       ├─ server-01  Pass
       ├─ server-02  Fail
  >    │  └─ thermal check  Fail  not taken: fan_rpm
       ├─ server-03  Aborted
       └─ firmware check  Pending
  ---------------------------------------------------------
  log: rack/servers  (rolled up: thermal check has no own log)
  page 4/7  errors only: off  captures: 1  (1-9 opens a capture)
      14:02:11Z info     starting thermal ramp
  [1] 14:02:45Z info     survey captured
      14:03:02Z error    temperature 87 C exceeds 75 C
  q Quit  Tab Inspect / Re-Run  ← → failures  h Hide passes  n p page  f Errors only
  • ← / → jump between failures, skipping everything that passed. h folds the passing branches out of a large rack.
  • The highlighted node's timeline follows the cursor. 1–9 open captured artifacts.
  • Missing readings are shown as not taken by name, so an absent value is never mistaken for zero.

The same record is available as versioned JSON (--json, with published schemas) and as JUnit XML (--junit) for CI and dashboards.

Documentation

GitLab renders every page under https://gitlab.com/Tyler-Meador/HILT/-/tree/main/docs. Each release branch also carries the built site under docs/site/, so a station can open it from a clone with no network. The Floor Technician guide is the URL that belongs on a station label.

Status

HILT is at 0.0.x and the API is still moving. Semantic versioning starts at 1.0.0; until then API stability describes what may change and CHANGELOG.md records every change. Issues and merge requests are welcome.

License

Copyright 2026 Tyler Meador. HILT is released under the Apache License, Version 2.0. The full text is in LICENSE; the copyright notice is in NOTICE. There is no warranty; see the license for the terms.

Metadata

Release files for hilt-core 1.0.0rc1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hilt-core 1.0.0rc1
File Size Uploaded Provenance Actions
hilt_core-1.0.0rc1.tar.gz 792.9 kB Without provenance Details for hilt_core-1.0.0rc1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for hilt-core 1.0.0rc1
File Interpreter ABI Platform Actions
hilt_core-1.0.0rc1-py3-none-any.whl Python 3 none any Details for hilt_core-1.0.0rc1-py3-none-any.whl

Total release size: 1.0 MB

Release files / hilt_core-1.0.0rc1.tar.gz

Download URL hilt_core-1.0.0rc1.tar.gz
Size 792.9 kB
Tags Source
SHA-256 checksum
How to use checksums
ecbb696de3c7c9f494b3b806dd3b12e499b9e5520bb39ec0018f3c7260c9d123
BLAKE2b-256 checksum
How to use checksums
94d5e4b9c78ed653768514a58e8a3dfe01abd178f297d3b2f993b9f7b6b56fb9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.11 {"installer":{"name":"uv","version":"0.10.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Pop!_OS","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / hilt_core-1.0.0rc1-py3-none-any.whl

Download URL hilt_core-1.0.0rc1-py3-none-any.whl
Size 247.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ab565df6fbea2658ba542329cfc830ea0aa22afca27df8a618c4f962172934dd
BLAKE2b-256 checksum
How to use checksums
b75d2e1c5d8166e7af889e6411995e15a04675269fac2aa2b9da0d32eb67fedf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.11 {"installer":{"name":"uv","version":"0.10.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Pop!_OS","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for hilt-core 1.0.0rc1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page