Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions FEATURES_SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,12 +258,10 @@ This document provides a comprehensive overview of all implemented features in t
- **Status:** Production Ready
- **Module:** `src/config.zig`
- **Features:**
- YAML configuration support
- JSON configuration support
- TOML configuration support
- Configuration validation
- Strict JSON configuration support
- Unknown-key, type, and runtime-value validation
- CLI-over-config precedence
- Default configuration
- Environment-specific configs

### 16. Test History ✅

Expand Down
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ A modern, feature-rich testing framework for Zig inspired by Jest, Vitest, and B
- **Watch Mode** - Automatically re-run tests on file changes
- **Memory Profiling** - Track memory usage and detect leaks
- **Parallel Execution** - Run tests in parallel for faster execution
- **Configuration Files** - YAML/JSON/TOML config file support
- **Configuration Files** - Strict JSON config files with CLI overrides

## Installation

Expand Down Expand Up @@ -100,8 +100,8 @@ This will automatically discover and run all `*.test.zig` files in the `tests` d

Discovery mode rejects options that it cannot honor yet instead of silently
ignoring them. Reporter selection, quiet output, timeouts, parallel jobs,
snapshot updates, memory profiling, configuration files, JUnit output, and the
web UI require the shared execution pipeline planned for future releases.
snapshot updates, memory profiling, JUnit output, and the web UI require the
shared execution pipeline planned for future releases.

**Examples:**

