From a690a5085ddbfb540cf07db146645a9f8a4e92f6 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Sun, 5 Jan 2020 02:01:28 -0500 Subject: [PATCH] rework and improve some of the zig build steps * `RunStep` moved to lib/std/build/run.zig and gains ability to compare output and exit code against expected values. Multiple redundant locations in the test harness code are replaced to use `RunStep`. * `WriteFileStep` moved to lib/std/build/write_file.zig and gains ability to write more than one file into the cache directory, for when the files need to be relative to each other. This makes usage of `WriteFileStep` no longer problematic when parallelizing zig build. * Added `CheckFileStep`, which can be used to validate that the output of another step produced a valid file. Multiple redundant locations in the test harness code are replaced to use `CheckFileStep`. * Added `TranslateCStep`. This exposes `zig translate-c` to the build system, which is likely to be rarely useful by most Zig users; however Zig's own test suite uses it both for translate-c tests and for run-translated-c tests. * Refactored ad-hoc code to handle source files coming from multiple kinds of sources, into `std.build.FileSource`. * Added `std.build.Builder.addExecutableFromWriteFileStep`. * Added `std.build.Builder.addExecutableSource`. * Added `std.build.Builder.addWriteFiles`. * Added `std.build.Builder.addTranslateC`. * Added `std.build.LibExeObjStep.addCSourceFileSource`. * Added `std.build.LibExeObjStep.addAssemblyFileFromWriteFileStep`. * Added `std.build.LibExeObjStep.addAssemblyFileSource`. * Exposed `std.fs.base64_encoder`. --- lib/std/build.zig | 324 ++++++++----------- lib/std/build/check_file.zig | 52 +++ lib/std/build/run.zig | 302 ++++++++++++++++++ lib/std/build/translate_c.zig | 73 +++++ lib/std/build/write_file.zig | 94 ++++++ lib/std/fs.zig | 13 +- test/src/compare_output.zig | 165 ++++++++++ test/src/run_translated_c.zig | 112 ++----- test/src/translate_c.zig | 109 +++++++ test/tests.zig | 576 +--------------------------------- 10 files changed, 955 insertions(+), 865 deletions(-) create mode 100644 lib/std/build/check_file.zig create mode 100644 lib/std/build/run.zig create mode 100644 lib/std/build/translate_c.zig create mode 100644 lib/std/build/write_file.zig create mode 100644 test/src/compare_output.zig create mode 100644 test/src/translate_c.zig diff --git a/lib/std/build.zig b/lib/std/build.zig index 05f33eafdd..6ba6a08af0 100644 --- a/lib/std/build.zig +++ b/lib/std/build.zig @@ -17,6 +17,10 @@ const fmt_lib = std.fmt; const File = std.fs.File; pub const FmtStep = @import("build/fmt.zig").FmtStep; +pub const TranslateCStep = @import("build/translate_c.zig").TranslateCStep; +pub const WriteFileStep = @import("build/write_file.zig").WriteFileStep; +pub const RunStep = @import("build/run.zig").RunStep; +pub const CheckFileStep = @import("build/check_file.zig").CheckFileStep; pub const Builder = struct { install_tls: TopLevelStep, @@ -203,23 +207,53 @@ pub const Builder = struct { } pub fn addExecutable(self: *Builder, name: []const u8, root_src: ?[]const u8) *LibExeObjStep { + return LibExeObjStep.createExecutable( + self, + name, + if (root_src) |p| FileSource{ .path = p } else null, + false, + ); + } + + pub fn addExecutableFromWriteFileStep( + self: *Builder, + name: []const u8, + wfs: *WriteFileStep, + basename: []const u8, + ) *LibExeObjStep { + return LibExeObjStep.createExecutable(self, name, @as(FileSource, .{ + .write_file = .{ + .step = wfs, + .basename = basename, + }, + }), false); + } + + pub fn addExecutableSource( + self: *Builder, + name: []const u8, + root_src: ?FileSource, + ) *LibExeObjStep { return LibExeObjStep.createExecutable(self, name, root_src, false); } pub fn addObject(self: *Builder, name: []const u8, root_src: ?[]const u8) *LibExeObjStep { - return LibExeObjStep.createObject(self, name, root_src); + const root_src_param = if (root_src) |p| @as(FileSource, .{ .path = p }) else null; + return LibExeObjStep.createObject(self, name, root_src_param); } pub fn addSharedLibrary(self: *Builder, name: []const u8, root_src: ?[]const u8, ver: Version) *LibExeObjStep { - return LibExeObjStep.createSharedLibrary(self, name, root_src, ver); + const root_src_param = if (root_src) |p| @as(FileSource, .{ .path = p }) else null; + return LibExeObjStep.createSharedLibrary(self, name, root_src_param, ver); } pub fn addStaticLibrary(self: *Builder, name: []const u8, root_src: ?[]const u8) *LibExeObjStep { - return LibExeObjStep.createStaticLibrary(self, name, root_src); + const root_src_param = if (root_src) |p| @as(FileSource, .{ .path = p }) else null; + return LibExeObjStep.createStaticLibrary(self, name, root_src_param); } pub fn addTest(self: *Builder, root_src: []const u8) *LibExeObjStep { - return LibExeObjStep.createTest(self, "test", root_src); + return LibExeObjStep.createTest(self, "test", .{ .path = root_src }); } pub fn addAssemble(self: *Builder, name: []const u8, src: []const u8) *LibExeObjStep { @@ -256,8 +290,14 @@ pub const Builder = struct { } pub fn addWriteFile(self: *Builder, file_path: []const u8, data: []const u8) *WriteFileStep { + const write_file_step = self.addWriteFiles(); + write_file_step.add(file_path, data); + return write_file_step; + } + + pub fn addWriteFiles(self: *Builder) *WriteFileStep { const write_file_step = self.allocator.create(WriteFileStep) catch unreachable; - write_file_step.* = WriteFileStep.init(self, file_path, data); + write_file_step.* = WriteFileStep.init(self); return write_file_step; } @@ -278,6 +318,10 @@ pub const Builder = struct { return FmtStep.create(self, paths); } + pub fn addTranslateC(self: *Builder, source: FileSource) *TranslateCStep { + return TranslateCStep.create(self, source); + } + pub fn version(self: *const Builder, major: u32, minor: u32, patch: u32) Version { return Version{ .major = major, @@ -1002,7 +1046,7 @@ const Pkg = struct { }; const CSourceFile = struct { - source_path: []const u8, + source: FileSource, args: []const []const u8, }; @@ -1015,6 +1059,33 @@ fn isLibCLibrary(name: []const u8) bool { return false; } +pub const FileSource = union(enum) { + /// Relative to build root + path: []const u8, + write_file: struct { + step: *WriteFileStep, + basename: []const u8, + }, + translate_c: *TranslateCStep, + + pub fn addStepDependencies(self: FileSource, step: *Step) void { + switch (self) { + .path => {}, + .write_file => |wf| step.dependOn(&wf.step.step), + .translate_c => |tc| step.dependOn(&tc.step), + } + } + + /// Should only be called during make() + pub fn getPath(self: FileSource, builder: *Builder) []const u8 { + return switch (self) { + .path => |p| builder.pathFromRoot(p), + .write_file => |wf| wf.step.getOutputPath(wf.basename), + .translate_c => |tc| tc.getOutputPath(), + }; + } +}; + pub const LibExeObjStep = struct { step: Step, builder: *Builder, @@ -1047,7 +1118,7 @@ pub const LibExeObjStep = struct { filter: ?[]const u8, single_threaded: bool, - root_src: ?[]const u8, + root_src: ?FileSource, out_h_filename: []const u8, out_lib_filename: []const u8, out_pdb_filename: []const u8, @@ -1099,7 +1170,7 @@ pub const LibExeObjStep = struct { StaticPath: []const u8, OtherStep: *LibExeObjStep, SystemLib: []const u8, - AssemblyFile: []const u8, + AssemblyFile: FileSource, CSourceFile: *CSourceFile, }; @@ -1116,37 +1187,44 @@ pub const LibExeObjStep = struct { Test, }; - pub fn createSharedLibrary(builder: *Builder, name: []const u8, root_src: ?[]const u8, ver: Version) *LibExeObjStep { + pub fn createSharedLibrary(builder: *Builder, name: []const u8, root_src: ?FileSource, ver: Version) *LibExeObjStep { const self = builder.allocator.create(LibExeObjStep) catch unreachable; self.* = initExtraArgs(builder, name, root_src, Kind.Lib, true, ver); return self; } - pub fn createStaticLibrary(builder: *Builder, name: []const u8, root_src: ?[]const u8) *LibExeObjStep { + pub fn createStaticLibrary(builder: *Builder, name: []const u8, root_src: ?FileSource) *LibExeObjStep { const self = builder.allocator.create(LibExeObjStep) catch unreachable; self.* = initExtraArgs(builder, name, root_src, Kind.Lib, false, builder.version(0, 0, 0)); return self; } - pub fn createObject(builder: *Builder, name: []const u8, root_src: ?[]const u8) *LibExeObjStep { + pub fn createObject(builder: *Builder, name: []const u8, root_src: ?FileSource) *LibExeObjStep { const self = builder.allocator.create(LibExeObjStep) catch unreachable; self.* = initExtraArgs(builder, name, root_src, Kind.Obj, false, builder.version(0, 0, 0)); return self; } - pub fn createExecutable(builder: *Builder, name: []const u8, root_src: ?[]const u8, is_dynamic: bool) *LibExeObjStep { + pub fn createExecutable(builder: *Builder, name: []const u8, root_src: ?FileSource, is_dynamic: bool) *LibExeObjStep { const self = builder.allocator.create(LibExeObjStep) catch unreachable; self.* = initExtraArgs(builder, name, root_src, Kind.Exe, is_dynamic, builder.version(0, 0, 0)); return self; } - pub fn createTest(builder: *Builder, name: []const u8, root_src: []const u8) *LibExeObjStep { + pub fn createTest(builder: *Builder, name: []const u8, root_src: FileSource) *LibExeObjStep { const self = builder.allocator.create(LibExeObjStep) catch unreachable; self.* = initExtraArgs(builder, name, root_src, Kind.Test, false, builder.version(0, 0, 0)); return self; } - fn initExtraArgs(builder: *Builder, name: []const u8, root_src: ?[]const u8, kind: Kind, is_dynamic: bool, ver: Version) LibExeObjStep { + fn initExtraArgs( + builder: *Builder, + name: []const u8, + root_src: ?FileSource, + kind: Kind, + is_dynamic: bool, + ver: Version, + ) LibExeObjStep { if (mem.indexOf(u8, name, "/") != null or mem.indexOf(u8, name, "\\") != null) { panic("invalid name: '{}'. It looks like a file path, but it is supposed to be the library or application name.", .{name}); } @@ -1196,6 +1274,7 @@ pub const LibExeObjStep = struct { .install_step = null, }; self.computeOutFileNames(); + if (root_src) |rs| rs.addStepDependencies(&self.step); return self; } @@ -1486,15 +1565,22 @@ pub const LibExeObjStep = struct { } pub fn addCSourceFile(self: *LibExeObjStep, file: []const u8, args: []const []const u8) void { + self.addCSourceFileSource(.{ + .args = args, + .source = .{ .path = file }, + }); + } + + pub fn addCSourceFileSource(self: *LibExeObjStep, source: CSourceFile) void { const c_source_file = self.builder.allocator.create(CSourceFile) catch unreachable; - const args_copy = self.builder.allocator.alloc([]u8, args.len) catch unreachable; - for (args) |arg, i| { + + const args_copy = self.builder.allocator.alloc([]u8, source.args.len) catch unreachable; + for (source.args) |arg, i| { args_copy[i] = self.builder.dupe(arg); } - c_source_file.* = CSourceFile{ - .source_path = self.builder.dupe(file), - .args = args_copy, - }; + + c_source_file.* = source; + c_source_file.args = args_copy; self.link_objects.append(LinkObject{ .CSourceFile = c_source_file }) catch unreachable; } @@ -1571,6 +1657,20 @@ pub const LibExeObjStep = struct { self.link_objects.append(LinkObject{ .AssemblyFile = self.builder.dupe(path) }) catch unreachable; } + pub fn addAssemblyFileFromWriteFileStep(self: *LibExeObjStep, wfs: *WriteFileStep, basename: []const u8) void { + self.addAssemblyFileSource(.{ + .write_file = .{ + .step = wfs, + .basename = self.builder.dupe(basename), + }, + }); + } + + pub fn addAssemblyFileSource(self: *LibExeObjStep, source: FileSource) void { + self.link_objects.append(LinkObject{ .AssemblyFile = source }) catch unreachable; + source.addStepDependencies(&self.step); + } + pub fn addObjectFile(self: *LibExeObjStep, path: []const u8) void { self.link_objects.append(LinkObject{ .StaticPath = self.builder.dupe(path) }) catch unreachable; } @@ -1698,25 +1798,23 @@ pub const LibExeObjStep = struct { }; zig_args.append(cmd) catch unreachable; - if (self.root_src) |root_src| { - zig_args.append(builder.pathFromRoot(root_src)) catch unreachable; - } + if (self.root_src) |root_src| try zig_args.append(root_src.getPath(builder)); for (self.link_objects.toSlice()) |link_object| { switch (link_object) { - LinkObject.StaticPath => |static_path| { + .StaticPath => |static_path| { try zig_args.append("--object"); try zig_args.append(builder.pathFromRoot(static_path)); }, - LinkObject.OtherStep => |other| switch (other.kind) { - LibExeObjStep.Kind.Exe => unreachable, - LibExeObjStep.Kind.Test => unreachable, - LibExeObjStep.Kind.Obj => { + .OtherStep => |other| switch (other.kind) { + .Exe => unreachable, + .Test => unreachable, + .Obj => { try zig_args.append("--object"); try zig_args.append(other.getOutputPath()); }, - LibExeObjStep.Kind.Lib => { + .Lib => { if (!other.is_dynamic or self.target.isWindows()) { try zig_args.append("--object"); try zig_args.append(other.getOutputLibPath()); @@ -1732,20 +1830,20 @@ pub const LibExeObjStep = struct { } }, }, - LinkObject.SystemLib => |name| { + .SystemLib => |name| { try zig_args.append("--library"); try zig_args.append(name); }, - LinkObject.AssemblyFile => |asm_file| { + .AssemblyFile => |asm_file| { try zig_args.append("--c-source"); - try zig_args.append(builder.pathFromRoot(asm_file)); + try zig_args.append(asm_file.getPath(builder)); }, - LinkObject.CSourceFile => |c_source_file| { + .CSourceFile => |c_source_file| { try zig_args.append("--c-source"); for (c_source_file.args) |arg| { try zig_args.append(arg); } - try zig_args.append(self.builder.pathFromRoot(c_source_file.source_path)); + try zig_args.append(c_source_file.source.getPath(builder)); }, } } @@ -2041,134 +2139,6 @@ pub const LibExeObjStep = struct { } }; -pub const RunStep = struct { - step: Step, - builder: *Builder, - - /// See also addArg and addArgs to modifying this directly - argv: ArrayList(Arg), - - /// Set this to modify the current working directory - cwd: ?[]const u8, - - /// Override this field to modify the environment, or use setEnvironmentVariable - env_map: ?*BufMap, - - pub const Arg = union(enum) { - Artifact: *LibExeObjStep, - Bytes: []u8, - }; - - pub fn create(builder: *Builder, name: []const u8) *RunStep { - const self = builder.allocator.create(RunStep) catch unreachable; - self.* = RunStep{ - .builder = builder, - .step = Step.init(name, builder.allocator, make), - .argv = ArrayList(Arg).init(builder.allocator), - .cwd = null, - .env_map = null, - }; - return self; - } - - pub fn addArtifactArg(self: *RunStep, artifact: *LibExeObjStep) void { - self.argv.append(Arg{ .Artifact = artifact }) catch unreachable; - self.step.dependOn(&artifact.step); - } - - pub fn addArg(self: *RunStep, arg: []const u8) void { - self.argv.append(Arg{ .Bytes = self.builder.dupe(arg) }) catch unreachable; - } - - pub fn addArgs(self: *RunStep, args: []const []const u8) void { - for (args) |arg| { - self.addArg(arg); - } - } - - pub fn clearEnvironment(self: *RunStep) void { - const new_env_map = self.builder.allocator.create(BufMap) catch unreachable; - new_env_map.* = BufMap.init(self.builder.allocator); - self.env_map = new_env_map; - } - - pub fn addPathDir(self: *RunStep, search_path: []const u8) void { - const env_map = self.getEnvMap(); - - var key: []const u8 = undefined; - var prev_path: ?[]const u8 = undefined; - if (builtin.os == .windows) { - key = "Path"; - prev_path = env_map.get(key); - if (prev_path == null) { - key = "PATH"; - prev_path = env_map.get(key); - } - } else { - key = "PATH"; - prev_path = env_map.get(key); - } - - if (prev_path) |pp| { - const new_path = self.builder.fmt("{}" ++ [1]u8{fs.path.delimiter} ++ "{}", .{ pp, search_path }); - env_map.set(key, new_path) catch unreachable; - } else { - env_map.set(key, search_path) catch unreachable; - } - } - - pub fn getEnvMap(self: *RunStep) *BufMap { - return self.env_map orelse { - const env_map = self.builder.allocator.create(BufMap) catch unreachable; - env_map.* = process.getEnvMap(self.builder.allocator) catch unreachable; - self.env_map = env_map; - return env_map; - }; - } - - pub fn setEnvironmentVariable(self: *RunStep, key: []const u8, value: []const u8) void { - const env_map = self.getEnvMap(); - env_map.set(key, value) catch unreachable; - } - - fn make(step: *Step) !void { - const self = @fieldParentPtr(RunStep, "step", step); - - const cwd = if (self.cwd) |cwd| self.builder.pathFromRoot(cwd) else self.builder.build_root; - - var argv = ArrayList([]const u8).init(self.builder.allocator); - for (self.argv.toSlice()) |arg| { - switch (arg) { - Arg.Bytes => |bytes| try argv.append(bytes), - Arg.Artifact => |artifact| { - if (artifact.target.isWindows()) { - // On Windows we don't have rpaths so we have to add .dll search paths to PATH - self.addPathForDynLibs(artifact); - } - const executable_path = artifact.installed_path orelse artifact.getOutputPath(); - try argv.append(executable_path); - }, - } - } - - return self.builder.spawnChildEnvMap(cwd, self.env_map orelse self.builder.env_map, argv.toSliceConst()); - } - - fn addPathForDynLibs(self: *RunStep, artifact: *LibExeObjStep) void { - for (artifact.link_objects.toSliceConst()) |link_object| { - switch (link_object) { - LibExeObjStep.LinkObject.OtherStep => |other| { - if (other.target.isWindows() and other.isDynamicLibrary()) { - self.addPathDir(fs.path.dirname(other.getOutputPath()).?); - self.addPathForDynLibs(other); - } - }, - else => {}, - } - } - } -}; - const InstallArtifactStep = struct { step: Step, builder: *Builder, @@ -2321,36 +2291,6 @@ pub const InstallDirStep = struct { } }; -pub const WriteFileStep = struct { - step: Step, - builder: *Builder, - file_path: []const u8, - data: []const u8, - - pub fn init(builder: *Builder, file_path: []const u8, data: []const u8) WriteFileStep { - return WriteFileStep{ - .builder = builder, - .step = Step.init(builder.fmt("writefile {}", .{file_path}), builder.allocator, make), - .file_path = file_path, - .data = data, - }; - } - - fn make(step: *Step) !void { - const self = @fieldParentPtr(WriteFileStep, "step", step); - const full_path = self.builder.pathFromRoot(self.file_path); - const full_path_dir = fs.path.dirname(full_path) orelse "."; - fs.makePath(self.builder.allocator, full_path_dir) catch |err| { - warn("unable to make path {}: {}\n", .{ full_path_dir, @errorName(err) }); - return err; - }; - io.writeFile(full_path, self.data) catch |err| { - warn("unable to write {}: {}\n", .{ full_path, @errorName(err) }); - return err; - }; - } -}; - pub const LogStep = struct { step: Step, builder: *Builder, diff --git a/lib/std/build/check_file.zig b/lib/std/build/check_file.zig new file mode 100644 index 0000000000..782cdb5600 --- /dev/null +++ b/lib/std/build/check_file.zig @@ -0,0 +1,52 @@ +const std = @import("../std.zig"); +const build = std.build; +const Step = build.Step; +const Builder = build.Builder; +const fs = std.fs; +const mem = std.mem; +const warn = std.debug.warn; + +pub const CheckFileStep = struct { + step: Step, + builder: *Builder, + expected_matches: []const []const u8, + source: build.FileSource, + max_bytes: usize = 20 * 1024 * 1024, + + pub fn create( + builder: *Builder, + source: build.FileSource, + expected_matches: []const []const u8, + ) *CheckFileStep { + const self = builder.allocator.create(CheckFileStep) catch unreachable; + self.* = CheckFileStep{ + .builder = builder, + .step = Step.init("CheckFile", builder.allocator, make), + .source = source, + .expected_matches = expected_matches, + }; + self.source.addStepDependencies(&self.step); + return self; + } + + fn make(step: *Step) !void { + const self = @fieldParentPtr(CheckFileStep, "step", step); + + const src_path = self.source.getPath(self.builder); + const contents = try fs.cwd().readFileAlloc(self.builder.allocator, src_path, self.max_bytes); + + for (self.expected_matches) |expected_match| { + if (mem.indexOf(u8, contents, expected_match) == null) { + warn( + \\ + \\========= Expected to find: =================== + \\{} + \\========= But file does not contain it: ======= + \\{} + \\ + , .{ expected_match, contents }); + return error.TestFailed; + } + } + } +}; diff --git a/lib/std/build/run.zig b/lib/std/build/run.zig new file mode 100644 index 0000000000..75809bde03 --- /dev/null +++ b/lib/std/build/run.zig @@ -0,0 +1,302 @@ +const std = @import("../std.zig"); +const builtin = std.builtin; +const build = std.build; +const Step = build.Step; +const Builder = build.Builder; +const LibExeObjStep = build.LibExeObjStep; +const fs = std.fs; +const mem = std.mem; +const process = std.process; +const ArrayList = std.ArrayList; +const BufMap = std.BufMap; +const Buffer = std.Buffer; +const warn = std.debug.warn; + +const max_stdout_size = 1 * 1024 * 1024; // 1 MiB + +pub const RunStep = struct { + step: Step, + builder: *Builder, + + /// See also addArg and addArgs to modifying this directly + argv: ArrayList(Arg), + + /// Set this to modify the current working directory + cwd: ?[]const u8, + + /// Override this field to modify the environment, or use setEnvironmentVariable + env_map: ?*BufMap, + + stdout_action: StdIoAction = .inherit, + stderr_action: StdIoAction = .inherit, + + expected_exit_code: u8 = 0, + + pub const StdIoAction = union(enum) { + inherit, + ignore, + expect_exact: []const u8, + expect_matches: []const []const u8, + }; + + pub const Arg = union(enum) { + Artifact: *LibExeObjStep, + Bytes: []u8, + }; + + pub fn create(builder: *Builder, name: []const u8) *RunStep { + const self = builder.allocator.create(RunStep) catch unreachable; + self.* = RunStep{ + .builder = builder, + .step = Step.init(name, builder.allocator, make), + .argv = ArrayList(Arg).init(builder.allocator), + .cwd = null, + .env_map = null, + }; + return self; + } + + pub fn addArtifactArg(self: *RunStep, artifact: *LibExeObjStep) void { + self.argv.append(Arg{ .Artifact = artifact }) catch unreachable; + self.step.dependOn(&artifact.step); + } + + pub fn addArg(self: *RunStep, arg: []const u8) void { + self.argv.append(Arg{ .Bytes = self.builder.dupe(arg) }) catch unreachable; + } + + pub fn addArgs(self: *RunStep, args: []const []const u8) void { + for (args) |arg| { + self.addArg(arg); + } + } + + pub fn clearEnvironment(self: *RunStep) void { + const new_env_map = self.builder.allocator.create(BufMap) catch unreachable; + new_env_map.* = BufMap.init(self.builder.allocator); + self.env_map = new_env_map; + } + + pub fn addPathDir(self: *RunStep, search_path: []const u8) void { + const env_map = self.getEnvMap(); + + var key: []const u8 = undefined; + var prev_path: ?[]const u8 = undefined; + if (builtin.os == .windows) { + key = "Path"; + prev_path = env_map.get(key); + if (prev_path == null) { + key = "PATH"; + prev_path = env_map.get(key); + } + } else { + key = "PATH"; + prev_path = env_map.get(key); + } + + if (prev_path) |pp| { + const new_path = self.builder.fmt("{}" ++ [1]u8{fs.path.delimiter} ++ "{}", .{ pp, search_path }); + env_map.set(key, new_path) catch unreachable; + } else { + env_map.set(key, search_path) catch unreachable; + } + } + + pub fn getEnvMap(self: *RunStep) *BufMap { + return self.env_map orelse { + const env_map = self.builder.allocator.create(BufMap) catch unreachable; + env_map.* = process.getEnvMap(self.builder.allocator) catch unreachable; + self.env_map = env_map; + return env_map; + }; + } + + pub fn setEnvironmentVariable(self: *RunStep, key: []const u8, value: []const u8) void { + const env_map = self.getEnvMap(); + env_map.set(key, value) catch unreachable; + } + + pub fn expectStdErrEqual(self: *RunStep, bytes: []const u8) void { + self.stderr_action = .{ .expect_exact = bytes }; + } + + pub fn expectStdOutEqual(self: *RunStep, bytes: []const u8) void { + self.stdout_action = .{ .expect_exact = bytes }; + } + + fn stdIoActionToBehavior(action: StdIoAction) std.ChildProcess.StdIo { + return switch (action) { + .ignore => .Ignore, + .inherit => .Inherit, + .expect_exact, .expect_matches => .Pipe, + }; + } + + fn make(step: *Step) !void { + const self = @fieldParentPtr(RunStep, "step", step); + + const cwd = if (self.cwd) |cwd| self.builder.pathFromRoot(cwd) else self.builder.build_root; + + var argv_list = ArrayList([]const u8).init(self.builder.allocator); + for (self.argv.toSlice()) |arg| { + switch (arg) { + Arg.Bytes => |bytes| try argv_list.append(bytes), + Arg.Artifact => |artifact| { + if (artifact.target.isWindows()) { + // On Windows we don't have rpaths so we have to add .dll search paths to PATH + self.addPathForDynLibs(artifact); + } + const executable_path = artifact.installed_path orelse artifact.getOutputPath(); + try argv_list.append(executable_path); + }, + } + } + + const argv = argv_list.toSliceConst(); + + const child = std.ChildProcess.init(argv, self.builder.allocator) catch unreachable; + defer child.deinit(); + + child.cwd = cwd; + child.env_map = self.env_map orelse self.builder.env_map; + + child.stdin_behavior = .Ignore; + child.stdout_behavior = stdIoActionToBehavior(self.stdout_action); + child.stderr_behavior = stdIoActionToBehavior(self.stderr_action); + + child.spawn() catch |err| { + warn("Unable to spawn {}: {}\n", .{ argv[0], @errorName(err) }); + return err; + }; + + var stdout = Buffer.initNull(self.builder.allocator); + var stderr = Buffer.initNull(self.builder.allocator); + + // TODO need to poll to read these streams to prevent a deadlock (or rely on evented I/O). + + switch (self.stdout_action) { + .expect_exact, .expect_matches => { + var stdout_file_in_stream = child.stdout.?.inStream(); + stdout_file_in_stream.stream.readAllBuffer(&stdout, max_stdout_size) catch unreachable; + }, + .inherit, .ignore => {}, + } + + switch (self.stdout_action) { + .expect_exact, .expect_matches => { + var stderr_file_in_stream = child.stderr.?.inStream(); + stderr_file_in_stream.stream.readAllBuffer(&stderr, max_stdout_size) catch unreachable; + }, + .inherit, .ignore => {}, + } + + const term = child.wait() catch |err| { + warn("Unable to spawn {}: {}\n", .{ argv[0], @errorName(err) }); + return err; + }; + + switch (term) { + .Exited => |code| { + if (code != self.expected_exit_code) { + warn("The following command exited with error code {} (expected {}):\n", .{ + code, + self.expected_exit_code, + }); + printCmd(cwd, argv); + return error.UncleanExit; + } + }, + else => { + warn("The following command terminated unexpectedly:\n", .{}); + printCmd(cwd, argv); + return error.UncleanExit; + }, + } + + switch (self.stderr_action) { + .inherit, .ignore => {}, + .expect_exact => |expected_bytes| { + if (!mem.eql(u8, expected_bytes, stderr.toSliceConst())) { + warn( + \\ + \\========= Expected this stderr: ========= + \\{} + \\========= But found: ==================== + \\{} + \\ + , .{ expected_bytes, stderr.toSliceConst() }); + printCmd(cwd, argv); + return error.TestFailed; + } + }, + .expect_matches => |matches| for (matches) |match| { + if (mem.indexOf(u8, stderr.toSliceConst(), match) == null) { + warn( + \\ + \\========= Expected to find in stderr: ========= + \\{} + \\========= But stderr does not contain it: ===== + \\{} + \\ + , .{ match, stderr.toSliceConst() }); + printCmd(cwd, argv); + return error.TestFailed; + } + }, + } + + switch (self.stdout_action) { + .inherit, .ignore => {}, + .expect_exact => |expected_bytes| { + if (!mem.eql(u8, expected_bytes, stdout.toSliceConst())) { + warn( + \\ + \\========= Expected this stdout: ========= + \\{} + \\========= But found: ==================== + \\{} + \\ + , .{ expected_bytes, stdout.toSliceConst() }); + printCmd(cwd, argv); + return error.TestFailed; + } + }, + .expect_matches => |matches| for (matches) |match| { + if (mem.indexOf(u8, stdout.toSliceConst(), match) == null) { + warn( + \\ + \\========= Expected to find in stdout: ========= + \\{} + \\========= But stdout does not contain it: ===== + \\{} + \\ + , .{ match, stdout.toSliceConst() }); + printCmd(cwd, argv); + return error.TestFailed; + } + }, + } + } + + fn printCmd(cwd: ?[]const u8, argv: []const []const u8) void { + if (cwd) |yes_cwd| warn("cd {} && ", .{yes_cwd}); + for (argv) |arg| { + warn("{} ", .{arg}); + } + warn("\n", .{}); + } + + fn addPathForDynLibs(self: *RunStep, artifact: *LibExeObjStep) void { + for (artifact.link_objects.toSliceConst()) |link_object| { + switch (link_object) { + .OtherStep => |other| { + if (other.target.isWindows() and other.isDynamicLibrary()) { + self.addPathDir(fs.path.dirname(other.getOutputPath()).?); + self.addPathForDynLibs(other); + } + }, + else => {}, + } + } + } +}; diff --git a/lib/std/build/translate_c.zig b/lib/std/build/translate_c.zig new file mode 100644 index 0000000000..7574dbe331 --- /dev/null +++ b/lib/std/build/translate_c.zig @@ -0,0 +1,73 @@ +const std = @import("../std.zig"); +const build = std.build; +const Step = build.Step; +const Builder = build.Builder; +const WriteFileStep = build.WriteFileStep; +const LibExeObjStep = build.LibExeObjStep; +const CheckFileStep = build.CheckFileStep; +const fs = std.fs; +const mem = std.mem; + +pub const TranslateCStep = struct { + step: Step, + builder: *Builder, + source: build.FileSource, + output_dir: ?[]const u8, + out_basename: []const u8, + + pub fn create(builder: *Builder, source: build.FileSource) *TranslateCStep { + const self = builder.allocator.create(TranslateCStep) catch unreachable; + self.* = TranslateCStep{ + .step = Step.init("zig translate-c", builder.allocator, make), + .builder = builder, + .source = source, + .output_dir = null, + .out_basename = undefined, + }; + source.addStepDependencies(&self.step); + return self; + } + + /// Unless setOutputDir was called, this function must be called only in + /// the make step, from a step that has declared a dependency on this one. + /// To run an executable built with zig build, use `run`, or create an install step and invoke it. + pub fn getOutputPath(self: *TranslateCStep) []const u8 { + return fs.path.join( + self.builder.allocator, + &[_][]const u8{ self.output_dir.?, self.out_basename }, + ) catch unreachable; + } + + /// Creates a step to build an executable from the translated source. + pub fn addExecutable(self: *TranslateCStep) *LibExeObjStep { + return self.builder.addExecutableSource("translated_c", @as(build.FileSource, .{ .translate_c = self })); + } + + pub fn addCheckFile(self: *TranslateCStep, expected_matches: []const []const u8) *CheckFileStep { + return CheckFileStep.create(self.builder, .{ .translate_c = self }, expected_matches); + } + + fn make(step: *Step) !void { + const self = @fieldParentPtr(TranslateCStep, "step", step); + + const argv = [_][]const u8{ + self.builder.zig_exe, + "translate-c", + "-lc", + "--cache", + "on", + self.source.getPath(self.builder), + }; + + const output_path_nl = try self.builder.exec(&argv); + const output_path = mem.trimRight(u8, output_path_nl, "\r\n"); + + self.out_basename = fs.path.basename(output_path); + if (self.output_dir) |output_dir| { + const full_dest = try fs.path.join(self.builder.allocator, &[_][]const u8{ output_dir, self.out_basename }); + try self.builder.updateFile(output_path, full_dest); + } else { + self.output_dir = fs.path.dirname(output_path).?; + } + } +}; diff --git a/lib/std/build/write_file.zig b/lib/std/build/write_file.zig new file mode 100644 index 0000000000..13d131ac61 --- /dev/null +++ b/lib/std/build/write_file.zig @@ -0,0 +1,94 @@ +const std = @import("../std.zig"); +const build = @import("../build.zig"); +const Step = build.Step; +const Builder = build.Builder; +const fs = std.fs; +const warn = std.debug.warn; +const ArrayList = std.ArrayList; + +pub const WriteFileStep = struct { + step: Step, + builder: *Builder, + output_dir: []const u8, + files: ArrayList(File), + + pub const File = struct { + basename: []const u8, + bytes: []const u8, + }; + + pub fn init(builder: *Builder) WriteFileStep { + return WriteFileStep{ + .builder = builder, + .step = Step.init("writefile", builder.allocator, make), + .files = ArrayList(File).init(builder.allocator), + .output_dir = undefined, + }; + } + + pub fn add(self: *WriteFileStep, basename: []const u8, bytes: []const u8) void { + self.files.append(.{ .basename = basename, .bytes = bytes }) catch unreachable; + } + + /// Unless setOutputDir was called, this function must be called only in + /// the make step, from a step that has declared a dependency on this one. + /// To run an executable built with zig build, use `run`, or create an install step and invoke it. + pub fn getOutputPath(self: *WriteFileStep, basename: []const u8) []const u8 { + return fs.path.join( + self.builder.allocator, + &[_][]const u8{ self.output_dir, basename }, + ) catch unreachable; + } + + fn make(step: *Step) !void { + const self = @fieldParentPtr(WriteFileStep, "step", step); + + // The cache is used here not really as a way to speed things up - because writing + // the data to a file would probably be very fast - but as a way to find a canonical + // location to put build artifacts. + + // If, for example, a hard-coded path was used as the location to put WriteFileStep + // files, then two WriteFileSteps executing in parallel might clobber each other. + + // TODO port the cache system from stage1 to zig std lib. Until then we use blake2b + // directly and construct the path, and no "cache hit" detection happens; the files + // are always written. + var hash = std.crypto.Blake2b384.init(); + + // Random bytes to make WriteFileStep unique. Refresh this with + // new random bytes when WriteFileStep implementation is modified + // in a non-backwards-compatible way. + hash.update("eagVR1dYXoE7ARDP"); + for (self.files.toSliceConst()) |file| { + hash.update(file.basename); + hash.update(file.bytes); + hash.update("|"); + } + var digest: [48]u8 = undefined; + hash.final(&digest); + var hash_basename: [64]u8 = undefined; + fs.base64_encoder.encode(&hash_basename, &digest); + self.output_dir = try fs.path.join(self.builder.allocator, &[_][]const u8{ + self.builder.cache_root, + "o", + &hash_basename, + }); + // TODO replace with something like fs.makePathAndOpenDir + fs.makePath(self.builder.allocator, self.output_dir) catch |err| { + warn("unable to make path {}: {}\n", .{ self.output_dir, @errorName(err) }); + return err; + }; + var dir = try fs.cwd().openDirTraverse(self.output_dir); + defer dir.close(); + for (self.files.toSliceConst()) |file| { + dir.writeFile(file.basename, file.bytes) catch |err| { + warn("unable to write {} into {}: {}\n", .{ + file.basename, + self.output_dir, + @errorName(err), + }); + return err; + }; + } + } +}; diff --git a/lib/std/fs.zig b/lib/std/fs.zig index ea62086f59..540d5f1a1a 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -37,8 +37,11 @@ pub const MAX_PATH_BYTES = switch (builtin.os) { else => @compileError("Unsupported OS"), }; -// here we replace the standard +/ with -_ so that it can be used in a file name -const b64_fs_encoder = base64.Base64Encoder.init("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", base64.standard_pad_char); +/// Base64, replacing the standard `+/` with `-_` so that it can be used in a file name on any filesystem. +pub const base64_encoder = base64.Base64Encoder.init( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", + base64.standard_pad_char, +); /// TODO remove the allocator requirement from this API pub fn atomicSymLink(allocator: *Allocator, existing_path: []const u8, new_path: []const u8) !void { @@ -58,7 +61,7 @@ pub fn atomicSymLink(allocator: *Allocator, existing_path: []const u8, new_path: tmp_path[dirname.len] = path.sep; while (true) { try crypto.randomBytes(rand_buf[0..]); - b64_fs_encoder.encode(tmp_path[dirname.len + 1 ..], &rand_buf); + base64_encoder.encode(tmp_path[dirname.len + 1 ..], &rand_buf); if (symLink(existing_path, tmp_path)) { return rename(tmp_path, new_path); @@ -227,10 +230,10 @@ pub const AtomicFile = struct { while (true) { try crypto.randomBytes(rand_buf[0..]); - b64_fs_encoder.encode(tmp_path_slice[dirname_component_len..tmp_path_len], &rand_buf); + base64_encoder.encode(tmp_path_slice[dirname_component_len..tmp_path_len], &rand_buf); const file = my_cwd.createFileC( - tmp_path_slice, + tmp_path_slice, .{ .mode = mode, .exclusive = true }, ) catch |err| switch (err) { error.PathAlreadyExists => continue, diff --git a/test/src/compare_output.zig b/test/src/compare_output.zig new file mode 100644 index 0000000000..ae994a0697 --- /dev/null +++ b/test/src/compare_output.zig @@ -0,0 +1,165 @@ +// This is the implementation of the test harness. +// For the actual test cases, see test/compare_output.zig. +const std = @import("std"); +const builtin = std.builtin; +const build = std.build; +const ArrayList = std.ArrayList; +const fmt = std.fmt; +const mem = std.mem; +const fs = std.fs; +const warn = std.debug.warn; +const Mode = builtin.Mode; + +pub const CompareOutputContext = struct { + b: *build.Builder, + step: *build.Step, + test_index: usize, + test_filter: ?[]const u8, + modes: []const Mode, + + const Special = enum { + None, + Asm, + RuntimeSafety, + }; + + const TestCase = struct { + name: []const u8, + sources: ArrayList(SourceFile), + expected_output: []const u8, + link_libc: bool, + special: Special, + cli_args: []const []const u8, + + const SourceFile = struct { + filename: []const u8, + source: []const u8, + }; + + pub fn addSourceFile(self: *TestCase, filename: []const u8, source: []const u8) void { + self.sources.append(SourceFile{ + .filename = filename, + .source = source, + }) catch unreachable; + } + + pub fn setCommandLineArgs(self: *TestCase, args: []const []const u8) void { + self.cli_args = args; + } + }; + + pub fn createExtra(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8, special: Special) TestCase { + var tc = TestCase{ + .name = name, + .sources = ArrayList(TestCase.SourceFile).init(self.b.allocator), + .expected_output = expected_output, + .link_libc = false, + .special = special, + .cli_args = &[_][]const u8{}, + }; + const root_src_name = if (special == Special.Asm) "source.s" else "source.zig"; + tc.addSourceFile(root_src_name, source); + return tc; + } + + pub fn create(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8) TestCase { + return createExtra(self, name, source, expected_output, Special.None); + } + + pub fn addC(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8) void { + var tc = self.create(name, source, expected_output); + tc.link_libc = true; + self.addCase(tc); + } + + pub fn add(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8) void { + const tc = self.create(name, source, expected_output); + self.addCase(tc); + } + + pub fn addAsm(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8) void { + const tc = self.createExtra(name, source, expected_output, Special.Asm); + self.addCase(tc); + } + + pub fn addRuntimeSafety(self: *CompareOutputContext, name: []const u8, source: []const u8) void { + const tc = self.createExtra(name, source, undefined, Special.RuntimeSafety); + self.addCase(tc); + } + + pub fn addCase(self: *CompareOutputContext, case: TestCase) void { + const b = self.b; + + const write_src = b.addWriteFiles(); + for (case.sources.toSliceConst()) |src_file| { + write_src.add(src_file.filename, src_file.source); + } + + switch (case.special) { + Special.Asm => { + const annotated_case_name = fmt.allocPrint(self.b.allocator, "assemble-and-link {}", .{ + case.name, + }) catch unreachable; + if (self.test_filter) |filter| { + if (mem.indexOf(u8, annotated_case_name, filter) == null) return; + } + + const exe = b.addExecutable("test", null); + exe.addAssemblyFileFromWriteFileStep(write_src, case.sources.toSliceConst()[0].filename); + + const run = exe.run(); + run.addArgs(case.cli_args); + run.expectStdErrEqual(""); + run.expectStdOutEqual(case.expected_output); + + self.step.dependOn(&run.step); + }, + Special.None => { + for (self.modes) |mode| { + const annotated_case_name = fmt.allocPrint(self.b.allocator, "{} {} ({})", .{ + "compare-output", + case.name, + @tagName(mode), + }) catch unreachable; + if (self.test_filter) |filter| { + if (mem.indexOf(u8, annotated_case_name, filter) == null) continue; + } + + const basename = case.sources.toSliceConst()[0].filename; + const exe = b.addExecutableFromWriteFileStep("test", write_src, basename); + exe.setBuildMode(mode); + if (case.link_libc) { + exe.linkSystemLibrary("c"); + } + + const run = exe.run(); + run.addArgs(case.cli_args); + run.expectStdErrEqual(""); + run.expectStdOutEqual(case.expected_output); + + self.step.dependOn(&run.step); + } + }, + Special.RuntimeSafety => { + const annotated_case_name = fmt.allocPrint(self.b.allocator, "safety {}", .{case.name}) catch unreachable; + if (self.test_filter) |filter| { + if (mem.indexOf(u8, annotated_case_name, filter) == null) return; + } + + const basename = case.sources.toSliceConst()[0].filename; + const exe = b.addExecutableFromWriteFileStep("test", write_src, basename); + if (case.link_libc) { + exe.linkSystemLibrary("c"); + } + + const run = exe.run(); + run.addArgs(case.cli_args); + run.stderr_action = .ignore; + run.stdout_action = .ignore; + run.expected_exit_code = 126; + + self.step.dependOn(&run.step); + }, + } + } +}; diff --git a/test/src/run_translated_c.zig b/test/src/run_translated_c.zig index ae09e32dbc..d136a5fc94 100644 --- a/test/src/run_translated_c.zig +++ b/test/src/run_translated_c.zig @@ -33,89 +33,6 @@ pub const RunTranslatedCContext = struct { } }; - const DoEverythingStep = struct { - step: build.Step, - context: *RunTranslatedCContext, - name: []const u8, - case: *const TestCase, - test_index: usize, - - pub fn create( - context: *RunTranslatedCContext, - name: []const u8, - case: *const TestCase, - ) *DoEverythingStep { - const allocator = context.b.allocator; - const ptr = allocator.create(DoEverythingStep) catch unreachable; - ptr.* = DoEverythingStep{ - .context = context, - .name = name, - .case = case, - .test_index = context.test_index, - .step = build.Step.init("RunTranslatedC", allocator, make), - }; - context.test_index += 1; - return ptr; - } - - fn make(step: *build.Step) !void { - const self = @fieldParentPtr(DoEverythingStep, "step", step); - const b = self.context.b; - - warn("Test {}/{} {}...", .{ self.test_index + 1, self.context.test_index, self.name }); - // translate from c to zig - const translated_c_code = blk: { - var zig_args = ArrayList([]const u8).init(b.allocator); - defer zig_args.deinit(); - - const rel_c_filename = try fs.path.join(b.allocator, &[_][]const u8{ - b.cache_root, - self.case.sources.toSliceConst()[0].filename, - }); - - try zig_args.append(b.zig_exe); - try zig_args.append("translate-c"); - try zig_args.append("-lc"); - try zig_args.append(b.pathFromRoot(rel_c_filename)); - - break :blk try b.exec(zig_args.toSliceConst()); - }; - - // write stdout to a file - - const translated_c_path = try fs.path.join(b.allocator, - &[_][]const u8{ b.cache_root, "translated_c.zig" }); - try fs.cwd().writeFile(translated_c_path, translated_c_code); - - // zig run the result - const run_stdout = blk: { - var zig_args = ArrayList([]const u8).init(b.allocator); - defer zig_args.deinit(); - - try zig_args.append(b.zig_exe); - try zig_args.append("-lc"); - try zig_args.append("run"); - try zig_args.append(translated_c_path); - - break :blk try b.exec(zig_args.toSliceConst()); - }; - // compare stdout - if (!mem.eql(u8, self.case.expected_stdout, run_stdout)) { - warn( - \\ - \\========= Expected this output: ========= - \\{} - \\========= But found: ==================== - \\{} - \\ - , .{ self.case.expected_stdout, run_stdout }); - return error.TestFailed; - } - - warn("OK\n", .{}); - } - }; - pub fn create( self: *RunTranslatedCContext, allow_warnings: bool, @@ -159,22 +76,29 @@ pub const RunTranslatedCContext = struct { pub fn addCase(self: *RunTranslatedCContext, case: *const TestCase) void { const b = self.b; - const annotated_case_name = fmt.allocPrint(self.b.allocator, "run-translated-c {}", .{ case.name }) catch unreachable; + const annotated_case_name = fmt.allocPrint(self.b.allocator, "run-translated-c {}", .{case.name}) catch unreachable; if (self.test_filter) |filter| { if (mem.indexOf(u8, annotated_case_name, filter) == null) return; } - const do_everything_step = DoEverythingStep.create(self, annotated_case_name, case); - self.step.dependOn(&do_everything_step.step); - + const write_src = b.addWriteFiles(); for (case.sources.toSliceConst()) |src_file| { - const expanded_src_path = fs.path.join( - b.allocator, - &[_][]const u8{ b.cache_root, src_file.filename }, - ) catch unreachable; - const write_src = b.addWriteFile(expanded_src_path, src_file.source); - do_everything_step.step.dependOn(&write_src.step); + write_src.add(src_file.filename, src_file.source); } + const translate_c = b.addTranslateC(.{ + .write_file = .{ + .step = write_src, + .basename = case.sources.toSliceConst()[0].filename, + }, + }); + const exe = translate_c.addExecutable(); + exe.linkLibC(); + const run = exe.run(); + if (!case.allow_warnings) { + run.expectStdErrEqual(""); + } + run.expectStdOutEqual(case.expected_stdout); + + self.step.dependOn(&run.step); } }; - diff --git a/test/src/translate_c.zig b/test/src/translate_c.zig new file mode 100644 index 0000000000..6db1e6f181 --- /dev/null +++ b/test/src/translate_c.zig @@ -0,0 +1,109 @@ +// This is the implementation of the test harness. +// For the actual test cases, see test/translate_c.zig. +const std = @import("std"); +const build = std.build; +const ArrayList = std.ArrayList; +const fmt = std.fmt; +const mem = std.mem; +const fs = std.fs; +const warn = std.debug.warn; + +pub const TranslateCContext = struct { + b: *build.Builder, + step: *build.Step, + test_index: usize, + test_filter: ?[]const u8, + + const TestCase = struct { + name: []const u8, + sources: ArrayList(SourceFile), + expected_lines: ArrayList([]const u8), + allow_warnings: bool, + + const SourceFile = struct { + filename: []const u8, + source: []const u8, + }; + + pub fn addSourceFile(self: *TestCase, filename: []const u8, source: []const u8) void { + self.sources.append(SourceFile{ + .filename = filename, + .source = source, + }) catch unreachable; + } + + pub fn addExpectedLine(self: *TestCase, text: []const u8) void { + self.expected_lines.append(text) catch unreachable; + } + }; + + pub fn create( + self: *TranslateCContext, + allow_warnings: bool, + filename: []const u8, + name: []const u8, + source: []const u8, + expected_lines: []const []const u8, + ) *TestCase { + const tc = self.b.allocator.create(TestCase) catch unreachable; + tc.* = TestCase{ + .name = name, + .sources = ArrayList(TestCase.SourceFile).init(self.b.allocator), + .expected_lines = ArrayList([]const u8).init(self.b.allocator), + .allow_warnings = allow_warnings, + }; + + tc.addSourceFile(filename, source); + var arg_i: usize = 0; + while (arg_i < expected_lines.len) : (arg_i += 1) { + tc.addExpectedLine(expected_lines[arg_i]); + } + return tc; + } + + pub fn add( + self: *TranslateCContext, + name: []const u8, + source: []const u8, + expected_lines: []const []const u8, + ) void { + const tc = self.create(false, "source.h", name, source, expected_lines); + self.addCase(tc); + } + + pub fn addAllowWarnings( + self: *TranslateCContext, + name: []const u8, + source: []const u8, + expected_lines: []const []const u8, + ) void { + const tc = self.create(true, "source.h", name, source, expected_lines); + self.addCase(tc); + } + + pub fn addCase(self: *TranslateCContext, case: *const TestCase) void { + const b = self.b; + + const translate_c_cmd = "translate-c"; + const annotated_case_name = fmt.allocPrint(self.b.allocator, "{} {}", .{ translate_c_cmd, case.name }) catch unreachable; + if (self.test_filter) |filter| { + if (mem.indexOf(u8, annotated_case_name, filter) == null) return; + } + + const write_src = b.addWriteFiles(); + for (case.sources.toSliceConst()) |src_file| { + write_src.add(src_file.filename, src_file.source); + } + + const translate_c = b.addTranslateC(.{ + .write_file = .{ + .step = write_src, + .basename = case.sources.toSliceConst()[0].filename, + }, + }); + + const check_file = translate_c.addCheckFile(case.expected_lines.toSliceConst()); + + self.step.dependOn(&check_file.step); + } +}; diff --git a/test/tests.zig b/test/tests.zig index 4636eb8132..c178364308 100644 --- a/test/tests.zig +++ b/test/tests.zig @@ -26,7 +26,9 @@ const run_translated_c = @import("run_translated_c.zig"); const gen_h = @import("gen_h.zig"); // Implementations +pub const TranslateCContext = @import("src/translate_c.zig").TranslateCContext; pub const RunTranslatedCContext = @import("src/run_translated_c.zig").RunTranslatedCContext; +pub const CompareOutputContext = @import("src/compare_output.zig").CompareOutputContext; const TestTarget = struct { target: Target = .Native, @@ -498,356 +500,6 @@ pub fn addPkgTests( return step; } -pub const CompareOutputContext = struct { - b: *build.Builder, - step: *build.Step, - test_index: usize, - test_filter: ?[]const u8, - modes: []const Mode, - - const Special = enum { - None, - Asm, - RuntimeSafety, - }; - - const TestCase = struct { - name: []const u8, - sources: ArrayList(SourceFile), - expected_output: []const u8, - link_libc: bool, - special: Special, - cli_args: []const []const u8, - - const SourceFile = struct { - filename: []const u8, - source: []const u8, - }; - - pub fn addSourceFile(self: *TestCase, filename: []const u8, source: []const u8) void { - self.sources.append(SourceFile{ - .filename = filename, - .source = source, - }) catch unreachable; - } - - pub fn setCommandLineArgs(self: *TestCase, args: []const []const u8) void { - self.cli_args = args; - } - }; - - const RunCompareOutputStep = struct { - step: build.Step, - context: *CompareOutputContext, - exe: *LibExeObjStep, - name: []const u8, - expected_output: []const u8, - test_index: usize, - cli_args: []const []const u8, - - pub fn create( - context: *CompareOutputContext, - exe: *LibExeObjStep, - name: []const u8, - expected_output: []const u8, - cli_args: []const []const u8, - ) *RunCompareOutputStep { - const allocator = context.b.allocator; - const ptr = allocator.create(RunCompareOutputStep) catch unreachable; - ptr.* = RunCompareOutputStep{ - .context = context, - .exe = exe, - .name = name, - .expected_output = expected_output, - .test_index = context.test_index, - .step = build.Step.init("RunCompareOutput", allocator, make), - .cli_args = cli_args, - }; - ptr.step.dependOn(&exe.step); - context.test_index += 1; - return ptr; - } - - fn make(step: *build.Step) !void { - const self = @fieldParentPtr(RunCompareOutputStep, "step", step); - const b = self.context.b; - - const full_exe_path = self.exe.getOutputPath(); - var args = ArrayList([]const u8).init(b.allocator); - defer args.deinit(); - - args.append(full_exe_path) catch unreachable; - for (self.cli_args) |arg| { - args.append(arg) catch unreachable; - } - - warn("Test {}/{} {}...", .{ self.test_index + 1, self.context.test_index, self.name }); - - const child = std.ChildProcess.init(args.toSliceConst(), b.allocator) catch unreachable; - defer child.deinit(); - - child.stdin_behavior = .Ignore; - child.stdout_behavior = .Pipe; - child.stderr_behavior = .Pipe; - child.env_map = b.env_map; - - child.spawn() catch |err| debug.panic("Unable to spawn {}: {}\n", .{ full_exe_path, @errorName(err) }); - - var stdout = Buffer.initNull(b.allocator); - var stderr = Buffer.initNull(b.allocator); - - var stdout_file_in_stream = child.stdout.?.inStream(); - var stderr_file_in_stream = child.stderr.?.inStream(); - - stdout_file_in_stream.stream.readAllBuffer(&stdout, max_stdout_size) catch unreachable; - stderr_file_in_stream.stream.readAllBuffer(&stderr, max_stdout_size) catch unreachable; - - const term = child.wait() catch |err| { - debug.panic("Unable to spawn {}: {}\n", .{ full_exe_path, @errorName(err) }); - }; - switch (term) { - .Exited => |code| { - if (code != 0) { - warn("Process {} exited with error code {}\n", .{ full_exe_path, code }); - printInvocation(args.toSliceConst()); - return error.TestFailed; - } - }, - else => { - warn("Process {} terminated unexpectedly\n", .{full_exe_path}); - printInvocation(args.toSliceConst()); - return error.TestFailed; - }, - } - - if (!mem.eql(u8, self.expected_output, stdout.toSliceConst())) { - warn( - \\ - \\========= Expected this output: ========= - \\{} - \\========= But found: ==================== - \\{} - \\ - , .{ self.expected_output, stdout.toSliceConst() }); - return error.TestFailed; - } - warn("OK\n", .{}); - } - }; - - const RuntimeSafetyRunStep = struct { - step: build.Step, - context: *CompareOutputContext, - exe: *LibExeObjStep, - name: []const u8, - test_index: usize, - - pub fn create(context: *CompareOutputContext, exe: *LibExeObjStep, name: []const u8) *RuntimeSafetyRunStep { - const allocator = context.b.allocator; - const ptr = allocator.create(RuntimeSafetyRunStep) catch unreachable; - ptr.* = RuntimeSafetyRunStep{ - .context = context, - .exe = exe, - .name = name, - .test_index = context.test_index, - .step = build.Step.init("RuntimeSafetyRun", allocator, make), - }; - ptr.step.dependOn(&exe.step); - context.test_index += 1; - return ptr; - } - - fn make(step: *build.Step) !void { - const self = @fieldParentPtr(RuntimeSafetyRunStep, "step", step); - const b = self.context.b; - - const full_exe_path = self.exe.getOutputPath(); - - warn("Test {}/{} {}...", .{ self.test_index + 1, self.context.test_index, self.name }); - - const child = std.ChildProcess.init(&[_][]const u8{full_exe_path}, b.allocator) catch unreachable; - defer child.deinit(); - - child.env_map = b.env_map; - child.stdin_behavior = .Ignore; - child.stdout_behavior = .Ignore; - child.stderr_behavior = .Ignore; - - const term = child.spawnAndWait() catch |err| { - debug.panic("Unable to spawn {}: {}\n", .{ full_exe_path, @errorName(err) }); - }; - - const expected_exit_code: u32 = 126; - switch (term) { - .Exited => |code| { - if (code != expected_exit_code) { - warn("\nProgram expected to exit with code {} but exited with code {}\n", .{ - expected_exit_code, code, - }); - return error.TestFailed; - } - }, - .Signal => |sig| { - warn("\nProgram expected to exit with code {} but instead signaled {}\n", .{ - expected_exit_code, sig, - }); - return error.TestFailed; - }, - else => { - warn("\nProgram expected to exit with code {} but exited in an unexpected way\n", .{ - expected_exit_code, - }); - return error.TestFailed; - }, - } - - warn("OK\n", .{}); - } - }; - - pub fn createExtra(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8, special: Special) TestCase { - var tc = TestCase{ - .name = name, - .sources = ArrayList(TestCase.SourceFile).init(self.b.allocator), - .expected_output = expected_output, - .link_libc = false, - .special = special, - .cli_args = &[_][]const u8{}, - }; - const root_src_name = if (special == Special.Asm) "source.s" else "source.zig"; - tc.addSourceFile(root_src_name, source); - return tc; - } - - pub fn create(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8) TestCase { - return createExtra(self, name, source, expected_output, Special.None); - } - - pub fn addC(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8) void { - var tc = self.create(name, source, expected_output); - tc.link_libc = true; - self.addCase(tc); - } - - pub fn add(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8) void { - const tc = self.create(name, source, expected_output); - self.addCase(tc); - } - - pub fn addAsm(self: *CompareOutputContext, name: []const u8, source: []const u8, expected_output: []const u8) void { - const tc = self.createExtra(name, source, expected_output, Special.Asm); - self.addCase(tc); - } - - pub fn addRuntimeSafety(self: *CompareOutputContext, name: []const u8, source: []const u8) void { - const tc = self.createExtra(name, source, undefined, Special.RuntimeSafety); - self.addCase(tc); - } - - pub fn addCase(self: *CompareOutputContext, case: TestCase) void { - const b = self.b; - - const root_src = fs.path.join( - b.allocator, - &[_][]const u8{ b.cache_root, case.sources.items[0].filename }, - ) catch unreachable; - - switch (case.special) { - Special.Asm => { - const annotated_case_name = fmt.allocPrint(self.b.allocator, "assemble-and-link {}", .{ - case.name, - }) catch unreachable; - if (self.test_filter) |filter| { - if (mem.indexOf(u8, annotated_case_name, filter) == null) return; - } - - const exe = b.addExecutable("test", null); - exe.addAssemblyFile(root_src); - - for (case.sources.toSliceConst()) |src_file| { - const expanded_src_path = fs.path.join( - b.allocator, - &[_][]const u8{ b.cache_root, src_file.filename }, - ) catch unreachable; - const write_src = b.addWriteFile(expanded_src_path, src_file.source); - exe.step.dependOn(&write_src.step); - } - - const run_and_cmp_output = RunCompareOutputStep.create( - self, - exe, - annotated_case_name, - case.expected_output, - case.cli_args, - ); - - self.step.dependOn(&run_and_cmp_output.step); - }, - Special.None => { - for (self.modes) |mode| { - const annotated_case_name = fmt.allocPrint(self.b.allocator, "{} {} ({})", .{ - "compare-output", - case.name, - @tagName(mode), - }) catch unreachable; - if (self.test_filter) |filter| { - if (mem.indexOf(u8, annotated_case_name, filter) == null) continue; - } - - const exe = b.addExecutable("test", root_src); - exe.setBuildMode(mode); - if (case.link_libc) { - exe.linkSystemLibrary("c"); - } - - for (case.sources.toSliceConst()) |src_file| { - const expanded_src_path = fs.path.join( - b.allocator, - &[_][]const u8{ b.cache_root, src_file.filename }, - ) catch unreachable; - const write_src = b.addWriteFile(expanded_src_path, src_file.source); - exe.step.dependOn(&write_src.step); - } - - const run_and_cmp_output = RunCompareOutputStep.create( - self, - exe, - annotated_case_name, - case.expected_output, - case.cli_args, - ); - - self.step.dependOn(&run_and_cmp_output.step); - } - }, - Special.RuntimeSafety => { - const annotated_case_name = fmt.allocPrint(self.b.allocator, "safety {}", .{case.name}) catch unreachable; - if (self.test_filter) |filter| { - if (mem.indexOf(u8, annotated_case_name, filter) == null) return; - } - - const exe = b.addExecutable("test", root_src); - if (case.link_libc) { - exe.linkSystemLibrary("c"); - } - - for (case.sources.toSliceConst()) |src_file| { - const expanded_src_path = fs.path.join( - b.allocator, - &[_][]const u8{ b.cache_root, src_file.filename }, - ) catch unreachable; - const write_src = b.addWriteFile(expanded_src_path, src_file.source); - exe.step.dependOn(&write_src.step); - } - - const run_and_cmp_output = RuntimeSafetyRunStep.create(self, exe, annotated_case_name); - - self.step.dependOn(&run_and_cmp_output.step); - }, - } - } -}; - pub const StackTracesContext = struct { b: *build.Builder, step: *build.Step, @@ -1430,230 +1082,6 @@ pub const StandaloneContext = struct { } }; -pub const TranslateCContext = struct { - b: *build.Builder, - step: *build.Step, - test_index: usize, - test_filter: ?[]const u8, - - const TestCase = struct { - name: []const u8, - sources: ArrayList(SourceFile), - expected_lines: ArrayList([]const u8), - allow_warnings: bool, - - const SourceFile = struct { - filename: []const u8, - source: []const u8, - }; - - pub fn addSourceFile(self: *TestCase, filename: []const u8, source: []const u8) void { - self.sources.append(SourceFile{ - .filename = filename, - .source = source, - }) catch unreachable; - } - - pub fn addExpectedLine(self: *TestCase, text: []const u8) void { - self.expected_lines.append(text) catch unreachable; - } - }; - - const TranslateCCmpOutputStep = struct { - step: build.Step, - context: *TranslateCContext, - name: []const u8, - test_index: usize, - case: *const TestCase, - - pub fn create(context: *TranslateCContext, name: []const u8, case: *const TestCase) *TranslateCCmpOutputStep { - const allocator = context.b.allocator; - const ptr = allocator.create(TranslateCCmpOutputStep) catch unreachable; - ptr.* = TranslateCCmpOutputStep{ - .step = build.Step.init("ParseCCmpOutput", allocator, make), - .context = context, - .name = name, - .test_index = context.test_index, - .case = case, - }; - - context.test_index += 1; - return ptr; - } - - fn make(step: *build.Step) !void { - const self = @fieldParentPtr(TranslateCCmpOutputStep, "step", step); - const b = self.context.b; - - const root_src = fs.path.join( - b.allocator, - &[_][]const u8{ b.cache_root, self.case.sources.items[0].filename }, - ) catch unreachable; - - var zig_args = ArrayList([]const u8).init(b.allocator); - zig_args.append(b.zig_exe) catch unreachable; - - const translate_c_cmd = "translate-c"; - zig_args.append(translate_c_cmd) catch unreachable; - zig_args.append(b.pathFromRoot(root_src)) catch unreachable; - - warn("Test {}/{} {}...", .{ self.test_index + 1, self.context.test_index, self.name }); - - if (b.verbose) { - printInvocation(zig_args.toSliceConst()); - } - - const child = std.ChildProcess.init(zig_args.toSliceConst(), b.allocator) catch unreachable; - defer child.deinit(); - - child.env_map = b.env_map; - child.stdin_behavior = .Ignore; - child.stdout_behavior = .Pipe; - child.stderr_behavior = .Pipe; - - child.spawn() catch |err| debug.panic("Unable to spawn {}: {}\n", .{ - zig_args.toSliceConst()[0], - @errorName(err), - }); - - var stdout_buf = Buffer.initNull(b.allocator); - var stderr_buf = Buffer.initNull(b.allocator); - - var stdout_file_in_stream = child.stdout.?.inStream(); - var stderr_file_in_stream = child.stderr.?.inStream(); - - stdout_file_in_stream.stream.readAllBuffer(&stdout_buf, max_stdout_size) catch unreachable; - stderr_file_in_stream.stream.readAllBuffer(&stderr_buf, max_stdout_size) catch unreachable; - - const term = child.wait() catch |err| { - debug.panic("Unable to spawn {}: {}\n", .{ zig_args.toSliceConst()[0], @errorName(err) }); - }; - switch (term) { - .Exited => |code| { - if (code != 0) { - warn("Compilation failed with exit code {}\n{}\n", .{ code, stderr_buf.toSliceConst() }); - printInvocation(zig_args.toSliceConst()); - return error.TestFailed; - } - }, - .Signal => |code| { - warn("Compilation failed with signal {}\n", .{code}); - printInvocation(zig_args.toSliceConst()); - return error.TestFailed; - }, - else => { - warn("Compilation terminated unexpectedly\n", .{}); - printInvocation(zig_args.toSliceConst()); - return error.TestFailed; - }, - } - - const stdout = stdout_buf.toSliceConst(); - const stderr = stderr_buf.toSliceConst(); - - if (stderr.len != 0 and !self.case.allow_warnings) { - warn( - \\====== translate-c emitted warnings: ======= - \\{} - \\============================================ - \\ - , .{stderr}); - printInvocation(zig_args.toSliceConst()); - return error.TestFailed; - } - - for (self.case.expected_lines.toSliceConst()) |expected_line| { - if (mem.indexOf(u8, stdout, expected_line) == null) { - warn( - \\ - \\========= Expected this output: ================ - \\{} - \\========= But found: =========================== - \\{} - \\ - , .{ expected_line, stdout }); - printInvocation(zig_args.toSliceConst()); - return error.TestFailed; - } - } - warn("OK\n", .{}); - } - }; - - fn printInvocation(args: []const []const u8) void { - for (args) |arg| { - warn("{} ", .{arg}); - } - warn("\n", .{}); - } - - pub fn create( - self: *TranslateCContext, - allow_warnings: bool, - filename: []const u8, - name: []const u8, - source: []const u8, - expected_lines: []const []const u8, - ) *TestCase { - const tc = self.b.allocator.create(TestCase) catch unreachable; - tc.* = TestCase{ - .name = name, - .sources = ArrayList(TestCase.SourceFile).init(self.b.allocator), - .expected_lines = ArrayList([]const u8).init(self.b.allocator), - .allow_warnings = allow_warnings, - }; - - tc.addSourceFile(filename, source); - var arg_i: usize = 0; - while (arg_i < expected_lines.len) : (arg_i += 1) { - tc.addExpectedLine(expected_lines[arg_i]); - } - return tc; - } - - pub fn add( - self: *TranslateCContext, - name: []const u8, - source: []const u8, - expected_lines: []const []const u8, - ) void { - const tc = self.create(false, "source.h", name, source, expected_lines); - self.addCase(tc); - } - - pub fn addAllowWarnings( - self: *TranslateCContext, - name: []const u8, - source: []const u8, - expected_lines: []const []const u8, - ) void { - const tc = self.create(true, "source.h", name, source, expected_lines); - self.addCase(tc); - } - - pub fn addCase(self: *TranslateCContext, case: *const TestCase) void { - const b = self.b; - - const translate_c_cmd = "translate-c"; - const annotated_case_name = fmt.allocPrint(self.b.allocator, "{} {}", .{ translate_c_cmd, case.name }) catch unreachable; - if (self.test_filter) |filter| { - if (mem.indexOf(u8, annotated_case_name, filter) == null) return; - } - - const translate_c_and_cmp = TranslateCCmpOutputStep.create(self, annotated_case_name, case); - self.step.dependOn(&translate_c_and_cmp.step); - - for (case.sources.toSliceConst()) |src_file| { - const expanded_src_path = fs.path.join( - b.allocator, - &[_][]const u8{ b.cache_root, src_file.filename }, - ) catch unreachable; - const write_src = b.addWriteFile(expanded_src_path, src_file.source); - translate_c_and_cmp.step.dependOn(&write_src.step); - } - } -}; - pub const GenHContext = struct { b: *build.Builder, step: *build.Step,