mirror of
https://github.com/ziglang/zig.git
synced 2025-12-06 06:13:07 +00:00
compiler: allow emitting tests to an object file
This is fairly straightforward; the actual compiler changes are limited to the CLI, since `Compilation` already supports this combination. A new `std.Build` API is introduced to allow representing this. By passing the `emit_object` option to `std.Build.addTest`, you get a `Step.Compile` which emits an object file; you can then use that as you would any other object, such as either installing it for external use, or linking it into another step. A standalone test is added to cover the build system API. It builds a test into an object, and links it into a final executable, which it then runs. Using this build system mechanism prevents the build system from noticing that you're running a `zig test`, so the build runner and test runner do not communicate over stdio. However, that's okay, because the real-world use cases for this feature don't want to do that anyway! Resolves: #23374
This commit is contained in:
parent
6a7ca4b8b0
commit
927f233ff8
@ -1019,6 +1019,10 @@ pub const TestOptions = struct {
|
|||||||
use_llvm: ?bool = null,
|
use_llvm: ?bool = null,
|
||||||
use_lld: ?bool = null,
|
use_lld: ?bool = null,
|
||||||
zig_lib_dir: ?LazyPath = null,
|
zig_lib_dir: ?LazyPath = null,
|
||||||
|
/// Emits an object file instead of a test binary.
|
||||||
|
/// The object must be linked separately.
|
||||||
|
/// Usually used in conjunction with a custom `test_runner`.
|
||||||
|
emit_object: bool = false,
|
||||||
|
|
||||||
/// Prefer populating this field (using e.g. `createModule`) instead of populating
|
/// Prefer populating this field (using e.g. `createModule`) instead of populating
|
||||||
/// the following fields (`root_source_file` etc). In a future release, those fields
|
/// the following fields (`root_source_file` etc). In a future release, those fields
|
||||||
@ -1067,7 +1071,7 @@ pub fn addTest(b: *Build, options: TestOptions) *Step.Compile {
|
|||||||
}
|
}
|
||||||
return .create(b, .{
|
return .create(b, .{
|
||||||
.name = options.name,
|
.name = options.name,
|
||||||
.kind = .@"test",
|
.kind = if (options.emit_object) .test_obj else .@"test",
|
||||||
.root_module = options.root_module orelse b.createModule(.{
|
.root_module = options.root_module orelse b.createModule(.{
|
||||||
.root_source_file = options.root_source_file orelse @panic("`root_module` and `root_source_file` cannot both be null"),
|
.root_source_file = options.root_source_file orelse @panic("`root_module` and `root_source_file` cannot both be null"),
|
||||||
.target = options.target orelse b.graph.host,
|
.target = options.target orelse b.graph.host,
|
||||||
|
|||||||
@ -474,7 +474,7 @@ pub fn addObjectFile(m: *Module, object: LazyPath) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn addObject(m: *Module, object: *Step.Compile) void {
|
pub fn addObject(m: *Module, object: *Step.Compile) void {
|
||||||
assert(object.kind == .obj);
|
assert(object.kind == .obj or object.kind == .test_obj);
|
||||||
m.linkLibraryOrObject(object);
|
m.linkLibraryOrObject(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -293,6 +293,7 @@ pub const Kind = enum {
|
|||||||
lib,
|
lib,
|
||||||
obj,
|
obj,
|
||||||
@"test",
|
@"test",
|
||||||
|
test_obj,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const HeaderInstallation = union(enum) {
|
pub const HeaderInstallation = union(enum) {
|
||||||
@ -370,7 +371,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Avoid the common case of the step name looking like "zig test test".
|
// Avoid the common case of the step name looking like "zig test test".
|
||||||
const name_adjusted = if (options.kind == .@"test" and mem.eql(u8, name, "test"))
|
const name_adjusted = if ((options.kind == .@"test" or options.kind == .test_obj) and mem.eql(u8, name, "test"))
|
||||||
""
|
""
|
||||||
else
|
else
|
||||||
owner.fmt("{s} ", .{name});
|
owner.fmt("{s} ", .{name});
|
||||||
@ -385,6 +386,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
|
|||||||
.lib => "zig build-lib",
|
.lib => "zig build-lib",
|
||||||
.obj => "zig build-obj",
|
.obj => "zig build-obj",
|
||||||
.@"test" => "zig test",
|
.@"test" => "zig test",
|
||||||
|
.test_obj => "zig test-obj",
|
||||||
},
|
},
|
||||||
name_adjusted,
|
name_adjusted,
|
||||||
@tagName(options.root_module.optimize orelse .Debug),
|
@tagName(options.root_module.optimize orelse .Debug),
|
||||||
@ -396,7 +398,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile {
|
|||||||
.target = target,
|
.target = target,
|
||||||
.output_mode = switch (options.kind) {
|
.output_mode = switch (options.kind) {
|
||||||
.lib => .Lib,
|
.lib => .Lib,
|
||||||
.obj => .Obj,
|
.obj, .test_obj => .Obj,
|
||||||
.exe, .@"test" => .Exe,
|
.exe, .@"test" => .Exe,
|
||||||
},
|
},
|
||||||
.link_mode = options.linkage,
|
.link_mode = options.linkage,
|
||||||
@ -1053,6 +1055,7 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
|
|||||||
.exe => "build-exe",
|
.exe => "build-exe",
|
||||||
.obj => "build-obj",
|
.obj => "build-obj",
|
||||||
.@"test" => "test",
|
.@"test" => "test",
|
||||||
|
.test_obj => "test-obj",
|
||||||
};
|
};
|
||||||
try zig_args.append(cmd);
|
try zig_args.append(cmd);
|
||||||
|
|
||||||
@ -1222,9 +1225,9 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
|
|||||||
switch (other.kind) {
|
switch (other.kind) {
|
||||||
.exe => return step.fail("cannot link with an executable build artifact", .{}),
|
.exe => return step.fail("cannot link with an executable build artifact", .{}),
|
||||||
.@"test" => return step.fail("cannot link with a test", .{}),
|
.@"test" => return step.fail("cannot link with a test", .{}),
|
||||||
.obj => {
|
.obj, .test_obj => {
|
||||||
const included_in_lib_or_obj = !my_responsibility and
|
const included_in_lib_or_obj = !my_responsibility and
|
||||||
(dep_compile.kind == .lib or dep_compile.kind == .obj);
|
(dep_compile.kind == .lib or dep_compile.kind == .obj or dep_compile.kind == .test_obj);
|
||||||
if (!already_linked and !included_in_lib_or_obj) {
|
if (!already_linked and !included_in_lib_or_obj) {
|
||||||
try zig_args.append(other.getEmittedBin().getPath2(b, step));
|
try zig_args.append(other.getEmittedBin().getPath2(b, step));
|
||||||
total_linker_objects += 1;
|
total_linker_objects += 1;
|
||||||
|
|||||||
@ -56,7 +56,7 @@ pub fn create(owner: *std.Build, artifact: *Step.Compile, options: Options) *Ins
|
|||||||
const dest_dir: ?InstallDir = switch (options.dest_dir) {
|
const dest_dir: ?InstallDir = switch (options.dest_dir) {
|
||||||
.disabled => null,
|
.disabled => null,
|
||||||
.default => switch (artifact.kind) {
|
.default => switch (artifact.kind) {
|
||||||
.obj => @panic("object files have no standard installation procedure"),
|
.obj, .test_obj => @panic("object files have no standard installation procedure"),
|
||||||
.exe, .@"test" => .bin,
|
.exe, .@"test" => .bin,
|
||||||
.lib => if (artifact.isDll()) .bin else .lib,
|
.lib => if (artifact.isDll()) .bin else .lib,
|
||||||
},
|
},
|
||||||
|
|||||||
87
src/main.zig
87
src/main.zig
@ -278,6 +278,9 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
|
|||||||
} else if (mem.eql(u8, cmd, "test")) {
|
} else if (mem.eql(u8, cmd, "test")) {
|
||||||
dev.check(.test_command);
|
dev.check(.test_command);
|
||||||
return buildOutputType(gpa, arena, args, .zig_test);
|
return buildOutputType(gpa, arena, args, .zig_test);
|
||||||
|
} else if (mem.eql(u8, cmd, "test-obj")) {
|
||||||
|
dev.check(.test_command);
|
||||||
|
return buildOutputType(gpa, arena, args, .zig_test_obj);
|
||||||
} else if (mem.eql(u8, cmd, "run")) {
|
} else if (mem.eql(u8, cmd, "run")) {
|
||||||
dev.check(.run_command);
|
dev.check(.run_command);
|
||||||
return buildOutputType(gpa, arena, args, .run);
|
return buildOutputType(gpa, arena, args, .run);
|
||||||
@ -764,6 +767,7 @@ const ArgMode = union(enum) {
|
|||||||
cpp,
|
cpp,
|
||||||
translate_c,
|
translate_c,
|
||||||
zig_test,
|
zig_test,
|
||||||
|
zig_test_obj,
|
||||||
run,
|
run,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -975,7 +979,10 @@ fn buildOutputType(
|
|||||||
.dynamic_linker = null,
|
.dynamic_linker = null,
|
||||||
.modules = .{},
|
.modules = .{},
|
||||||
.opts = .{
|
.opts = .{
|
||||||
.is_test = arg_mode == .zig_test,
|
.is_test = switch (arg_mode) {
|
||||||
|
.zig_test, .zig_test_obj => true,
|
||||||
|
.build, .cc, .cpp, .translate_c, .run => false,
|
||||||
|
},
|
||||||
// Populated while parsing CLI args.
|
// Populated while parsing CLI args.
|
||||||
.output_mode = undefined,
|
.output_mode = undefined,
|
||||||
// Populated in the call to `createModule` for the root module.
|
// Populated in the call to `createModule` for the root module.
|
||||||
@ -1030,7 +1037,7 @@ fn buildOutputType(
|
|||||||
var n_jobs: ?u32 = null;
|
var n_jobs: ?u32 = null;
|
||||||
|
|
||||||
switch (arg_mode) {
|
switch (arg_mode) {
|
||||||
.build, .translate_c, .zig_test, .run => {
|
.build, .translate_c, .zig_test, .zig_test_obj, .run => {
|
||||||
switch (arg_mode) {
|
switch (arg_mode) {
|
||||||
.build => |m| {
|
.build => |m| {
|
||||||
create_module.opts.output_mode = m;
|
create_module.opts.output_mode = m;
|
||||||
@ -1042,6 +1049,9 @@ fn buildOutputType(
|
|||||||
.zig_test, .run => {
|
.zig_test, .run => {
|
||||||
create_module.opts.output_mode = .Exe;
|
create_module.opts.output_mode = .Exe;
|
||||||
},
|
},
|
||||||
|
.zig_test_obj => {
|
||||||
|
create_module.opts.output_mode = .Obj;
|
||||||
|
},
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2834,6 +2844,10 @@ fn buildOutputType(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (arg_mode == .zig_test_obj and !test_no_exec and listen == .none) {
|
||||||
|
fatal("test-obj requires --test-no-exec", .{});
|
||||||
|
}
|
||||||
|
|
||||||
if (arg_mode == .translate_c and create_module.c_source_files.items.len != 1) {
|
if (arg_mode == .translate_c and create_module.c_source_files.items.len != 1) {
|
||||||
fatal("translate-c expects exactly 1 source file (found {d})", .{create_module.c_source_files.items.len});
|
fatal("translate-c expects exactly 1 source file (found {d})", .{create_module.c_source_files.items.len});
|
||||||
}
|
}
|
||||||
@ -2903,10 +2917,10 @@ fn buildOutputType(
|
|||||||
create_module.opts.any_error_tracing = true;
|
create_module.opts.any_error_tracing = true;
|
||||||
|
|
||||||
const src_path = try introspect.resolvePath(arena, unresolved_src_path);
|
const src_path = try introspect.resolvePath(arena, unresolved_src_path);
|
||||||
const name = if (arg_mode == .zig_test)
|
const name = switch (arg_mode) {
|
||||||
"test"
|
.zig_test => "test",
|
||||||
else
|
.build, .cc, .cpp, .translate_c, .zig_test_obj, .run => fs.path.stem(fs.path.basename(src_path)),
|
||||||
fs.path.stem(fs.path.basename(src_path));
|
};
|
||||||
|
|
||||||
try create_module.modules.put(arena, name, .{
|
try create_module.modules.put(arena, name, .{
|
||||||
.paths = .{
|
.paths = .{
|
||||||
@ -2935,7 +2949,7 @@ fn buildOutputType(
|
|||||||
rc_source_files_owner_index = create_module.rc_source_files.items.len;
|
rc_source_files_owner_index = create_module.rc_source_files.items.len;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!create_module.opts.have_zcu and arg_mode == .zig_test) {
|
if (!create_module.opts.have_zcu and create_module.opts.is_test) {
|
||||||
fatal("`zig test` expects a zig source file argument", .{});
|
fatal("`zig test` expects a zig source file argument", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3037,16 +3051,36 @@ fn buildOutputType(
|
|||||||
break :m null;
|
break :m null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const root_mod = if (arg_mode == .zig_test) root_mod: {
|
const root_mod = switch (arg_mode) {
|
||||||
const test_mod = if (test_runner_path) |test_runner| test_mod: {
|
.zig_test, .zig_test_obj => root_mod: {
|
||||||
const test_mod = try Package.Module.create(arena, .{
|
const test_mod = if (test_runner_path) |test_runner| test_mod: {
|
||||||
|
const test_mod = try Package.Module.create(arena, .{
|
||||||
|
.global_cache_directory = global_cache_directory,
|
||||||
|
.paths = .{
|
||||||
|
.root = .{
|
||||||
|
.root_dir = Cache.Directory.cwd(),
|
||||||
|
.sub_path = fs.path.dirname(test_runner) orelse "",
|
||||||
|
},
|
||||||
|
.root_src_path = fs.path.basename(test_runner),
|
||||||
|
},
|
||||||
|
.fully_qualified_name = "root",
|
||||||
|
.cc_argv = &.{},
|
||||||
|
.inherited = .{},
|
||||||
|
.global = create_module.resolved_options,
|
||||||
|
.parent = main_mod,
|
||||||
|
.builtin_mod = main_mod.getBuiltinDependency(),
|
||||||
|
.builtin_modules = null, // `builtin_mod` is specified
|
||||||
|
});
|
||||||
|
test_mod.deps = try main_mod.deps.clone(arena);
|
||||||
|
break :test_mod test_mod;
|
||||||
|
} else try Package.Module.create(arena, .{
|
||||||
.global_cache_directory = global_cache_directory,
|
.global_cache_directory = global_cache_directory,
|
||||||
.paths = .{
|
.paths = .{
|
||||||
.root = .{
|
.root = .{
|
||||||
.root_dir = Cache.Directory.cwd(),
|
.root_dir = zig_lib_directory,
|
||||||
.sub_path = fs.path.dirname(test_runner) orelse "",
|
.sub_path = "compiler",
|
||||||
},
|
},
|
||||||
.root_src_path = fs.path.basename(test_runner),
|
.root_src_path = "test_runner.zig",
|
||||||
},
|
},
|
||||||
.fully_qualified_name = "root",
|
.fully_qualified_name = "root",
|
||||||
.cc_argv = &.{},
|
.cc_argv = &.{},
|
||||||
@ -3056,28 +3090,11 @@ fn buildOutputType(
|
|||||||
.builtin_mod = main_mod.getBuiltinDependency(),
|
.builtin_mod = main_mod.getBuiltinDependency(),
|
||||||
.builtin_modules = null, // `builtin_mod` is specified
|
.builtin_modules = null, // `builtin_mod` is specified
|
||||||
});
|
});
|
||||||
test_mod.deps = try main_mod.deps.clone(arena);
|
|
||||||
break :test_mod test_mod;
|
|
||||||
} else try Package.Module.create(arena, .{
|
|
||||||
.global_cache_directory = global_cache_directory,
|
|
||||||
.paths = .{
|
|
||||||
.root = .{
|
|
||||||
.root_dir = zig_lib_directory,
|
|
||||||
.sub_path = "compiler",
|
|
||||||
},
|
|
||||||
.root_src_path = "test_runner.zig",
|
|
||||||
},
|
|
||||||
.fully_qualified_name = "root",
|
|
||||||
.cc_argv = &.{},
|
|
||||||
.inherited = .{},
|
|
||||||
.global = create_module.resolved_options,
|
|
||||||
.parent = main_mod,
|
|
||||||
.builtin_mod = main_mod.getBuiltinDependency(),
|
|
||||||
.builtin_modules = null, // `builtin_mod` is specified
|
|
||||||
});
|
|
||||||
|
|
||||||
break :root_mod test_mod;
|
break :root_mod test_mod;
|
||||||
} else main_mod;
|
},
|
||||||
|
else => main_mod,
|
||||||
|
};
|
||||||
|
|
||||||
const target = main_mod.resolved_target.result;
|
const target = main_mod.resolved_target.result;
|
||||||
|
|
||||||
@ -3202,7 +3219,7 @@ fn buildOutputType(
|
|||||||
.directory = blk: {
|
.directory = blk: {
|
||||||
switch (arg_mode) {
|
switch (arg_mode) {
|
||||||
.run, .zig_test => break :blk null,
|
.run, .zig_test => break :blk null,
|
||||||
else => {
|
.build, .cc, .cpp, .translate_c, .zig_test_obj => {
|
||||||
if (output_to_cache) {
|
if (output_to_cache) {
|
||||||
break :blk null;
|
break :blk null;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -5,6 +5,9 @@
|
|||||||
.simple = .{
|
.simple = .{
|
||||||
.path = "simple",
|
.path = "simple",
|
||||||
},
|
},
|
||||||
|
.test_obj_link_run = .{
|
||||||
|
.path = "test_obj_link_run",
|
||||||
|
},
|
||||||
.test_runner_path = .{
|
.test_runner_path = .{
|
||||||
.path = "test_runner_path",
|
.path = "test_runner_path",
|
||||||
},
|
},
|
||||||
|
|||||||
32
test/standalone/test_obj_link_run/build.zig
Normal file
32
test/standalone/test_obj_link_run/build.zig
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
pub fn build(b: *std.Build) void {
|
||||||
|
// To avoid having to explicitly link required system libraries into the final test
|
||||||
|
// executable (e.g. ntdll on Windows), we'll just link everything with libc here.
|
||||||
|
|
||||||
|
const test_obj = b.addTest(.{
|
||||||
|
.emit_object = true,
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/main.zig"),
|
||||||
|
.target = b.graph.host,
|
||||||
|
.link_libc = true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const test_exe_mod = b.createModule(.{
|
||||||
|
.root_source_file = null,
|
||||||
|
.target = b.graph.host,
|
||||||
|
.link_libc = true,
|
||||||
|
});
|
||||||
|
test_exe_mod.addObject(test_obj);
|
||||||
|
const test_exe = b.addExecutable(.{
|
||||||
|
.name = "test",
|
||||||
|
.root_module = test_exe_mod,
|
||||||
|
});
|
||||||
|
|
||||||
|
const test_step = b.step("test", "Test the program");
|
||||||
|
b.default_step = test_step;
|
||||||
|
|
||||||
|
const test_run = b.addRunArtifact(test_exe);
|
||||||
|
test_step.dependOn(&test_run.step);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
17
test/standalone/test_obj_link_run/src/main.zig
Normal file
17
test/standalone/test_obj_link_run/src/main.zig
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
test {
|
||||||
|
try std.testing.expect(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "equality" {
|
||||||
|
try std.testing.expect(one() == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "arithmetic" {
|
||||||
|
try std.testing.expect(one() + 2 == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn one() u32 {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
Loading…
x
Reference in New Issue
Block a user