Expand All @@ -119,6 +119,33 @@ zig-test --test-dir tests --no-recursive
zig-test --test-dir tests --bail
```

### Configuration files

`--config <file>` loads a strict JSON configuration before test discovery.
Unknown keys, duplicate keys, wrong value types, unsupported extensions, and
invalid runtime values fail with a configuration error. TOML and YAML are not
currently supported. Explicit CLI options are parsed after the file and take
precedence over configured values.

```json
{
"test": {
"test_dir": "tests",
"pattern": "*.test.zig",
"recursive": true,
"filter": "database"
},
"coverage": {
"enabled": false,
"output_dir": "coverage"
}
}
```

The accepted top-level sections are `test`, `parallel`, `reporter`, `snapshot`,
`watch`, `memory`, `ui`, and `coverage`. Options retain the same execution-mode
limitations as their CLI equivalents; unsupported combinations fail explicitly.

### Programmatic Mode

For more control, you can manually register tests:
Expand Down Expand Up @@ -824,7 +851,7 @@ MIT License - see [LICENSE](LICENSE) for details.
- [x] Watch mode for file changes
- [x] TAP/JUnit reporters
- [x] Memory profiling
- [x] Configuration files (YAML/JSON/TOML)
- [x] Strict JSON configuration files
- [x] Parallel test execution
- [ ] Parameterized tests (it.each)
- [ ] Property-based testing
Expand Down
16 changes: 16 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,22 @@ pub fn build(b: *std.Build) void {
test_step.dependOn(&run_snapshot_usage_tests.step);
test_step.dependOn(&run_time_tests.step);

// Exercise configuration through the real CLI. The second invocation
// proves that an explicit CLI filter overrides the configured filter.
const run_config_fixture = b.addRunArtifact(exe);
run_config_fixture.addArgs(&.{ "--config", "tests/fixtures/zig-test.json" });
test_step.dependOn(&run_config_fixture.step);

const run_config_override = b.addRunArtifact(exe);
run_config_override.addArgs(&.{ "--config", "tests/fixtures/zig-test.json", "--filter", "unselected test fails" });
run_config_override.expectExitCode(1);
test_step.dependOn(&run_config_override.step);

const run_invalid_config = b.addRunArtifact(exe);
run_invalid_config.addArgs(&.{ "--config", "tests/fixtures/invalid-zig-test.json" });
run_invalid_config.expectExitCode(2);
test_step.dependOn(&run_invalid_config.step);

// Examples
const basic_example = b.addExecutable(.{
.name = "basic_example",
Expand Down
53 changes: 39 additions & 14 deletions src/cli.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const std = @import("std");
const config_mod = @import("config.zig");
const test_runner = @import("test_runner.zig");

pub const CLIOptions = struct {
Expand Down Expand Up @@ -61,6 +62,32 @@ pub const CLI = struct {
};
}

/// Apply configuration defaults before parsing command-line arguments.
/// A subsequent `parse` call gives explicitly supplied CLI flags priority.
pub fn applyConfig(self: *Self, config: config_mod.TestConfig) !void {
self.options.test_dir = config.test_options.test_dir;
self.options.pattern = config.test_options.pattern;
self.options.no_recursive = !config.test_options.recursive;
self.options.filter = config.test_options.filter;
self.options.timeout = config.test_options.timeout;
self.options.parallel = config.parallel.enabled;
self.options.jobs = config.parallel.jobs;
self.options.reporter = try reporterFromName(config.reporter.reporter);
self.options.junit_output = config.reporter.junit_output;
self.options.verbose = config.reporter.verbose;
self.options.update_snapshots = config.snapshot.update;
self.options.snapshot_dir = config.snapshot.snapshot_dir;
self.options.watch = config.watch.enabled;
self.options.watch_debounce = config.watch.debounce_ms;
self.options.profile_memory = config.memory.enabled;
self.options.memory_threshold = config.memory.report_threshold;
self.options.fail_on_leak = config.memory.fail_on_leak;
self.options.ui = config.ui.enabled;
self.options.ui_port = config.ui.port;
self.options.coverage = config.coverage.enabled;
self.options.coverage_dir = config.coverage.output_dir;
}

/// Parse command-line arguments
pub fn parse(self: *Self, args: []const []const u8) !void {
var i: usize = 1; // Skip program name
Expand All @@ -87,20 +114,10 @@ pub const CLI = struct {
i += 1;
const reporter_name = args[i];

if (std.mem.eql(u8, reporter_name, "spec")) {
self.options.reporter = .spec;
} else if (std.mem.eql(u8, reporter_name, "dot")) {
self.options.reporter = .dot;
} else if (std.mem.eql(u8, reporter_name, "json")) {
self.options.reporter = .json;
} else if (std.mem.eql(u8, reporter_name, "tap")) {
self.options.reporter = .tap;
} else if (std.mem.eql(u8, reporter_name, "junit")) {
self.options.reporter = .junit;
} else {
self.options.reporter = reporterFromName(reporter_name) catch {
std.debug.print("Error: Unknown reporter '{s}'. Available: spec, dot, json, tap, junit\n", .{reporter_name});
return CLIError.InvalidArgument;
}
};
} else if (std.mem.eql(u8, arg, "--filter") or std.mem.eql(u8, arg, "--grep")) {
if (i + 1 >= args.len) {
std.debug.print("Error: --filter/--grep requires a value\n", .{});
Expand Down Expand Up @@ -273,7 +290,6 @@ pub const CLI = struct {
if (self.options.profile_memory) return "--profile-memory";
if (self.options.memory_threshold != 0) return "--memory-threshold";
if (self.options.fail_on_leak) return "--fail-on-leak";
if (self.options.config != null) return "--config";
if (self.options.junit_output != null) return "--junit-output";
if (self.options.timeout != null) return "--timeout";
if (self.options.ui) return "--ui";
Expand All @@ -300,7 +316,7 @@ pub const CLI = struct {
\\ -q, --quiet Minimal output
\\ --no-color Disable colored output
\\ --timeout <ms> Global timeout for all tests in milliseconds
\\ -c, --config <file> Load configuration from file
\\ -c, --config <file> Load strict JSON configuration
\\
\\TEST DISCOVERY:
\\ --test-dir <dir> Directory to search for tests (default: .)
Expand Down Expand Up @@ -384,6 +400,15 @@ pub const CLI = struct {
}
};

fn reporterFromName(name: []const u8) !test_runner.ReporterType {
if (std.mem.eql(u8, name, "spec")) return .spec;
if (std.mem.eql(u8, name, "dot")) return .dot;
if (std.mem.eql(u8, name, "json")) return .json;
if (std.mem.eql(u8, name, "tap")) return .tap;
if (std.mem.eql(u8, name, "junit")) return .junit;
return CLIError.InvalidArgument;
}

test "CLI parse help" {
var cli = CLI.init(std.testing.allocator);
const args = [_][]const u8{ "zig-test", "--help" };
Expand Down
Loading
Loading