aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAndrew Kelley <andrew@ziglang.org>2022-05-04 20:28:34 -0400
committerGitHub <noreply@github.com>2022-05-04 20:28:34 -0400
commit080e870a717124204cbfa58a1f29061a701b0362 (patch)
tree4d940b2b245e885448d8072f61fbc88074f57d1f /src
parenta839ccc15322e63b13083da8f44959113a5e3313 (diff)
parent3624e1ef485c3575cb0c57a312386f49daf7ad37 (diff)
downloadzig-080e870a717124204cbfa58a1f29061a701b0362.tar.gz
zig-080e870a717124204cbfa58a1f29061a701b0362.zip
Merge pull request #11572 from ziglang/test-harness
test: improve test/batch sequence iterator and move all tests into cases/ parent dir
Diffstat (limited to 'src')
-rw-r--r--src/test.zig383
1 files changed, 205 insertions, 178 deletions
diff --git a/src/test.zig b/src/test.zig
index bdc02ff2e0..c5dda0757f 100644
--- a/src/test.zig
+++ b/src/test.zig
@@ -36,26 +36,13 @@ test {
{
const dir_path = try std.fs.path.join(arena, &.{
- std.fs.path.dirname(@src().file).?, "..", "test", "compile_errors",
+ std.fs.path.dirname(@src().file).?, "..", "test", "cases",
});
var dir = try std.fs.cwd().openDir(dir_path, .{ .iterate = true });
defer dir.close();
- // TODO make this incremental once the bug is solved that it triggers
- // See: https://github.com/ziglang/zig/issues/11344
- ctx.addTestCasesFromDir(dir, .independent);
- }
-
- {
- const dir_path = try std.fs.path.join(arena, &.{
- std.fs.path.dirname(@src().file).?, "..", "test", "incremental",
- });
-
- var dir = try std.fs.cwd().openDir(dir_path, .{ .iterate = true });
- defer dir.close();
-
- ctx.addTestCasesFromDir(dir, .incremental);
+ ctx.addTestCasesFromDir(dir);
}
try @import("test_cases").addCases(&ctx);
@@ -395,6 +382,134 @@ const TestManifest = struct {
}
};
+const TestStrategy = enum {
+ /// Execute tests as independent compilations, unless they are explicitly
+ /// incremental ("foo.0.zig", "foo.1.zig", etc.)
+ independent,
+ /// Execute all tests as incremental updates to a single compilation. Explicitly
+ /// incremental tests ("foo.0.zig", "foo.1.zig", etc.) still execute in order
+ incremental,
+};
+
+/// Iterates a set of filenames extracting batches that are either incremental
+/// ("foo.0.zig", "foo.1.zig", etc.) or independent ("foo.zig", "bar.zig", etc.).
+/// Assumes filenames are sorted.
+const TestIterator = struct {
+ start: usize = 0,
+ end: usize = 0,
+ filenames: []const []const u8,
+
+ const Error = error{InvalidIncrementalTestIndex};
+
+ fn next(it: *TestIterator) Error!?[]const []const u8 {
+ try it.nextInner();
+ if (it.start == it.end) return null;
+ return it.filenames[it.start..it.end];
+ }
+
+ fn nextInner(it: *TestIterator) Error!void {
+ it.start = it.end;
+ if (it.end == it.filenames.len) return;
+ if (it.end + 1 == it.filenames.len) {
+ it.end += 1;
+ return;
+ }
+
+ const remaining = it.filenames[it.end..];
+ var i: usize = 0;
+ while (i < remaining.len - 1) : (i += 1) {
+ // First, check if this file is part of an incremental update sequence
+ // Split filename into "<base_name>.<index>.<file_ext>"
+ const prev_parts = getTestFileNameParts(remaining[i]);
+ const new_parts = getTestFileNameParts(remaining[i + 1]);
+
+ // If base_name and file_ext match, these files are in the same test sequence
+ // and the new one should be the incremented version of the previous test
+ if (std.mem.eql(u8, prev_parts.base_name, new_parts.base_name) and
+ std.mem.eql(u8, prev_parts.file_ext, new_parts.file_ext))
+ {
+ // This is "foo.X.zig" followed by "foo.Y.zig". Make sure that X = Y + 1
+ if (prev_parts.test_index == null)
+ return error.InvalidIncrementalTestIndex;
+ if (new_parts.test_index == null)
+ return error.InvalidIncrementalTestIndex;
+ if (new_parts.test_index.? != prev_parts.test_index.? + 1)
+ return error.InvalidIncrementalTestIndex;
+ } else {
+ // This is not the same test sequence, so the new file must be the first file
+ // in a new sequence ("*.0.zig") or an independent test file ("*.zig")
+ if (new_parts.test_index != null and new_parts.test_index.? != 0)
+ return error.InvalidIncrementalTestIndex;
+
+ it.end += i + 1;
+ break;
+ }
+ } else {
+ it.end += remaining.len;
+ }
+ }
+};
+
+/// For a filename in the format "<filename>.X.<ext>" or "<filename>.<ext>", returns
+/// "<filename>", "<ext>" and X parsed as a decimal number. If X is not present, or
+/// cannot be parsed as a decimal number, it is treated as part of <filename>
+fn getTestFileNameParts(name: []const u8) struct {
+ base_name: []const u8,
+ file_ext: []const u8,
+ test_index: ?usize,
+} {
+ const file_ext = std.fs.path.extension(name);
+ const trimmed = name[0 .. name.len - file_ext.len]; // Trim off ".<ext>"
+ const maybe_index = std.fs.path.extension(trimmed); // Extract ".X"
+
+ // Attempt to parse index
+ const index: ?usize = if (maybe_index.len > 0)
+ std.fmt.parseInt(usize, maybe_index[1..], 10) catch null
+ else
+ null;
+
+ // Adjust "<filename>" extent based on parsing success
+ const base_name_end = trimmed.len - if (index != null) maybe_index.len else 0;
+ return .{
+ .base_name = name[0..base_name_end],
+ .file_ext = if (file_ext.len > 0) file_ext[1..] else file_ext,
+ .test_index = index,
+ };
+}
+
+/// Sort test filenames in-place, so that incremental test cases ("foo.0.zig",
+/// "foo.1.zig", etc.) are contiguous and appear in numerical order.
+fn sortTestFilenames(
+ filenames: [][]const u8,
+) void {
+ const Context = struct {
+ pub fn lessThan(_: @This(), a: []const u8, b: []const u8) bool {
+ const a_parts = getTestFileNameParts(a);
+ const b_parts = getTestFileNameParts(b);
+
+ // Sort "<base_name>.X.<file_ext>" based on "<base_name>" and "<file_ext>" first
+ return switch (std.mem.order(u8, a_parts.base_name, b_parts.base_name)) {
+ .lt => true,
+ .gt => false,
+ .eq => switch (std.mem.order(u8, a_parts.file_ext, b_parts.file_ext)) {
+ .lt => true,
+ .gt => false,
+ .eq => b: { // a and b differ only in their ".X" part
+
+ // Sort "<base_name>.<file_ext>" before any "<base_name>.X.<file_ext>"
+ if (a_parts.test_index == null) break :b true;
+ if (b_parts.test_index == null) break :b false;
+
+ // Make sure that incremental tests appear in linear order
+ return a_parts.test_index.? < b_parts.test_index.?;
+ },
+ },
+ };
+ }
+ };
+ std.sort.sort([]const u8, filenames, Context{}, Context.lessThan);
+}
+
pub const TestContext = struct {
arena: Allocator,
cases: std.ArrayList(Case),
@@ -895,100 +1010,29 @@ pub const TestContext = struct {
case.compiles(fixed_src);
}
- const Strategy = enum {
- /// Execute tests as independent compilations, unless they are explicitly
- /// incremental ("foo.0.zig", "foo.1.zig", etc.)
- independent,
- /// Execute all tests as incremental updates to a single compilation. Explicitly
- /// incremental tests ("foo.0.zig", "foo.1.zig", etc.) still execute in order
- incremental,
- };
-
- /// Adds a test for each file in the provided directory, using the selected strategy.
+ /// Adds a test for each file in the provided directory.
+ /// Testing strategy (TestStrategy) is inferred automatically from filenames.
/// Recurses nested directories.
///
/// Each file should include a test manifest as a contiguous block of comments at
/// the end of the file. The first line should be the test type, followed by a set of
/// key-value config values, followed by a blank line, then the expected output.
- pub fn addTestCasesFromDir(ctx: *TestContext, dir: std.fs.Dir, strategy: Strategy) void {
+ pub fn addTestCasesFromDir(ctx: *TestContext, dir: std.fs.Dir) void {
var current_file: []const u8 = "none";
- ctx.addTestCasesFromDirInner(dir, strategy, &current_file) catch |err| {
+ ctx.addTestCasesFromDirInner(dir, &current_file) catch |err| {
std.debug.panic("test harness failed to process file '{s}': {s}\n", .{
current_file, @errorName(err),
});
};
}
- /// For a filename in the format "<filename>.X.<ext>" or "<filename>.<ext>", returns
- /// "<filename>", "<ext>" and X parsed as a decimal number. If X is not present, or
- /// cannot be parsed as a decimal number, it is treated as part of <filename>
- fn getTestFileNameParts(name: []const u8) struct {
- base_name: []const u8,
- file_ext: []const u8,
- test_index: ?usize,
- } {
- const file_ext = std.fs.path.extension(name);
- const trimmed = name[0 .. name.len - file_ext.len]; // Trim off ".<ext>"
- const maybe_index = std.fs.path.extension(trimmed); // Extract ".X"
-
- // Attempt to parse index
- const index: ?usize = if (maybe_index.len > 0)
- std.fmt.parseInt(usize, maybe_index[1..], 10) catch null
- else
- null;
-
- // Adjust "<filename>" extent based on parsing success
- const base_name_end = trimmed.len - if (index != null) maybe_index.len else 0;
- return .{
- .base_name = name[0..base_name_end],
- .file_ext = if (file_ext.len > 0) file_ext[1..] else file_ext,
- .test_index = index,
- };
- }
-
- /// Sort test filenames in-place, so that incremental test cases ("foo.0.zig",
- /// "foo.1.zig", etc.) are contiguous and appear in numerical order.
- fn sortTestFilenames(
- filenames: [][]const u8,
- ) void {
- const Context = struct {
- pub fn lessThan(_: @This(), a: []const u8, b: []const u8) bool {
- const a_parts = getTestFileNameParts(a);
- const b_parts = getTestFileNameParts(b);
-
- // Sort "<base_name>.X.<file_ext>" based on "<base_name>" and "<file_ext>" first
- return switch (std.mem.order(u8, a_parts.base_name, b_parts.base_name)) {
- .lt => true,
- .gt => false,
- .eq => switch (std.mem.order(u8, a_parts.file_ext, b_parts.file_ext)) {
- .lt => true,
- .gt => false,
- .eq => b: { // a and b differ only in their ".X" part
-
- // Sort "<base_name>.<file_ext>" before any "<base_name>.X.<file_ext>"
- if (a_parts.test_index == null) break :b true;
- if (b_parts.test_index == null) break :b false;
-
- // Make sure that incremental tests appear in linear order
- return a_parts.test_index.? < b_parts.test_index.?;
- },
- },
- };
- }
- };
- std.sort.sort([]const u8, filenames, Context{}, Context.lessThan);
- }
-
fn addTestCasesFromDirInner(
ctx: *TestContext,
dir: std.fs.Dir,
- strategy: Strategy,
/// This is kept up to date with the currently being processed file so
/// that if any errors occur the caller knows it happened during this file.
current_file: *[]const u8,
) !void {
- var cases = std.ArrayList(usize).init(ctx.arena);
-
var it = try dir.walk(ctx.arena);
var filenames = std.ArrayList([]const u8).init(ctx.arena);
@@ -1006,104 +1050,87 @@ pub const TestContext = struct {
// Sort filenames, so that incremental tests are contiguous and in-order
sortTestFilenames(filenames.items);
- var prev_filename: []const u8 = "";
- for (filenames.items) |filename| {
- current_file.* = filename;
+ var test_it = TestIterator{ .filenames = filenames.items };
+ while (try test_it.next()) |batch| {
+ const strategy: TestStrategy = if (batch.len > 1) .incremental else .independent;
+ var cases = std.ArrayList(usize).init(ctx.arena);
- // First, check if this file is part of an incremental update sequence
- // Split filename into "<base_name>.<index>.<file_ext>"
- const prev_parts = getTestFileNameParts(prev_filename);
- const new_parts = getTestFileNameParts(filename);
+ for (batch) |filename| {
+ current_file.* = filename;
- // If base_name and file_ext match, these files are in the same test sequence
- // and the new one should be the incremented version of the previous test
- if (std.mem.eql(u8, prev_parts.base_name, new_parts.base_name) and
- std.mem.eql(u8, prev_parts.file_ext, new_parts.file_ext))
- {
- // This is "foo.X.zig" followed by "foo.Y.zig". Make sure that X = Y + 1
- if (prev_parts.test_index == null) return error.InvalidIncrementalTestIndex;
- if (new_parts.test_index == null) return error.InvalidIncrementalTestIndex;
- if (new_parts.test_index.? != prev_parts.test_index.? + 1) return error.InvalidIncrementalTestIndex;
- } else {
- // This is not the same test sequence, so the new file must be the first file
- // in a new sequence ("*.0.zig") or an independent test file ("*.zig")
- if (new_parts.test_index != null and new_parts.test_index.? != 0) return error.InvalidIncrementalTestIndex;
- cases.clearRetainingCapacity();
- }
- prev_filename = filename;
+ const max_file_size = 10 * 1024 * 1024;
+ const src = try dir.readFileAllocOptions(ctx.arena, filename, max_file_size, null, 1, 0);
- const max_file_size = 10 * 1024 * 1024;
- const src = try dir.readFileAllocOptions(ctx.arena, filename, max_file_size, null, 1, 0);
+ // Parse the manifest
+ var manifest = try TestManifest.parse(ctx.arena, src);
- // Parse the manifest
- var manifest = try TestManifest.parse(ctx.arena, src);
+ if (cases.items.len == 0) {
+ const backends = try manifest.getConfigForKeyAlloc(ctx.arena, "backend", Backend);
+ const targets = try manifest.getConfigForKeyAlloc(ctx.arena, "target", CrossTarget);
+ const is_test = manifest.getConfigForKeyAssertSingle("is_test", bool);
+ const output_mode = manifest.getConfigForKeyAssertSingle("output_mode", std.builtin.OutputMode);
- if (cases.items.len == 0) {
- const backends = try manifest.getConfigForKeyAlloc(ctx.arena, "backend", Backend);
- const targets = try manifest.getConfigForKeyAlloc(ctx.arena, "target", CrossTarget);
- const is_test = manifest.getConfigForKeyAssertSingle("is_test", bool);
- const output_mode = manifest.getConfigForKeyAssertSingle("output_mode", std.builtin.OutputMode);
+ const name_prefix = blk: {
+ const ext_index = std.mem.lastIndexOfScalar(u8, current_file.*, '.') orelse
+ return error.InvalidFilename;
+ const index = std.mem.lastIndexOfScalar(u8, current_file.*[0..ext_index], '.') orelse ext_index;
+ break :blk current_file.*[0..index];
+ };
- const name_prefix = blk: {
- const ext_index = std.mem.lastIndexOfScalar(u8, current_file.*, '.') orelse
- return error.InvalidFilename;
- const index = std.mem.lastIndexOfScalar(u8, current_file.*[0..ext_index], '.') orelse ext_index;
- break :blk current_file.*[0..index];
- };
+ // Cross-product to get all possible test combinations
+ for (backends) |backend| {
+ if (backend == .stage1 and skip_stage1) continue;
- // Cross-product to get all possible test combinations
- for (backends) |backend| {
- if (backend == .stage1 and skip_stage1) continue;
-
- for (targets) |target| {
- const name = try std.fmt.allocPrint(ctx.arena, "{s} ({s}, {s})", .{
- name_prefix,
- @tagName(backend),
- try target.zigTriple(ctx.arena),
- });
- const next = ctx.cases.items.len;
- try ctx.cases.append(.{
- .name = name,
- .target = target,
- .backend = backend,
- .updates = std.ArrayList(TestContext.Update).init(ctx.cases.allocator),
- .is_test = is_test,
- .output_mode = output_mode,
- .link_libc = backend == .llvm,
- .files = std.ArrayList(TestContext.File).init(ctx.cases.allocator),
- });
- try cases.append(next);
+ for (targets) |target| {
+ const name = try std.fmt.allocPrint(ctx.arena, "{s} ({s}, {s})", .{
+ name_prefix,
+ @tagName(backend),
+ try target.zigTriple(ctx.arena),
+ });
+ const next = ctx.cases.items.len;
+ try ctx.cases.append(.{
+ .name = name,
+ .target = target,
+ .backend = backend,
+ .updates = std.ArrayList(TestContext.Update).init(ctx.cases.allocator),
+ .is_test = is_test,
+ .output_mode = output_mode,
+ .link_libc = backend == .llvm,
+ .files = std.ArrayList(TestContext.File).init(ctx.cases.allocator),
+ });
+ try cases.append(next);
+ }
}
}
- }
- for (cases.items) |case_index| {
- const case = &ctx.cases.items[case_index];
- switch (manifest.@"type") {
- .@"error" => {
- const errors = try manifest.trailingAlloc(ctx.arena);
- switch (strategy) {
- .independent => {
- case.addError(src, errors);
- },
- .incremental => {
- case.addErrorNamed("update", src, errors);
- },
- }
- },
- .run => {
- var output = std.ArrayList(u8).init(ctx.arena);
- var trailing_it = manifest.trailing();
- while (trailing_it.next()) |line| {
- try output.appendSlice(line);
- try output.append('\n');
- }
- if (output.items.len > 0) {
- try output.resize(output.items.len - 1);
- }
- case.addCompareOutput(src, output.toOwnedSlice());
- },
- .cli => @panic("TODO cli tests"),
+ for (cases.items) |case_index| {
+ const case = &ctx.cases.items[case_index];
+ switch (manifest.@"type") {
+ .@"error" => {
+ const errors = try manifest.trailingAlloc(ctx.arena);
+ switch (strategy) {
+ .independent => {
+ case.addError(src, errors);
+ },
+ .incremental => {
+ case.addErrorNamed("update", src, errors);
+ },
+ }
+ },
+ .run => {
+ var output = std.ArrayList(u8).init(ctx.arena);
+ var trailing_it = manifest.trailing();
+ while (trailing_it.next()) |line| {
+ try output.appendSlice(line);
+ try output.append('\n');
+ }
+ if (output.items.len > 0) {
+ try output.resize(output.items.len - 1);
+ }
+ case.addCompareOutput(src, output.toOwnedSlice());
+ },
+ .cli => @panic("TODO cli tests"),
+ }
}
}
}