Zig 0.16 · v0.0.2 preview

Architecture tests that read like policy

ArchUnitZig turns a Zig project's imports into an owned dependency graph, then lets ordinary zig test tests enforce boundaries over files, layers, slices, reports, and Zig-native metrics.

Release status ArchUnitZig targets Zig 0.16.0 and is currently pre-release. Pin a reviewed commit when reproducibility matters.

Install from the package manager

Add the package with a stable dependency name. During pre-release development, replace main with a reviewed commit hash in durable projects.

zig fetch --save=archunit git+https://github.com/LukasNiessen/ArchUnitZig.git#main

Expose ArchUnitZig only to the test module that owns your architecture policy. This is the repository's compiled standalone-consumer fixture—not an abbreviated approximation.

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    const archunit = b.dependency("archunit", .{ .target = target, .optimize = optimize });

    const architecture = b.createModule(.{
        .root_source_file = b.path("test/architecture.zig"),
        .target = target,
        .optimize = optimize,
    });
    architecture.addImport("archunit", archunit.module("archunit"));
    const architecture_tests = b.addTest(.{ .root_module = architecture });
    const run_architecture_tests = b.addRunArtifact(architecture_tests);
    run_architecture_tests.setCwd(b.path("."));

    const format = b.addFmt(.{ .paths = &.{ "build.zig", "build.zig.zon", "src", "test" }, .check = true });
    const test_step = b.step("test", "Run formatting and architecture tests");
    test_step.dependOn(&format.step);
    test_step.dependOn(&run_architecture_tests.step);
}

Compiled source: test/fixtures/readme-consumer/build.zig

Write the first rule

This test selects production Zig files, asks for a positive policy, creates a cycle rule, and delegates failures to the native test adapter. Every fluent stage owns allocated state, so each stage has a matching deinit.

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

test "production files have no cycles" {
    var files = try archunit.files(std.testing.allocator, .{});
    defer files.deinit();
    var production = try files.inPath(&.{ .{ .glob = "src/*.zig" }, .{ .glob = "src/**/*.zig" } });
    defer production.deinit();
    var should = try production.should();
    defer should.deinit();
    var rule = try should.haveNoCycles();
    defer rule.deinit(std.testing.allocator);
    try archunit.expectPasses(&rule, .init(.init(std.testing.allocator, std.testing.io)));
}

Compiled source: test/fixtures/readme-consumer/test/architecture.zig

Run it

zig build test

A passing rule is quiet. A violation returns error.ArchitectureViolation and prints stable, structured evidence through ArchUnitZig's result formatter.

Lazy until the terminal

Selectors and predicates perform no project I/O. check, expectPasses, report rendering, or export is the execution boundary.

Allocator-visible ownership

Builders are independent owned values. Clone explicitly when branching and release every owned stage.

Violations are values

Rules return tagged, inspectable violations before presentation. Tests can assert exact paths, kinds, and dependency evidence.

Zig concepts stay Zig-shaped

Imports, resources, modules, declarations, containers, and coupling replace class-oriented vocabulary that has no honest Zig equivalent.

Choose a domain

Start with file rules for most boundaries, move to named layers for an architectural map, and use testing and diagnostics when integrating the result into a larger suite. The compiler-generated API reference lists the complete public facade.

Next: Grammar & patterns