zig/test/src/Cases.zig
2025-12-04 03:46:35 +01:00

1026 lines
36 KiB
Zig

const Cases = @This();
const builtin = @import("builtin");
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const getExternalExecutor = std.zig.system.getExternalExecutor;
const ArrayList = std.ArrayList;
gpa: Allocator,
arena: Allocator,
cases: std.array_list.Managed(Case),
pub const IncrementalCase = struct {
base_path: []const u8,
};
pub const File = struct {
src: [:0]const u8,
path: []const u8,
};
pub const DepModule = struct {
name: []const u8,
path: []const u8,
};
pub const Backend = enum {
/// Test does not care which backend is used; compiler gets to pick the default.
auto,
selfhosted,
llvm,
};
pub const CFrontend = enum {
clang,
aro,
};
pub const Case = struct {
/// The name of the test case. This is shown if a test fails, and
/// otherwise ignored.
name: []const u8,
/// The platform the test targets. For non-native platforms, an emulator
/// such as QEMU is required for tests to complete.
target: std.Build.ResolvedTarget,
/// In order to be able to run e.g. Execution updates, this must be set
/// to Executable.
output_mode: std.builtin.OutputMode,
optimize_mode: std.builtin.OptimizeMode = .Debug,
files: std.array_list.Managed(File),
case: ?union(enum) {
/// Check that it compiles with no errors.
Compile: void,
/// Check the main binary output file against an expected set of bytes.
/// This is most useful with, for example, `-ofmt=c`.
CompareObjectFile: []const u8,
/// An error update attempts to compile bad code, and ensures that it
/// fails to compile, and for the expected reasons.
/// A slice containing the expected stderr template, which
/// gets some values substituted.
Error: []const []const u8,
/// An execution update compiles and runs the input, testing the
/// stdout against the expected results
/// This is a slice containing the expected message.
Execution: []const u8,
/// A header update compiles the input with the equivalent of
/// `-femit-h` and tests the produced header against the
/// expected result.
Header: []const u8,
},
emit_asm: bool = false,
emit_bin: bool = true,
emit_h: bool = false,
is_test: bool = false,
expect_exact: bool = false,
backend: Backend = .auto,
link_libc: bool = false,
pic: ?bool = null,
pie: ?bool = null,
/// A list of imports to cache alongside the source file.
imports: []const []const u8 = &.{},
/// Where to look for imports relative to the `cases_dir_path` given to
/// `lower_to_build_steps`. If null, file imports will assert.
import_path: ?[]const u8 = null,
deps: std.array_list.Managed(DepModule),
pub fn addSourceFile(case: *Case, name: []const u8, src: [:0]const u8) void {
case.files.append(.{ .path = name, .src = src }) catch @panic("OOM");
}
pub fn addDepModule(case: *Case, name: []const u8, path: []const u8) void {
case.deps.append(.{
.name = name,
.path = path,
}) catch @panic("out of memory");
}
/// Adds a subcase in which the module is updated with `src`, compiled,
/// run, and the output is tested against `result`.
pub fn addCompareOutput(self: *Case, src: [:0]const u8, result: []const u8) void {
assert(self.case == null);
self.case = .{ .Execution = result };
self.addSourceFile("tmp.zig", src);
}
/// Adds a subcase in which the module is updated with `src`, which
/// should contain invalid input, and ensures that compilation fails
/// for the expected reasons, given in sequential order in `errors` in
/// the form `:line:column: error: message`.
pub fn addError(self: *Case, src: [:0]const u8, errors: []const []const u8) void {
assert(errors.len != 0);
assert(self.case == null);
self.case = .{ .Error = errors };
self.addSourceFile("tmp.zig", src);
}
/// Adds a subcase in which the module is updated with `src`, and
/// asserts that it compiles without issue
pub fn addCompile(self: *Case, src: [:0]const u8) void {
assert(self.case == null);
self.case = .Compile;
self.addSourceFile("tmp.zig", src);
}
};
pub fn addExe(
ctx: *Cases,
name: []const u8,
target: std.Build.ResolvedTarget,
) *Case {
ctx.cases.append(.{
.name = name,
.target = target,
.files = .init(ctx.arena),
.case = null,
.output_mode = .Exe,
.deps = std.array_list.Managed(DepModule).init(ctx.arena),
}) catch @panic("out of memory");
return &ctx.cases.items[ctx.cases.items.len - 1];
}
/// Adds a test case for Zig input, producing an executable
pub fn exe(ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget) *Case {
return ctx.addExe(name, target);
}
pub fn exeFromCompiledC(ctx: *Cases, name: []const u8, target_query: std.Target.Query, b: *std.Build) *Case {
var adjusted_query = target_query;
adjusted_query.ofmt = .c;
ctx.cases.append(.{
.name = name,
.target = b.resolveTargetQuery(adjusted_query),
.files = .init(ctx.arena),
.case = null,
.output_mode = .Exe,
.deps = std.array_list.Managed(DepModule).init(ctx.arena),
.link_libc = true,
}) catch @panic("out of memory");
return &ctx.cases.items[ctx.cases.items.len - 1];
}
pub fn addObjLlvm(ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget) *Case {
const can_emit_asm = switch (target.result.cpu.arch) {
.csky,
.xtensa,
=> false,
else => true,
};
const can_emit_bin = switch (target.result.cpu.arch) {
.arc,
.csky,
.nvptx,
.nvptx64,
.xcore,
.xtensa,
=> false,
else => true,
};
ctx.cases.append(.{
.name = name,
.target = target,
.files = .init(ctx.arena),
.case = null,
.output_mode = .Obj,
.deps = std.array_list.Managed(DepModule).init(ctx.arena),
.backend = .llvm,
.emit_bin = can_emit_bin,
.emit_asm = can_emit_asm,
}) catch @panic("out of memory");
return &ctx.cases.items[ctx.cases.items.len - 1];
}
pub fn addObj(
ctx: *Cases,
name: []const u8,
target: std.Build.ResolvedTarget,
) *Case {
ctx.cases.append(.{
.name = name,
.target = target,
.files = .init(ctx.arena),
.case = null,
.output_mode = .Obj,
.deps = std.array_list.Managed(DepModule).init(ctx.arena),
}) catch @panic("out of memory");
return &ctx.cases.items[ctx.cases.items.len - 1];
}
pub fn addTest(
ctx: *Cases,
name: []const u8,
target: std.Build.ResolvedTarget,
) *Case {
ctx.cases.append(.{
.name = name,
.target = target,
.files = .init(ctx.arena),
.case = null,
.output_mode = .Exe,
.is_test = true,
.deps = std.array_list.Managed(DepModule).init(ctx.arena),
}) catch @panic("out of memory");
return &ctx.cases.items[ctx.cases.items.len - 1];
}
/// Adds a test case for Zig input, producing an object file.
pub fn obj(ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget) *Case {
return ctx.addObj(name, target);
}
/// Adds a test case for ZIR input, producing an object file.
pub fn objZIR(ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget) *Case {
return ctx.addObj(name, target, .ZIR);
}
/// Adds a test case for Zig or ZIR input, producing C code.
pub fn addC(ctx: *Cases, name: []const u8, target: std.Build.ResolvedTarget) *Case {
var target_adjusted = target;
target_adjusted.ofmt = std.Target.ObjectFormat.c;
ctx.cases.append(.{
.name = name,
.target = target_adjusted,
.files = .init(ctx.arena),
.case = null,
.output_mode = .Obj,
.deps = std.array_list.Managed(DepModule).init(ctx.arena),
}) catch @panic("out of memory");
return &ctx.cases.items[ctx.cases.items.len - 1];
}
pub fn addTransform(
ctx: *Cases,
name: []const u8,
target: std.Build.ResolvedTarget,
src: [:0]const u8,
result: [:0]const u8,
) void {
ctx.addObj(name, target).addTransform(src, result);
}
/// Adds a test case that compiles the Zig given in `src` to ZIR and tests
/// the ZIR against `result`
pub fn transform(
ctx: *Cases,
name: []const u8,
target: std.Build.ResolvedTarget,
src: [:0]const u8,
result: [:0]const u8,
) void {
ctx.addTransform(name, target, src, result);
}
pub fn addError(
ctx: *Cases,
name: []const u8,
target: std.Build.ResolvedTarget,
src: [:0]const u8,
expected_errors: []const []const u8,
) void {
ctx.addObj(name, target).addError(src, expected_errors);
}
/// Adds a test case that ensures that the Zig given in `src` fails to
/// compile for the expected reasons, given in sequential order in
/// `expected_errors` in the form `:line:column: error: message`.
pub fn compileError(
ctx: *Cases,
name: []const u8,
target: std.Build.ResolvedTarget,
src: [:0]const u8,
expected_errors: []const []const u8,
) void {
ctx.addError(name, target, src, expected_errors);
}
/// Adds a test case that asserts that the Zig given in `src` compiles
/// without any errors.
pub fn addCompile(
ctx: *Cases,
name: []const u8,
target: std.Build.ResolvedTarget,
src: [:0]const u8,
) void {
ctx.addObj(name, target).addCompile(src);
}
/// Adds a test for each file in the provided directory. 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 addFromDir(ctx: *Cases, dir: std.fs.Dir, b: *std.Build) void {
var current_file: []const u8 = "none";
ctx.addFromDirInner(dir, &current_file, b) catch |err| {
std.debug.panicExtra(
@returnAddress(),
"test harness failed to process file '{s}': {s}\n",
.{ current_file, @errorName(err) },
);
};
}
fn addFromDirInner(
ctx: *Cases,
iterable_dir: std.fs.Dir,
/// 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,
b: *std.Build,
) !void {
var it = try iterable_dir.walk(ctx.arena);
var filenames: ArrayList([]const u8) = .empty;
while (try it.next()) |entry| {
if (entry.kind != .file) continue;
// Ignore stuff such as .swp files
if (!knownFileExtension(entry.basename)) continue;
try filenames.append(ctx.arena, try ctx.arena.dupe(u8, entry.path));
}
for (filenames.items) |filename| {
current_file.* = filename;
const max_file_size = 10 * 1024 * 1024;
const src = try iterable_dir.readFileAllocOptions(filename, ctx.arena, .limited(max_file_size), .@"1", 0);
// Parse the manifest
var manifest = try TestManifest.parse(ctx.arena, src);
const backends = try manifest.getConfigForKeyAlloc(ctx.arena, "backend", Backend);
const targets = try manifest.getConfigForKeyAlloc(ctx.arena, "target", std.Target.Query);
const is_test = try manifest.getConfigForKeyAssertSingle("is_test", bool);
const link_libc = try manifest.getConfigForKeyAssertSingle("link_libc", bool);
const output_mode = try manifest.getConfigForKeyAssertSingle("output_mode", std.builtin.OutputMode);
const pic = try manifest.getConfigForKeyAssertSingle("pic", ?bool);
const pie = try manifest.getConfigForKeyAssertSingle("pie", ?bool);
const emit_asm = try manifest.getConfigForKeyAssertSingle("emit_asm", bool);
const emit_bin = try manifest.getConfigForKeyAssertSingle("emit_bin", bool);
const imports = try manifest.getConfigForKeyAlloc(ctx.arena, "imports", []const u8);
var cases = std.array_list.Managed(usize).init(ctx.arena);
// Cross-product to get all possible test combinations
for (targets) |target_query| {
const resolved_target = b.resolveTargetQuery(target_query);
const target = &resolved_target.result;
for (backends) |backend| {
if (backend == .selfhosted and target.cpu.arch == .wasm32) {
// https://github.com/ziglang/zig/issues/25684
continue;
}
if (backend == .selfhosted and
target.cpu.arch != .aarch64 and target.cpu.arch != .wasm32 and target.cpu.arch != .x86_64 and target.cpu.arch != .spirv64)
{
// Other backends don't support new liveness format
continue;
}
if (backend == .selfhosted and target.os.tag == .macos and
target.cpu.arch == .x86_64 and builtin.cpu.arch == .aarch64)
{
// Rosetta has issues with ZLD
continue;
}
const next = ctx.cases.items.len;
try ctx.cases.append(.{
.name = try caseNameFromPath(ctx.arena, filename),
.import_path = std.fs.path.dirname(filename),
.backend = backend,
.files = .init(ctx.arena),
.case = null,
.emit_asm = emit_asm,
.emit_bin = emit_bin,
.is_test = is_test,
.output_mode = output_mode,
.link_libc = link_libc,
.pic = pic,
.pie = pie,
.deps = std.array_list.Managed(DepModule).init(ctx.cases.allocator),
.imports = imports,
.target = resolved_target,
});
try cases.append(next);
}
}
for (cases.items) |case_index| {
const case = &ctx.cases.items[case_index];
switch (manifest.type) {
.compile => {
case.addCompile(src);
},
.@"error" => {
const errors = try manifest.trailingLines(ctx.arena);
case.addError(src, errors);
},
.run => {
const output = try manifest.trailingSplit(ctx.arena);
case.addCompareOutput(src, output);
},
.translate_c => @panic("c_frontend specified for compile case"),
.run_translated_c => @panic("c_frontend specified for compile case"),
.cli => @panic("TODO cli tests"),
}
}
}
}
pub fn init(gpa: Allocator, arena: Allocator) Cases {
return .{
.gpa = gpa,
.cases = .init(gpa),
.arena = arena,
};
}
pub const CaseTestOptions = struct {
test_filters: []const []const u8,
test_target_filters: []const []const u8,
skip_compile_errors: bool,
skip_non_native: bool,
skip_spirv: bool,
skip_wasm: bool,
skip_freebsd: bool,
skip_netbsd: bool,
skip_windows: bool,
skip_darwin: bool,
skip_linux: bool,
skip_llvm: bool,
skip_libc: bool,
};
pub fn lowerToBuildSteps(
self: *Cases,
b: *std.Build,
parent_step: *std.Build.Step,
options: CaseTestOptions,
) void {
const host = b.resolveTargetQuery(.{});
const cases_dir_path = b.build_root.join(b.allocator, &.{ "test", "cases" }) catch @panic("OOM");
for (self.cases.items) |case| {
for (options.test_filters) |test_filter| {
if (std.mem.indexOf(u8, case.name, test_filter)) |_| break;
} else if (options.test_filters.len > 0) continue;
if (case.case.? == .Error and options.skip_compile_errors) continue;
if (options.skip_non_native and !case.target.query.isNative())
continue;
if (options.skip_spirv and case.target.query.cpu_arch != null and case.target.query.cpu_arch.?.isSpirV()) continue;
if (options.skip_wasm and case.target.query.cpu_arch != null and case.target.query.cpu_arch.?.isWasm()) continue;
if (options.skip_freebsd and case.target.query.os_tag == .freebsd) continue;
if (options.skip_netbsd and case.target.query.os_tag == .netbsd) continue;
if (options.skip_windows and case.target.query.os_tag == .windows) continue;
if (options.skip_darwin and case.target.query.os_tag != null and case.target.query.os_tag.?.isDarwin()) continue;
if (options.skip_linux and case.target.query.os_tag == .linux) continue;
const would_use_llvm = @import("../tests.zig").wouldUseLlvm(
switch (case.backend) {
.auto => null,
.selfhosted => false,
.llvm => true,
},
case.target.query,
case.optimize_mode,
);
if (options.skip_llvm and would_use_llvm) continue;
const triple_txt = case.target.query.zigTriple(b.allocator) catch @panic("OOM");
if (options.test_target_filters.len > 0) {
for (options.test_target_filters) |filter| {
if (std.mem.indexOf(u8, triple_txt, filter) != null) break;
} else continue;
}
if (options.skip_libc and case.link_libc)
continue;
const writefiles = b.addWriteFiles();
var file_sources = std.StringHashMap(std.Build.LazyPath).init(b.allocator);
defer file_sources.deinit();
const first_file = case.files.items[0];
const root_source_file = writefiles.add(first_file.path, first_file.src);
file_sources.put(first_file.path, root_source_file) catch @panic("OOM");
for (case.files.items[1..]) |file| {
file_sources.put(file.path, writefiles.add(file.path, file.src)) catch @panic("OOM");
}
for (case.imports) |import_rel| {
const import_abs = std.fs.path.join(b.allocator, &.{
cases_dir_path,
case.import_path orelse @panic("import_path not set"),
import_rel,
}) catch @panic("OOM");
_ = writefiles.addCopyFile(.{ .cwd_relative = import_abs }, import_rel);
}
const mod = b.createModule(.{
.root_source_file = root_source_file,
.target = case.target,
.optimize = case.optimize_mode,
});
if (case.link_libc) mod.link_libc = true;
if (case.pic) |pic| mod.pic = pic;
for (case.deps.items) |dep| {
mod.addAnonymousImport(dep.name, .{
.root_source_file = file_sources.get(dep.path).?,
});
}
const artifact = if (case.is_test) b.addTest(.{
.name = case.name,
.root_module = mod,
}) else switch (case.output_mode) {
.Obj => b.addObject(.{
.name = case.name,
.root_module = mod,
}),
.Lib => b.addLibrary(.{
.linkage = .static,
.name = case.name,
.root_module = mod,
}),
.Exe => b.addExecutable(.{
.name = case.name,
.root_module = mod,
}),
};
if (case.pie) |pie| artifact.pie = pie;
switch (case.backend) {
.auto => {},
.selfhosted => {
artifact.use_llvm = false;
artifact.use_lld = false;
},
.llvm => {
artifact.use_llvm = true;
},
}
switch (case.case.?) {
.Compile => {
// Force the assembly/binary to be emitted if requested.
if (case.emit_asm) {
_ = artifact.getEmittedAsm();
}
if (case.emit_bin) {
_ = artifact.getEmittedBin();
}
parent_step.dependOn(&artifact.step);
},
.CompareObjectFile => |expected_output| {
const check = b.addCheckFile(artifact.getEmittedBin(), .{
.expected_exact = expected_output,
});
parent_step.dependOn(&check.step);
},
.Error => |expected_msgs| {
assert(expected_msgs.len != 0);
artifact.expect_errors = .{ .exact = expected_msgs };
parent_step.dependOn(&artifact.step);
},
.Execution => |expected_stdout| no_exec: {
const run = if (case.target.result.ofmt == .c) run_step: {
if (getExternalExecutor(&host.result, &case.target.result, .{ .link_libc = true }) != .native) {
// We wouldn't be able to run the compiled C code.
break :no_exec;
}
const run_c = b.addSystemCommand(&.{
b.graph.zig_exe,
"run",
"-cflags",
"-Ilib",
"-std=c99",
"-pedantic",
"-Werror",
"-Wno-dollar-in-identifier-extension",
"-Wno-incompatible-library-redeclaration", // https://github.com/ziglang/zig/issues/875
"-Wno-incompatible-pointer-types",
"-Wno-overlength-strings",
"--",
"-lc",
"-target",
triple_txt,
});
run_c.addArtifactArg(artifact);
break :run_step run_c;
} else b.addRunArtifact(artifact);
run.skip_foreign_checks = true;
if (!case.is_test) {
run.expectStdOutEqual(expected_stdout);
}
parent_step.dependOn(&run.step);
},
.Header => @panic("TODO"),
}
}
}
/// Default config values for known test manifest key-value pairings.
/// Currently handled defaults are:
/// * backend
/// * target
/// * output_mode
/// * is_test
const TestManifestConfigDefaults = struct {
/// Asserts if the key doesn't exist - yep, it's an oversight alright.
fn get(@"type": TestManifest.Type, key: []const u8) []const u8 {
if (std.mem.eql(u8, key, "backend")) {
return "auto";
} else if (std.mem.eql(u8, key, "target")) {
if (@"type" == .@"error" or @"type" == .translate_c or @"type" == .run_translated_c) {
return "native";
}
return comptime blk: {
var defaults: []const u8 = "";
// TODO should we only return "mainstream" targets by default here?
// TODO we should also specify ABIs explicitly as the backends are
// getting more and more complete
// Linux
for (&[_][]const u8{ "x86_64", "arm", "aarch64" }) |arch| {
defaults = defaults ++ arch ++ "-linux" ++ ",";
}
// macOS
for (&[_][]const u8{ "x86_64", "aarch64" }) |arch| {
defaults = defaults ++ arch ++ "-macos" ++ ",";
}
// Windows
defaults = defaults ++ "x86_64-windows" ++ ",";
// Wasm
defaults = defaults ++ "wasm32-wasi";
break :blk defaults;
};
} else if (std.mem.eql(u8, key, "output_mode")) {
return switch (@"type") {
.@"error" => "Obj",
.run => "Exe",
.compile => "Obj",
.translate_c => "Obj",
.run_translated_c => "Obj",
.cli => @panic("TODO test harness for CLI tests"),
};
} else if (std.mem.eql(u8, key, "emit_asm")) {
return "false";
} else if (std.mem.eql(u8, key, "emit_bin")) {
return "true";
} else if (std.mem.eql(u8, key, "is_test")) {
return "false";
} else if (std.mem.eql(u8, key, "link_libc")) {
return "false";
} else if (std.mem.eql(u8, key, "c_frontend")) {
return "clang";
} else if (std.mem.eql(u8, key, "pic")) {
return "null";
} else if (std.mem.eql(u8, key, "pie")) {
return "null";
} else if (std.mem.eql(u8, key, "imports")) {
return "";
} else unreachable;
}
};
/// Manifest syntax example:
/// (see https://github.com/ziglang/zig/issues/11288)
///
/// error
/// backend=selfhosted,llvm
/// output_mode=exe
///
/// :3:19: error: foo
///
/// run
/// target=x86_64-linux,aarch64-macos
///
/// I am expected stdout! Hello!
///
/// cli
///
/// build test
const TestManifest = struct {
type: Type,
config_map: std.StringHashMap([]const u8),
trailing_bytes: []const u8 = "",
const valid_keys = std.StaticStringMap(void).initComptime(.{
.{ "emit_asm", {} },
.{ "emit_bin", {} },
.{ "is_test", {} },
.{ "output_mode", {} },
.{ "target", {} },
.{ "c_frontend", {} },
.{ "link_libc", {} },
.{ "backend", {} },
.{ "pic", {} },
.{ "pie", {} },
.{ "imports", {} },
});
const Type = enum {
@"error",
run,
cli,
compile,
translate_c,
run_translated_c,
};
const TrailingIterator = struct {
inner: std.mem.TokenIterator(u8, .any),
fn next(self: *TrailingIterator) ?[]const u8 {
const next_inner = self.inner.next() orelse return null;
return if (next_inner.len == 2) "" else std.mem.trimEnd(u8, next_inner[3..], " \t");
}
};
fn ConfigValueIterator(comptime T: type) type {
return struct {
inner: std.mem.TokenIterator(u8, .scalar),
fn next(self: *@This()) !?T {
const next_raw = self.inner.next() orelse return null;
const parseFn = getDefaultParser(T);
return try parseFn(next_raw);
}
};
}
fn parse(arena: Allocator, bytes: []const u8) !TestManifest {
// The manifest is the last contiguous block of comments in the file
// We scan for the beginning by searching backward for the first non-empty line that does not start with "//"
var start: ?usize = null;
var end: usize = bytes.len;
if (bytes.len > 0) {
var cursor: usize = bytes.len - 1;
while (true) {
// Move to beginning of line
while (cursor > 0 and bytes[cursor - 1] != '\n') cursor -= 1;
if (std.mem.startsWith(u8, bytes[cursor..], "//")) {
start = cursor; // Contiguous comment line, include in manifest
} else {
if (start != null) break; // Encountered non-comment line, end of manifest
// We ignore all-whitespace lines following the comment block, but anything else
// means that there is no manifest present.
if (std.mem.trim(u8, bytes[cursor..end], " \r\n\t").len == 0) {
end = cursor;
} else break; // If it's not whitespace, there is no manifest
}
// Move to previous line
if (cursor != 0) cursor -= 1 else break;
}
}
const actual_start = start orelse return error.MissingTestManifest;
const manifest_bytes = bytes[actual_start..end];
var it = std.mem.tokenizeAny(u8, manifest_bytes, "\r\n");
// First line is the test type
const tt: Type = blk: {
const line = it.next() orelse return error.MissingTestCaseType;
const raw = std.mem.trim(u8, line[2..], " \t");
if (std.mem.eql(u8, raw, "error")) {
break :blk .@"error";
} else if (std.mem.eql(u8, raw, "run")) {
break :blk .run;
} else if (std.mem.eql(u8, raw, "cli")) {
break :blk .cli;
} else if (std.mem.eql(u8, raw, "compile")) {
break :blk .compile;
} else if (std.mem.eql(u8, raw, "translate-c")) {
break :blk .translate_c;
} else if (std.mem.eql(u8, raw, "run-translated-c")) {
break :blk .run_translated_c;
} else {
std.log.warn("unknown test case type requested: {s}", .{raw});
return error.UnknownTestCaseType;
}
};
var manifest: TestManifest = .{
.type = tt,
.config_map = std.StringHashMap([]const u8).init(arena),
};
// Any subsequent line until a blank comment line is key=value(s) pair
while (it.next()) |line| {
const trimmed = std.mem.trim(u8, line[2..], " \t");
if (trimmed.len == 0) break;
// Parse key=value(s)
var kv_it = std.mem.splitScalar(u8, trimmed, '=');
const key = kv_it.first();
if (!valid_keys.has(key)) {
return error.InvalidKey;
}
try manifest.config_map.putNoClobber(key, kv_it.next() orelse return error.MissingValuesForConfig);
}
// Finally, trailing is expected output
manifest.trailing_bytes = manifest_bytes[it.index..];
return manifest;
}
fn getConfigForKey(
self: TestManifest,
key: []const u8,
comptime T: type,
) ConfigValueIterator(T) {
const bytes = self.config_map.get(key) orelse TestManifestConfigDefaults.get(self.type, key);
return ConfigValueIterator(T){
.inner = std.mem.tokenizeScalar(u8, bytes, ','),
};
}
fn getConfigForKeyAlloc(
self: TestManifest,
allocator: Allocator,
key: []const u8,
comptime T: type,
) ![]const T {
var out = std.array_list.Managed(T).init(allocator);
defer out.deinit();
var it = self.getConfigForKey(key, T);
while (try it.next()) |item| {
try out.append(item);
}
return try out.toOwnedSlice();
}
fn getConfigForKeyAssertSingle(self: TestManifest, key: []const u8, comptime T: type) !T {
var it = self.getConfigForKey(key, T);
const res = (try it.next()) orelse unreachable;
assert((try it.next()) == null);
return res;
}
fn trailing(self: TestManifest) TrailingIterator {
return .{
.inner = std.mem.tokenizeAny(u8, self.trailing_bytes, "\r\n"),
};
}
fn trailingSplit(self: TestManifest, allocator: Allocator) error{OutOfMemory}![]const u8 {
var out = std.array_list.Managed(u8).init(allocator);
defer out.deinit();
var trailing_it = self.trailing();
while (trailing_it.next()) |line| {
try out.appendSlice(line);
try out.append('\n');
}
if (out.items.len > 0) {
try out.resize(out.items.len - 1);
}
return try out.toOwnedSlice();
}
fn trailingLines(self: TestManifest, allocator: Allocator) error{OutOfMemory}![]const []const u8 {
var out = std.array_list.Managed([]const u8).init(allocator);
defer out.deinit();
var it = self.trailing();
while (it.next()) |line| {
try out.append(line);
}
return try out.toOwnedSlice();
}
fn trailingLinesSplit(self: TestManifest, allocator: Allocator) error{OutOfMemory}![]const []const u8 {
// Collect output lines split by empty lines
var out = std.array_list.Managed([]const u8).init(allocator);
defer out.deinit();
var buf = std.array_list.Managed(u8).init(allocator);
defer buf.deinit();
var it = self.trailing();
while (it.next()) |line| {
if (line.len == 0) {
if (buf.items.len != 0) {
try out.append(try buf.toOwnedSlice());
buf.items.len = 0;
}
continue;
}
try buf.appendSlice(line);
try buf.append('\n');
}
try out.append(try buf.toOwnedSlice());
return try out.toOwnedSlice();
}
fn ParseFn(comptime T: type) type {
return fn ([]const u8) anyerror!T;
}
fn getDefaultParser(comptime T: type) ParseFn(T) {
if (T == std.Target.Query) return struct {
fn parse(str: []const u8) anyerror!T {
return std.Target.Query.parse(.{ .arch_os_abi = str });
}
}.parse;
switch (@typeInfo(T)) {
.int => return struct {
fn parse(str: []const u8) anyerror!T {
return try std.fmt.parseInt(T, str, 0);
}
}.parse,
.bool => return struct {
fn parse(str: []const u8) anyerror!T {
if (std.mem.eql(u8, str, "true")) return true;
if (std.mem.eql(u8, str, "false")) return false;
std.debug.print("{s}\n", .{str});
return error.InvalidBool;
}
}.parse,
.@"enum" => return struct {
fn parse(str: []const u8) anyerror!T {
return std.meta.stringToEnum(T, str) orelse {
std.log.err("unknown enum variant for {s}: {s}", .{ @typeName(T), str });
return error.UnknownEnumVariant;
};
}
}.parse,
.optional => |o| return struct {
fn parse(str: []const u8) anyerror!T {
if (std.mem.eql(u8, str, "null")) return null;
return try getDefaultParser(o.child)(str);
}
}.parse,
.@"struct" => @compileError("no default parser for " ++ @typeName(T)),
.pointer => {
if (T == []const u8) {
return struct {
fn parse(str: []const u8) anyerror!T {
return str;
}
}.parse;
} else {
@compileError("no default parser for " ++ @typeName(T));
}
},
else => @compileError("no default parser for " ++ @typeName(T)),
}
}
};
fn knownFileExtension(filename: []const u8) bool {
// List taken from `Compilation.classifyFileExt` in the compiler.
for ([_][]const u8{
".c", ".C", ".cc", ".cpp",
".cxx", ".stub", ".m", ".mm",
".ll", ".bc", ".s", ".S",
".h", ".zig", ".so", ".dll",
".dylib", ".tbd", ".a", ".lib",
".o", ".obj", ".cu", ".def",
".rc", ".res", ".manifest",
}) |ext| {
if (std.mem.endsWith(u8, filename, ext)) return true;
}
// Final check for .so.X, .so.X.Y, .so.X.Y.Z.
// From `Compilation.hasSharedLibraryExt`.
var it = std.mem.splitScalar(u8, filename, '.');
_ = it.first();
var so_txt = it.next() orelse return false;
while (!std.mem.eql(u8, so_txt, "so")) {
so_txt = it.next() orelse return false;
}
const n1 = it.next() orelse return false;
const n2 = it.next();
const n3 = it.next();
_ = std.fmt.parseInt(u32, n1, 10) catch return false;
if (n2) |x| _ = std.fmt.parseInt(u32, x, 10) catch return false;
if (n3) |x| _ = std.fmt.parseInt(u32, x, 10) catch return false;
if (it.next() != null) return false;
return false;
}
/// `path` is a path relative to the root case directory.
/// e.g. `compile_errors/undeclared_identifier.zig`
/// The case name is computed by removing the extension and substituting path separators for dots.
/// e.g. `compile_errors.undeclared_identifier`
/// Including the directory components makes `-Dtest-filter` more useful, because you can filter
/// based on subdirectory; e.g. `-Dtest-filter=compile_errors` to run the compile error tets.
fn caseNameFromPath(arena: Allocator, path: []const u8) Allocator.Error![]const u8 {
const ext_len = std.fs.path.extension(path).len;
const path_sans_ext = path[0 .. path.len - ext_len];
const result = try arena.dupe(u8, path_sans_ext);
std.mem.replaceScalar(u8, result, std.fs.path.sep, '.');
return result;
}