Zig 0.16 · v0.0.2 preview

Testing, results, and diagnostics

Architecture rules integrate with zig test at one explicit adapter edge. Underneath that edge, violations, diagnostics, logging records, and rendered results remain inspectable values.

A compiled example

const std = @import("std");
const archunit = @import("archunit");

pub fn renderedFailure() !archunit.TestResult {
    var files = try archunit.files(std.testing.allocator, .{});
    defer files.deinit();
    var service = try files.inFile(&.{"src/service/root.zig"});
    defer service.deinit();
    var should_not = try service.shouldNot();
    defer should_not.deinit();
    var dependency = try should_not.dependOnFiles();
    defer dependency.deinit();
    var rule = try dependency.inFile(&.{"src/domain/root.zig"});
    defer rule.deinit(std.testing.allocator);
    var violations = try rule.check(.init(std.testing.allocator, std.testing.io));
    defer violations.deinit(std.testing.allocator);
    const sentence = try rule.description(std.testing.allocator);
    defer std.testing.allocator.free(sentence);
    return archunit.ResultFactory.fromViolations(
        std.testing.allocator,
        violations.items(),
        sentence,
        .{ .color = .{ .mode = .never } },
    );
}

test "testing support renders a concrete failure" {
    var result = try renderedFailure();
    defer result.deinit(std.testing.allocator);
    try std.testing.expect(!result.passed);
    try std.testing.expect(std.mem.indexOf(u8, result.message, "src/service/root.zig:1:16") != null);
}

Compiled source: test/readme/testing.zig

Native test helpers

expectPasses and assertPasses accept a concrete terminal, run its check, render its owned description through ResultFactory, emit a failure once, and return error.ArchitectureViolation. Pass std.testing.allocator and the test's concrete std.Io so allocation and output remain test-controlled.

Assert structured evidence first

For a negative control, call check directly, inspect the tagged ViolationList, and assert exact paths, policy kinds, import kinds, or source locations. Then render the same value and assert the user-facing result contains the concrete evidence. This proves both policy behavior and presentation without brittle whole-message matching everywhere.

Stable result formatting

ViolationFactory converts every known violation variant into an owned heading, details, and plain sort key. ResultFactory orders those values, numbers failures, and combines them with the rule sentence. ANSI color is added only after ordering and is opt-in when terminal capability is known.

Errors and violations are different

A policy disagreement is a returned violation. Invalid configuration and technical failures use Zig error unions plus optional ErrorContext and Diagnostic values. Keep those channels separate: a parser failure should not masquerade as a forbidden dependency, and a normal architecture violation should not look like an extractor crash.

Explicit operation-local logging

CheckOptions.logging defaults to null and has zero logging work. When configured, one check or export owns a session over borrowed writers, callback sinks, or an optional file sink. Events use a fixed vocabulary for check boundaries, extraction, cache behavior, violations, metrics, and exports.

Level and family filters run before message allocation or clock access. Control bytes are sanitized, messages are capped, and absolute project roots are not included in progress records. Sink and write failures propagate instead of disappearing.

A resilient architecture-test pattern

  1. Compile a clean external fixture and assert its public-facade rules pass.
  2. Keep a minimal violating twin and assert exact structured evidence.
  3. Render the same violation to prove its human output.
  4. Run rules over the library itself so documentation and boundaries cannot drift together unnoticed.
Next: Internals