Testing

Testing Livt Code

Test components, test functions, assertions, simulation-only APIs, and maintainable hardware verification workflows.

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:

livt
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:

livt
@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:

livt
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:

bash
livt test --run ByteClassifierTest:AcceptsDigits

Simulation-Only APIs

Simulation helpers belong in tests and debugging code:

livt
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:

livt
@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:

livt
@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:

livt
@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:

livt
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:

livt
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.