Testing is part of the normal Livt workflow. A Livt test is itself a component: it creates the design under test, drives inputs, calls public functions, waits when time needs to pass, and checks results with assertions.
Good tests make hardware changes safer. They also document how a component is supposed to be used.
Test Components
A test suite is a component marked with @Test:
namespace Livt.App.Tests
using Livt.App
@Test
component ByteClassifierTest
{
classifier: ByteClassifier
new()
{
this.classifier = new ByteClassifier()
}
}
The constructor creates the design under test and any test helpers.
Test Functions
A test case is a function marked with @Test:
@Test
fn AcceptsDigits()
{
assert this.classifier.IsAsciiDigit(0x30) == true
assert this.classifier.IsAsciiDigit(0x39) == true
}
Keep tests focused. A test name should describe one behavior, not an entire subsystem.
Assertions
Use assert to check expected behavior:
assert value == 0x42
assert this.queue.IsEmpty() == true
Prefer concrete assertions over visual inspection of logs. Simulation.Report is useful, but a report message does not fail a test when the behavior is wrong.
When an assertion fails, start with the first source location and simulation time. Later messages may be consequences. Reduce the run to one component or method while diagnosing it:
livt test --run ByteClassifierTest:AcceptsDigits
Simulation-Only APIs
Simulation helpers belong in tests and debugging code:
Simulation.Report("writing first byte")
They help explain long simulations, but they do not describe synthesizable hardware. Keep this boundary clear in source files intended for synthesis.
Testing Functions
Pure or mostly pure functions are the easiest to test:
@Test
fn RejectsNonDigits()
{
assert this.classifier.IsAsciiDigit(0x2F) == false
assert this.classifier.IsAsciiDigit(0x3A) == false
}
For functions, test boundary values first. If a function accepts a range, test the first accepted value, the last accepted value, and values just outside the range.
Testing Stateful Components
Stateful components need tests that perform actions in order:
@Test
fn CountsAcceptedPackets()
{
this.stats.Accept()
this.stats.Accept()
assert this.stats.acceptedCount == 2
}
The test reads like a short scenario: arrange the component, act on it, assert the observable result.
Testing Processes
In a test component, each state {} lets exactly one simulation clock cycle pass before the next statement runs. Use this to give a clocked process time to update its outputs:
@Test
fn UpdatesAfterCycles()
{
state {} // cycle 1
state {} // cycle 2
assert this.counter.count == 2
}
The counter process fires once per clock cycle. After two empty states the count has incremented twice, so the assertion checks for 2.
Keep waits intentional. If a test needs many cycles, explain why through the test name or a nearby comment.
Test Doubles
Interfaces make it easy to replace a complex dependency with a predictable test component:
component TestByteSource : IByteSource
{
override fn HasData() bool
{
return true
}
override fn Read() byte
{
return 0x42
}
}
Use test doubles when the component under test should not depend on a real UART, memory subsystem, or protocol stack.
Testing Memory-Mapped Interfaces
Use Livt.Bus transactions to test an HxS register interface through its public bus contract:
this.master.SetTimeoutTicks(200)
this.master.Write32(0x0, 5 as uint)
this.master.Verify32(0x0, 11 as uint)
This checks more than calling the bound function directly. It verifies the address, write layout, invocation, and read result as one observable behavior.
Test reset values, consecutive writes, repeated reads, unmapped addresses, and byte enables where they matter. Components with reserved bits, selectors, or delegated windows should include focused transactions for those boundaries.
See HxS Register Interfaces for complete component and test examples.
Writing Maintainable Tests
Good Livt tests have the same qualities as good software tests:
- one clear behavior per test
- descriptive test names
- direct assertions
- small fixtures
- no unnecessary timing
- no dependency on unrelated components
For hardware, add two more habits:
- check reset and initial state when it matters
- inspect generated VHDL when a test exercises a new hardware pattern
Each test should establish its own fixture state. Avoid relying on method order. Cover reset, ordinary behavior, and boundaries separately. For timed behavior, state the expected cycle count and assert immediately after it; unexplained waits can hide latency regressions.
Summary
Livt tests are components. They instantiate the design, run scenarios, and assert expected behavior. Use reports for visibility, assertions for correctness, and interfaces for small test doubles.