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.
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
Processes may need time to pass. Use states or simulation helpers to give the design cycles before checking output:
@Test
fn UpdatesAfterCycles()
{
state {}
state {}
assert this.counter.count == 2
}
Keep waits intentional. If a test needs many cycles, explain why through the test name or nearby code.
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.
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
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.