Running a rule

A fluent sentence is lazy. Only a terminal locates the Cargo project, reads sources, extracts the graph, and evaluates the condition.

As a Rust test

assert_passes! is the native integration for an ordinary #[test]. It borrows the completed rule, evaluates it once, and preserves formatted violations or a classified execution error in the panic message.

use archunit::{assert_passes, project_files};

#[test]
fn source_is_acyclic() {
    let rule = project_files().in_path("src/**").should().have_no_cycles();
    assert_passes!(rule);
}

Any Rust test framework that recognizes assertion panics needs no additional adapter or setup.

As structured data

Every terminal implements the object-safe Checkable contract:

use archunit::{ArchUnitError, Checkable, ViolationKind, project_files};

fn inspect() -> Result<(), ArchUnitError> {
    let rule = project_files()
        .in_path("src/api/**")
        .should_not()
        .depend_on_files()
        .in_path("src/database/**");
    let violations = rule.check()?;

    for violation in &violations {
        println!("{}", violation.kind());
        assert_eq!(violation.kind(), ViolationKind::FileDependency);
    }
    Ok(())
}

Ok(Vec::new()) means the rule passed. Ok with violations is an architecture verdict. Err(ArchUnitError::User(_)) means invalid API input; Err(ArchUnitError::Technical(_)) means the library or environment could not complete the check.

The closed Violation enum retains typed data for empty selections, cycles, file patterns, internal or external dependencies, custom file predicates, layers, slices, metric zones, custom metrics, numeric thresholds, and metric predicates. ViolationFactory and ResultFactory turn that data into consistent test output only at the reporting edge.

Check options

CheckOptions::new() is strict, quiet, cache-friendly, and production-only. Its consuming modifiers are:

Modifier Effect
with_allow_empty_tests(bool) opt out of the strict empty-selection guard
with_logging(LoggingOptions) enable this check’s explicit logger
with_clear_cache(bool) discard a matching extraction cache entry before this run
with_test_sources(bool) include Cargo test, example, and benchmark targets

Pass the value to check_with(&options) or as the macro’s second argument:

use archunit::{CheckOptions, assert_passes, project_files};

let options = CheckOptions::new().with_test_sources(true);
let rule = project_files().in_path("tests/**").should().have_no_cycles();
assert_passes!(rule, options);

Cache behavior

Extraction results are memoized by project identity and source options. Repeated rules in one test process can share the same immutable graph. Use with_clear_cache(true) when the process changed source files after a previous check, or call clear_graph_cache() for an explicit global reset.

Per-check logging

Logging has no environment-variable or process-global switch. Put one LoggingOptions value into the options for the check that needs it:

use archunit::{
    ArchUnitError, CheckOptions, Checkable, LogFileMode, LogLevel, LoggingOptions, project_files,
};

fn logged_check() -> Result<(), ArchUnitError> {
    let logging = LoggingOptions::new()
        .with_level(LogLevel::Debug)
        .with_console_output(false)
        .with_file_output("target/architecture-logs")
        .with_file_mode(LogFileMode::Overwrite);
    let options = CheckOptions::new().with_logging(logging);
    let rule = project_files()
        .in_path("src/api/**")
        .should_not()
        .depend_on_files()
        .in_path("src/database/**");

    assert!(rule.check_with(&options)?.is_empty());
    Ok(())
}

The levels are Debug, Info, Warn, and Error. The event vocabulary distinguishes check start and end, progress, violations, metrics, and ordinary caller records. File output creates parent directories and a collision-resistant timestamped .log; file_path() exposes the artifact path before the check so CI can archive it.

Keep one import out of the graph

Put archunit: ignore on the same line as an import or immediately above it:

use legacy_client::Client; // archunit: ignore

// Only this member of the grouped import is ignored.
use crate::adapters::{legacy, current}; // archunit: ignore crate::adapters::legacy

An optional scope matches the written Rust path exactly or by :: prefix. Ignored imports still establish aliases for resolving later qualified paths, and the directive does not suppress a separate expression or type path.

When a rule selects nothing

The default EmptyTestViolation protects against misspelled and stale scopes. It checks selected subjects, not derived edges, so an existing isolated file is not empty. Make a genuinely optional scope explicit:

use archunit::{CheckOptions, assert_passes, project_files};

let rule = project_files()
    .in_path("generated/**")
    .should()
    .have_no_cycles();
let options = CheckOptions::new().with_allow_empty_tests(true);

assert_passes!(rule, options);

Continue with how it works for the extraction pipeline and the boundaries the crate enforces on itself.