ReviewOS

also looking at this

zig-utils/zig-test-framework

feat: load strict JSON configuration files

#19
Merged glennmichael123 wants to merge codex/issue-12-config into main
10 files +249 -286

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

src/cli.zigmodified+39-14
Changes to src/cli.zig
@@ -1,4 +1,5 @@
11const std = ;
2const config_mod = ;
23const test_runner = ;
34
45pub const CLIOptions = struct {
@@ -61,6 +62,32 @@ pub const CLI = struct {
6162 };
6263 }
6364
65 /// Apply configuration defaults before parsing command-line arguments.
66 /// A subsequent `parse` call gives explicitly supplied CLI flags priority.
67 pub fn applyConfig(self: *Self, config: config_mod.TestConfig) !void {
68 self.options.test_dir = config.test_options.test_dir;
69 self.options.pattern = config.test_options.pattern;
70 self.options.no_recursive = !config.test_options.recursive;
71 self.options.filter = config.test_options.filter;
72 self.options.timeout = config.test_options.timeout;
73 self.options.parallel = config.parallel.enabled;
74 self.options.jobs = config.parallel.jobs;
75 self.options.reporter = try reporterFromName(config.reporter.reporter);
76 self.options.junit_output = config.reporter.junit_output;
77 self.options.verbose = config.reporter.verbose;
78 self.options.update_snapshots = config.snapshot.update;
79 self.options.snapshot_dir = config.snapshot.snapshot_dir;
80 self.options.watch = config.watch.enabled;
81 self.options.watch_debounce = config.watch.debounce_ms;
82 self.options.profile_memory = config.memory.enabled;
83 self.options.memory_threshold = config.memory.report_threshold;
84 self.options.fail_on_leak = config.memory.fail_on_leak;
85 self.options.ui = config.ui.enabled;
86 self.options.ui_port = config.ui.port;
87 self.options.coverage = config.coverage.enabled;
88 self.options.coverage_dir = config.coverage.output_dir;
89 }
90
6491 /// Parse command-line arguments
6592 pub fn parse(self: *Self, args: []const []const u8) !void {
6693 var i: usize = 1; // Skip program name
@@ -87,20 +114,10 @@ pub const CLI = struct {
87114 i += 1;
88115 const reporter_name = args[i];
89116
90 if (std.mem.eql(u8, reporter_name, "spec")) {
91 self.options.reporter = .spec;
92 } else if (std.mem.eql(u8, reporter_name, "dot")) {
93 self.options.reporter = .dot;
94 } else if (std.mem.eql(u8, reporter_name, "json")) {
95 self.options.reporter = .json;
96 } else if (std.mem.eql(u8, reporter_name, "tap")) {
97 self.options.reporter = .tap;
98 } else if (std.mem.eql(u8, reporter_name, "junit")) {
99 self.options.reporter = .junit;
100 } else {
117 self.options.reporter = reporterFromName(reporter_name) catch {
101118 std.debug.print("Error: Unknown reporter '{s}'. Available: spec, dot, json, tap, junit\n", .{reporter_name});
102119 return CLIError.InvalidArgument;
103 }
120 };
104121 } else if (std.mem.eql(u8, arg, "--filter") or std.mem.eql(u8, arg, "--grep")) {
105122 if (i + 1 >= args.len) {
106123 std.debug.print("Error: --filter/--grep requires a value\n", .{});
@@ -273,7 +290,6 @@ pub const CLI = struct {
273290 if (self.options.profile_memory) return "--profile-memory";
274291 if (self.options.memory_threshold != 0) return "--memory-threshold";
275292 if (self.options.fail_on_leak) return "--fail-on-leak";
276 if (self.options.config != null) return "--config";
277293 if (self.options.junit_output != null) return "--junit-output";
278294 if (self.options.timeout != null) return "--timeout";
279295 if (self.options.ui) return "--ui";
@@ -300,7 +316,7 @@ pub const CLI = struct {
300316 \\ -q, --quiet Minimal output
301317 \\ --no-color Disable colored output
302318 \\ --timeout <ms> Global timeout for all tests in milliseconds
303 \\ -c, --config <file> Load configuration from file
319 \\ -c, --config <file> Load strict JSON configuration
304320 \\
305321 \\TEST DISCOVERY:
306322 \\ --test-dir <dir> Directory to search for tests (default: .)
@@ -384,6 +400,15 @@ pub const CLI = struct {
384400 }
385401};
386402
403fn reporterFromName(name: []const u8) !test_runner.ReporterType {
404 if (std.mem.eql(u8, name, "spec")) return .spec;
405 if (std.mem.eql(u8, name, "dot")) return .dot;
406 if (std.mem.eql(u8, name, "json")) return .json;
407 if (std.mem.eql(u8, name, "tap")) return .tap;
408 if (std.mem.eql(u8, name, "junit")) return .junit;
409 return CLIError.InvalidArgument;
410}
411
387412test "CLI parse help" {
388413 var cli = CLI.init(std.testing.allocator);
389414 const args = [_][]const u8{ "zig-test", "--help" };
src/config.zigmodified+115-263
Changes to src/config.zig
@@ -1,309 +1,134 @@
11const std = ;
22const compat = ;
33
4/// Configuration file format
5pub const ConfigFormat = enum {
6 json,
7 toml, // Future support
8};
4/// JSON is the only configuration format currently supported by the CLI.
5pub const ConfigFormat = enum { json };
96
10/// Test framework configuration
7/// Configuration values mirror options that the current execution paths can
8/// actually honor. Defaults intentionally match `CLIOptions`.
119pub const TestConfig = struct {
12 // Test discovery options
1310 test_options: TestOptions = .{},
14
15 /// Parallel execution options
1611 parallel: ParallelOptions = .{},
17
18 /// Reporter options
1912 reporter: ReporterOptions = .{},
20
21 /// Snapshot testing options
2213 snapshot: SnapshotOptions = .{},
23
24 /// Watch mode options
2514 watch: WatchOptions = .{},
26
27 /// Memory profiling options
2815 memory: MemoryOptions = .{},
29
30 /// UI server options
3116 ui: UIOptions = .{},
32
33 /// Coverage options
3417 coverage: CoverageOptions = .{},
3518};
3619
3720pub const TestOptions = struct {
38 /// Test file pattern
3921 pattern: []const u8 = "*.test.zig",
40 /// Test directory
41 test_dir: []const u8 = "src",
42 /// Recursive search
22 test_dir: []const u8 = ".",
4323 recursive: bool = true,
44 /// Filter tests by name
4524 filter: ?[]const u8 = null,
46 /// Timeout per test in milliseconds
47 timeout: u64 = 5000,
25 timeout: ?u64 = null,
4826};
4927
5028pub const ParallelOptions = struct {
51 /// Enable parallel execution
5229 enabled: bool = false,
53 /// Number of worker threads (0 = auto-detect)
54 jobs: usize = 0,
30 jobs: ?usize = null,
5531};
5632
5733pub const ReporterOptions = struct {
58 /// Reporter type: spec, dot, json, tap, junit
5934 reporter: []const u8 = "spec",
60 /// JUnit XML output file
6135 junit_output: ?[]const u8 = null,
62 /// Verbose output
6336 verbose: bool = false,
6437};
6538
6639pub const SnapshotOptions = struct {
67 /// Snapshot directory
6840 snapshot_dir: []const u8 = ".snapshots",
69 /// Update snapshots
7041 update: bool = false,
71 /// Pretty print snapshots
72 pretty_print: bool = true,
7342};
7443
7544pub const WatchOptions = struct {
76 /// Enable watch mode
7745 enabled: bool = false,
78 /// Directory to watch
79 watch_dir: []const u8 = ".",
80 /// Debounce delay in milliseconds
8146 debounce_ms: u64 = 300,
82 /// Clear screen between runs
83 clear_screen: bool = true,
8447};
8548
8649pub const MemoryOptions = struct {
87 /// Enable memory profiling
8850 enabled: bool = false,
89 /// Detect memory leaks
90 detect_leaks: bool = true,
91 /// Track peak memory usage
92 track_peak: bool = true,
93 /// Report threshold in bytes
9451 report_threshold: usize = 0,
95 /// Fail tests on memory leaks
9652 fail_on_leak: bool = false,
9753};
9854
9955pub const UIOptions = struct {
100 /// Enable UI server
10156 enabled: bool = false,
102 /// Port for UI server
10357 port: u16 = 8080,
104 /// Open browser automatically
105 open_browser: bool = true,
10658};
10759
10860pub const CoverageOptions = struct {
109 /// Enable coverage tracking
11061 enabled: bool = false,
111 /// Coverage output directory
11262 output_dir: []const u8 = "coverage",
113 /// Minimum coverage threshold
114 threshold: f64 = 0.0,
11563};
11664
117/// Configuration loader
65const FileConfig = struct {
66 @"test": TestOptions = .{},
67 parallel: ParallelOptions = .{},
68 reporter: ReporterOptions = .{},
69 snapshot: SnapshotOptions = .{},
70 watch: WatchOptions = .{},
71 memory: MemoryOptions = .{},
72 ui: UIOptions = .{},
73 coverage: CoverageOptions = .{},
74};
75
76pub const ConfigError = error{
77 UnsupportedFormat,
78 InvalidReporter,
79 InvalidJobs,
80 InvalidPort,
81 InvalidTimeout,
82 InvalidDebounce,
83};
84
85/// Owns strings parsed from configuration files until `deinit` is called.
11886pub const ConfigLoader = struct {
11987 allocator: std.mem.Allocator,
88 arena: std.heap.ArenaAllocator,
12089
12190 const Self = @This();
12291
12392 pub fn init(allocator: std.mem.Allocator) Self {
12493 return .{
12594 .allocator = allocator,
95 .arena = .init(allocator),
12696 };
12797 }
12898
129 /// Load configuration from file
130 pub fn loadFromFile(self: *Self, path: []const u8) !TestConfig {
131 // Detect format from extension
132 const format = if (std.mem.endsWith(u8, path, ".json"))
133 ConfigFormat.json
134 else if (std.mem.endsWith(u8, path, ".toml"))
135 ConfigFormat.toml
136 else
137 return error.UnsupportedFormat;
138
139 switch (format) {
140 .json => return try self.loadFromJson(path),
141 .toml => return error.TomlNotImplementedYet,
142 }
99 pub fn deinit(self: *Self) void {
100 self.arena.deinit();
143101 }
144102
145 /// Load configuration from JSON file
146 fn loadFromJson(self: *Self, path: []const u8) !TestConfig {
103 /// Load a strict JSON configuration. Unknown keys, duplicate keys, and
104 /// values of the wrong type are rejected by the standard JSON parser.
105 pub fn loadFromFile(self: *Self, path: []const u8) !TestConfig {
106 if (!std.mem.endsWith(u8, path, ".json")) return ConfigError.UnsupportedFormat;
107
147108 const content = try compat.readFileAlloc(self.allocator, path);
148109 defer self.allocator.free(content);
149110
150 var parsed = try std.json.parseFromSlice(std.json.Value, self.allocator, content, .{});
151 defer parsed.deinit();
152
153 return try self.parseConfig(parsed.value);
154 }
155
156 /// Parse configuration from JSON value
157 fn parseConfig(self: *Self, value: std.json.Value) !TestConfig {
158 _ = self;
159
160 var config = TestConfig{};
161
162 if (value != .object) return config;
163
164 const root = value.object;
165
166 // Parse test options
167 if (root.get("test")) |test_value| {
168 if (test_value == .object) {
169 const test_obj = test_value.object;
170 if (test_obj.get("pattern")) |v| {
171 if (v == .string) config.test_options.pattern = v.string;
172 }
173 if (test_obj.get("test_dir")) |v| {
174 if (v == .string) config.test_options.test_dir = v.string;
175 }
176 if (test_obj.get("recursive")) |v| {
177 if (v == .bool) config.test_options.recursive = v.bool;
178 }
179 if (test_obj.get("timeout")) |v| {
180 if (v == .integer) config.test_options.timeout = @intCast(v.integer);
181 }
182 }
183 }
184
185 // Parse parallel options
186 if (root.get("parallel")) |parallel_value| {
187 if (parallel_value == .object) {
188 const parallel_obj = parallel_value.object;
189 if (parallel_obj.get("enabled")) |v| {
190 if (v == .bool) config.parallel.enabled = v.bool;
191 }
192 if (parallel_obj.get("jobs")) |v| {
193 if (v == .integer) config.parallel.jobs = @intCast(v.integer);
194 }
195 }
196 }
197
198 // Parse reporter options
199 if (root.get("reporter")) |reporter_value| {
200 if (reporter_value == .object) {
201 const reporter_obj = reporter_value.object;
202 if (reporter_obj.get("reporter")) |v| {
203 if (v == .string) config.reporter.reporter = v.string;
204 }
205 if (reporter_obj.get("junit_output")) |v| {
206 if (v == .string) config.reporter.junit_output = v.string;
207 }
208 if (reporter_obj.get("verbose")) |v| {
209 if (v == .bool) config.reporter.verbose = v.bool;
210 }
211 }
212 }
213
214 // Parse snapshot options
215 if (root.get("snapshot")) |snapshot_value| {
216 if (snapshot_value == .object) {
217 const snapshot_obj = snapshot_value.object;
218 if (snapshot_obj.get("snapshot_dir")) |v| {
219 if (v == .string) config.snapshot.snapshot_dir = v.string;
220 }
221 if (snapshot_obj.get("update")) |v| {
222 if (v == .bool) config.snapshot.update = v.bool;
223 }
224 if (snapshot_obj.get("pretty_print")) |v| {
225 if (v == .bool) config.snapshot.pretty_print = v.bool;
226 }
227 }
228 }
229
230 // Parse watch options
231 if (root.get("watch")) |watch_value| {
232 if (watch_value == .object) {
233 const watch_obj = watch_value.object;
234 if (watch_obj.get("enabled")) |v| {
235 if (v == .bool) config.watch.enabled = v.bool;
236 }
237 if (watch_obj.get("watch_dir")) |v| {
238 if (v == .string) config.watch.watch_dir = v.string;
239 }
240 if (watch_obj.get("debounce_ms")) |v| {
241 if (v == .integer) config.watch.debounce_ms = @intCast(v.integer);
242 }
243 if (watch_obj.get("clear_screen")) |v| {
244 if (v == .bool) config.watch.clear_screen = v.bool;
245 }
246 }
247 }
248
249 // Parse memory options
250 if (root.get("memory")) |memory_value| {
251 if (memory_value == .object) {
252 const memory_obj = memory_value.object;
253 if (memory_obj.get("enabled")) |v| {
254 if (v == .bool) config.memory.enabled = v.bool;
255 }
256 if (memory_obj.get("detect_leaks")) |v| {
257 if (v == .bool) config.memory.detect_leaks = v.bool;
258 }
259 if (memory_obj.get("track_peak")) |v| {
260 if (v == .bool) config.memory.track_peak = v.bool;
261 }
262 if (memory_obj.get("report_threshold")) |v| {
263 if (v == .integer) config.memory.report_threshold = @intCast(v.integer);
264 }
265 if (memory_obj.get("fail_on_leak")) |v| {
266 if (v == .bool) config.memory.fail_on_leak = v.bool;
267 }
268 }
269 }
270
271 // Parse UI options
272 if (root.get("ui")) |ui_value| {
273 if (ui_value == .object) {
274 const ui_obj = ui_value.object;
275 if (ui_obj.get("enabled")) |v| {
276 if (v == .bool) config.ui.enabled = v.bool;
277 }
278 if (ui_obj.get("port")) |v| {
279 if (v == .integer) config.ui.port = @intCast(v.integer);
280 }
281 if (ui_obj.get("open_browser")) |v| {
282 if (v == .bool) config.ui.open_browser = v.bool;
283 }
284 }
285 }
286
287 // Parse coverage options
288 if (root.get("coverage")) |coverage_value| {
289 if (coverage_value == .object) {
290 const coverage_obj = coverage_value.object;
291 if (coverage_obj.get("enabled")) |v| {
292 if (v == .bool) config.coverage.enabled = v.bool;
293 }
294 if (coverage_obj.get("output_dir")) |v| {
295 if (v == .string) config.coverage.output_dir = v.string;
296 }
297 if (coverage_obj.get("threshold")) |v| {
298 if (v == .float) config.coverage.threshold = v.float;
299 }
300 }
301 }
302
111 const file_config = try std.json.parseFromSliceLeaky(
112 FileConfig,
113 self.arena.allocator(),
114 content,
115 .{ .allocate = .alloc_always },
116 );
117 const config = TestConfig{
118 .test_options = file_config.@"test",
119 .parallel = file_config.parallel,
120 .reporter = file_config.reporter,
121 .snapshot = file_config.snapshot,
122 .watch = file_config.watch,
123 .memory = file_config.memory,
124 .ui = file_config.ui,
125 .coverage = file_config.coverage,
126 };
127 try validate(config);
303128 return config;
304129 }
305130
306 /// Try to find and load configuration file
131 /// Try conventional JSON configuration locations.
307132 pub fn autoLoad(self: *Self, config_name: []const u8) !?TestConfig {
308133 const search_paths = [_][]const u8{
309134 "zig-test.json",
@@ -312,55 +137,82 @@ pub const ConfigLoader = struct {
312137 ".config/zig-test.json",
313138 };
314139
315 for (search_paths) |path| {
316 const full_path = if (std.mem.eql(u8, config_name, "zig-test"))
317 path
318 else
319 try std.fmt.allocPrint(self.allocator, "{s}.json", .{config_name});
320
321 defer if (!std.mem.eql(u8, config_name, "zig-test")) self.allocator.free(full_path);
322
323 const config = self.loadFromFile(full_path) catch |err| {
324 if (err == error.FileNotFound) continue;
325 return err;
140 if (!std.mem.eql(u8, config_name, "zig-test")) {
141 const path = try std.fmt.allocPrint(self.allocator, "{s}.json", .{config_name});
142 defer self.allocator.free(path);
143 return self.loadFromFile(path) catch |err| switch (err) {
144 error.FileNotFound => null,
145 else => return err,
326146 };
147 }
327148
149 for (search_paths) |path| {
150 const config = self.loadFromFile(path) catch |err| switch (err) {
151 error.FileNotFound => continue,
152 else => return err,
153 };
328154 return config;
329155 }
330
331156 return null;
332157 }
333158};
334159
335// Tests
336test "TestConfig default values" {
337 const config = TestConfig{};
160fn validate(config: TestConfig) !void {
161 const reporter = config.reporter.reporter;
162 if (!std.mem.eql(u8, reporter, "spec") and
163 !std.mem.eql(u8, reporter, "dot") and
164 !std.mem.eql(u8, reporter, "json") and
165 !std.mem.eql(u8, reporter, "tap") and
166 !std.mem.eql(u8, reporter, "junit"))
167 {
168 return ConfigError.InvalidReporter;
169 }
170 if (config.parallel.jobs) |jobs| {
171 if (jobs == 0) return ConfigError.InvalidJobs;
172 }
173 if (config.ui.port == 0) return ConfigError.InvalidPort;
174 if (config.test_options.timeout) |timeout| {
175 if (timeout == 0) return ConfigError.InvalidTimeout;
176 }
177 if (config.watch.debounce_ms == 0) return ConfigError.InvalidDebounce;
178}
338179
180test "TestConfig defaults match CLI discovery defaults" {
181 const config = TestConfig{};
339182 try std.testing.expectEqualStrings("*.test.zig", config.test_options.pattern);
340 try std.testing.expectEqualStrings("src", config.test_options.test_dir);
341 try std.testing.expectEqual(true, config.test_options.recursive);
342 try std.testing.expectEqual(false, config.parallel.enabled);
183 try std.testing.expectEqualStrings(".", config.test_options.test_dir);
184 try std.testing.expect(config.test_options.recursive);
185 try std.testing.expect(config.test_options.timeout == null);
186 try std.testing.expect(!config.parallel.enabled);
343187 try std.testing.expectEqualStrings("spec", config.reporter.reporter);
344188}
345189
346test "ConfigLoader initialization" {
347 const allocator = std.testing.allocator;
348
349 const loader = ConfigLoader.init(allocator);
350 try std.testing.expect(loader.allocator.ptr == allocator.ptr);
190test "ConfigLoader rejects unsupported formats" {
191 var loader = ConfigLoader.init(std.testing.allocator);
192 defer loader.deinit();
193 try std.testing.expectError(ConfigError.UnsupportedFormat, loader.loadFromFile("zig-test.toml"));
351194}
352195
353test "ConfigLoader parse empty JSON" {
354 const allocator = std.testing.allocator;
355
356 var loader = ConfigLoader.init(allocator);
196test "configuration validation rejects invalid runtime values" {
197 var invalid = TestConfig{};
198 invalid.reporter.reporter = "pretty";
199 try std.testing.expectError(ConfigError.InvalidReporter, validate(invalid));
200 invalid.reporter.reporter = "spec";
201 invalid.parallel.jobs = 0;
202 try std.testing.expectError(ConfigError.InvalidJobs, validate(invalid));
203}
357204
358 var parsed = try std.json.parseFromSlice(std.json.Value, allocator, "{}", .{});
359 defer parsed.deinit();
205test "ConfigLoader loads supported JSON and rejects unknown keys" {
206 var loader = ConfigLoader.init(std.testing.allocator);
207 defer loader.deinit();
360208
361 const config = try loader.parseConfig(parsed.value);
209 const config = try loader.loadFromFile("tests/fixtures/zig-test.json");
210 try std.testing.expectEqualStrings("tests/fixtures", config.test_options.test_dir);
211 try std.testing.expectEqualStrings("selected test passes", config.test_options.filter.?);
212 try std.testing.expect(!config.test_options.recursive);
362213
363 // Should return defaults
364 try std.testing.expectEqualStrings("spec", config.reporter.reporter);
365 try std.testing.expectEqual(false, config.parallel.enabled);
214 try std.testing.expectError(
215 error.UnknownField,
216 loader.loadFromFile("tests/fixtures/invalid-zig-test.json"),
217 );
366218}
src/lib.zigmodified+1-0
Changes to src/lib.zig
@@ -161,6 +161,7 @@ pub const ProfileOptions = memory_profiler.ProfileOptions;
161161pub const TestConfig = config.TestConfig;
162162pub const ConfigLoader = config.ConfigLoader;
163163pub const ConfigFormat = config.ConfigFormat;
164pub const ConfigError = config.ConfigError;
164165
165166// Async Support
166167pub const Future = async_support.Future;
src/main.zigmodified+14-0
Changes to src/main.zig
@@ -71,6 +71,20 @@ pub fn main(init: std.process.Init.Minimal) !void {
7171 return;
7272 }
7373
74 // Configuration establishes defaults; parsing the arguments again makes
75 // every explicitly supplied CLI flag take precedence.
76 var config_loader = lib.ConfigLoader.init(allocator);
77 defer config_loader.deinit();
78 if (cli_parser.options.config) |config_path| {
79 const config = config_loader.loadFromFile(config_path) catch |err| {
80 std.debug.print("Error: invalid configuration '{s}': {s}\n", .{ config_path, @errorName(err) });
81 std.process.exit(2);
82 };
83 cli_parser = lib.CLI.init(allocator);
84 try cli_parser.applyConfig(config);
85 try cli_parser.parse(args);
86 }
87
7488 // Check if we should use test discovery or programmatic tests
7589 const use_discovery = cli_parser.options.test_dir != null;
7690
tests/cli_test.zigmodified+17-0
Changes to tests/cli_test.zig
@@ -23,3 +23,20 @@ test "CLI rejects missing and invalid values" {
2323 var invalid = ztf.CLI.init(std.testing.allocator);
2424 try std.testing.expectError(ztf.cli.CLIError.InvalidArgument, invalid.parse(&.{ "zig-test", "--jobs", "0" }));
2525}
26
27test "CLI flags override configuration defaults" {
28 var cli = ztf.CLI.init(std.testing.allocator);
29 var config = ztf.TestConfig{};
30 config.test_options.test_dir = "configured-tests";
31 config.test_options.filter = "configured-filter";
32 config.test_options.recursive = false;
33 config.reporter.verbose = true;
34
35 try cli.applyConfig(config);
36 try cli.parse(&.{ "zig-test", "--test-dir", "cli-tests", "--filter", "cli-filter" });
37
38 try std.testing.expectEqualStrings("cli-tests", cli.options.test_dir.?);
39 try std.testing.expectEqualStrings("cli-filter", cli.options.filter.?);
40 try std.testing.expect(cli.options.no_recursive);
41 try std.testing.expect(cli.options.verbose);
42}
tests/fixtures/invalid-zig-test.jsonadded+5-0
Changes to tests/fixtures/invalid-zig-test.json
@@ -0,0 +1,5 @@
1{
2 "test": {
3 "unknown_option": true
4 }
5}
tests/fixtures/zig-test.jsonadded+8-0
Changes to tests/fixtures/zig-test.json
@@ -0,0 +1,8 @@
1{
2 "test": {
3 "test_dir": "tests/fixtures",
4 "pattern": "filter_fixture.zig",
5 "recursive": false,
6 "filter": "selected test passes"
7 }
8}