Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Testing

RSConstruct uses integration tests exclusively. All tests live in the tests/ directory and exercise the compiled rsconstruct binary as a black box — no unit tests are embedded in src/.

Running tests

cargo test              # Run all tests
cargo test rsconstructignore    # Run tests matching a name
cargo test -- --nocapture  # Show stdout/stderr from tests

Test directory layout

tests/
├── common/
│   └── mod.rs                  # Shared helpers (not a test binary)
├── build.rs                    # Build command tests
├── cache.rs                    # Cache operation tests
├── complete.rs                 # Shell completion tests
├── config.rs                   # Config show/show-default tests
├── dry_run.rs                  # Dry-run flag tests
├── graph.rs                    # Dependency graph tests
├── init.rs                     # Project initialization tests
├── processor_cmd.rs            # Processor list/auto/files tests
├── rsconstructignore.rs                # .rsconstructignore / .gitignore exclusion tests
├── status.rs                   # Status command tests
├── tools.rs                    # Tools list/check tests
├── watch.rs                    # File watcher tests
├── processors.rs               # Module root for processor tests
└── processors/
    ├── cc_single_file.rs       # C/C++ compilation tests
    ├── spellcheck.rs           # Spellcheck processor tests
    └── template.rs             # Template processor tests

Each top-level .rs file in tests/ is compiled as a separate test binary by Cargo. The processors.rs file acts as a module root that declares the processors/ subdirectory modules:

#![allow(unused)]
fn main() {
mod common;
mod processors {
    pub mod cc_single_file;
    pub mod spellcheck;
    pub mod template;
}
}

This is the standard Rust pattern for grouping related integration tests into subdirectories without creating a separate binary per file.

Shared helpers

tests/common/mod.rs provides utilities used across all test files:

HelperPurpose
setup_test_project()Create an isolated project in a temp directory with rsconstruct.toml and basic directories
setup_cc_project(path)Create a C project structure with the cc_single_file processor enabled
run_rsconstruct(dir, args)Execute the rsconstruct binary in the given directory and return its output
run_rsconstruct_with_env(dir, args, env)Same as run_rsconstruct but with extra environment variables (e.g., NO_COLOR=1)

All helpers use env!("CARGO_BIN_EXE_rsconstruct") to locate the compiled binary, ensuring tests run against the freshly built version.

Every test creates a fresh TempDir for isolation. The directory is automatically cleaned up when the test ends.

Test categories

Command tests

Tests in build.rs, clean, dry_run.rs, init.rs, status.rs, and watch.rs exercise CLI commands end-to-end:

#![allow(unused)]
fn main() {
#[test]
fn force_rebuild() {
    let temp_dir = setup_test_project();
    // ... set up files ...
    let output = run_rsconstruct_with_env(temp_dir.path(), &["build", "--force"], &[("NO_COLOR", "1")]);
    assert!(output.status.success());
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("[template] Processing:"));
}
}

These tests verify exit codes, stdout messages, and side effects (files created or removed).

Processor tests

Tests under processors/ verify individual processor behavior: file discovery, compilation, linting, incremental skip logic, and error handling. Each processor test module follows the same pattern:

  1. Set up a temp project with appropriate source files
  2. Run rsconstruct build
  3. Assert outputs exist and contain expected content
  4. Optionally modify a file and rebuild to test incrementality

Ignore tests

rsconstructignore.rs tests .rsconstructignore pattern matching: exact file patterns, glob patterns, leading / (anchored), trailing / (directory), comments, blank lines, and interaction with multiple processors.

Common assertion patterns

Exit code:

#![allow(unused)]
fn main() {
assert!(output.status.success());
}

Stdout content:

#![allow(unused)]
fn main() {
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Processing:"));
assert!(!stdout.contains("error"));
}

File existence:

#![allow(unused)]
fn main() {
assert!(path.join("out/cc_single_file/main.elf").exists());
}

Incremental builds:

#![allow(unused)]
fn main() {
// First build
run_rsconstruct(path, &["build"]);

// Second build should skip
let output = run_rsconstruct_with_env(path, &["build"], &[("NO_COLOR", "1")]);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Skipping (unchanged):"));
}

Mtime-dependent rebuilds:

#![allow(unused)]
fn main() {
// Modify a file and wait for mtime to differ
std::thread::sleep(std::time::Duration::from_millis(100));
fs::write(path.join("src/header.h"), "// changed\n").unwrap();

let output = run_rsconstruct(path, &["build"]);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Processing:"));
}

Writing a new test

  1. Add a test function in the appropriate file (or create a new .rs file under tests/ for a new feature area)
  2. Use setup_test_project() or setup_cc_project() to create an isolated environment
  3. Write source files and configuration into the temp directory
  4. Run rsconstruct with run_rsconstruct() or run_rsconstruct_with_env()
  5. Assert on exit code, stdout/stderr content, and output file existence

If adding a new processor test module, declare it in tests/processors.rs:

#![allow(unused)]
fn main() {
mod processors {
    pub mod cc_single_file;
    pub mod spellcheck;
    pub mod template;
    pub mod my_new_processor;  // add here
}
}

Test coverage by area

AreaFileTests
Build commandbuild.rsForce rebuild, incremental skip, clean, deterministic order, keep-going, timings, parallel -j flag, parallel keep-going, parallel all-products, parallel timings, parallel caching
Cachecache.rsClear, size, trim, list operations
Completecomplete.rsBash/zsh/fish generation, config-driven completion
Configconfig.rsShow merged config, show defaults, annotation comments
Dry rundry_run.rsPreview output, force flag, short flag
Graphgraph.rsDOT, mermaid, JSON, text formats, empty project
Initinit.rsProject creation, duplicate detection, existing directory preservation
Processor commandprocessor_cmd.rsList, all, auto-detect, files, unknown processor error
Statusstatus.rsUP-TO-DATE / STALE / RESTORABLE reporting
Toolstools.rsList tools, list all, check availability
Watchwatch.rsInitial build, rebuild on change
Ignorersconstructignore.rsExact match, globs, leading slash, trailing slash, comments, cross-processor
Templateprocessors/template.rsRendering, incremental, extra_inputs
CCprocessors/cc_single_file.rsCompilation, headers, per-file flags, mixed C/C++, config change detection
Spellcheckprocessors/spellcheck.rsCorrect/misspelled words, code block filtering, custom words, incremental