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.

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

Processes may need time to pass. Use states or simulation helpers to give the design cycles before checking output:

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

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.

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.