| author | |
| committer | |
| log | 927f233ff82bd422b387b4eef7fcbc67823e4b85 |
| tree | b921686a62e6e60efa6db973488d9fffb93c8ebb |
| parent | 6a7ca4b8b0fcce9e5f6a4d3f799e83021929c975 |
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: #233748 files changed, 118 insertions(+), 42 deletions(-)
lib/std/Build.zig+5-1| ... | ... | @@ -1019,6 +1019,10 @@ pub const TestOptions = struct { |
| 1019 | 1019 | use_llvm: ?bool = null, |
| 1020 | 1020 | use_lld: ?bool = null, |
| 1021 | 1021 | zig_lib_dir: ?LazyPath = null, |
| 1022 | /// Emits an object file instead of a test binary. | |
| 1023 | /// The object must be linked separately. | |
| 1024 | /// Usually used in conjunction with a custom `test_runner`. | |
| 1025 | emit_object: bool = false, | |
| 1022 | 1026 | |
| 1023 | 1027 | /// Prefer populating this field (using e.g. `createModule`) instead of populating |
| 1024 | 1028 | /// 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 { |
| 1067 | 1071 | } |
| 1068 | 1072 | return .create(b, .{ |
| 1069 | 1073 | .name = options.name, |
| 1070 | .kind = .@"test", | |
| 1074 | .kind = if (options.emit_object) .test_obj else .@"test", | |
| 1071 | 1075 | .root_module = options.root_module orelse b.createModule(.{ |
| 1072 | 1076 | .root_source_file = options.root_source_file orelse @panic("`root_module` and `root_source_file` cannot both be null"), |
| 1073 | 1077 | .target = options.target orelse b.graph.host, |
lib/std/Build/Module.zig+1-1| ... | ... | @@ -474,7 +474,7 @@ pub fn addObjectFile(m: *Module, object: LazyPath) void { |
| 474 | 474 | } |
| 475 | 475 | |
| 476 | 476 | pub fn addObject(m: *Module, object: *Step.Compile) void { |
| 477 | assert(object.kind == .obj); | |
| 477 | assert(object.kind == .obj or object.kind == .test_obj); | |
| 478 | 478 | m.linkLibraryOrObject(object); |
| 479 | 479 | } |
| 480 | 480 |
lib/std/Build/Step/Compile.zig+7-4| ... | ... | @@ -293,6 +293,7 @@ pub const Kind = enum { |
| 293 | 293 | lib, |
| 294 | 294 | obj, |
| 295 | 295 | @"test", |
| 296 | test_obj, | |
| 296 | 297 | }; |
| 297 | 298 | |
| 298 | 299 | pub const HeaderInstallation = union(enum) { |
| ... | ... | @@ -370,7 +371,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile { |
| 370 | 371 | } |
| 371 | 372 | |
| 372 | 373 | // Avoid the common case of the step name looking like "zig test test". |
| 373 | const name_adjusted = if (options.kind == .@"test" and mem.eql(u8, name, "test")) | |
| 374 | const name_adjusted = if ((options.kind == .@"test" or options.kind == .test_obj) and mem.eql(u8, name, "test")) | |
| 374 | 375 | "" |
| 375 | 376 | else |
| 376 | 377 | owner.fmt("{s} ", .{name}); |
| ... | ... | @@ -385,6 +386,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile { |
| 385 | 386 | .lib => "zig build-lib", |
| 386 | 387 | .obj => "zig build-obj", |
| 387 | 388 | .@"test" => "zig test", |
| 389 | .test_obj => "zig test-obj", | |
| 388 | 390 | }, |
| 389 | 391 | name_adjusted, |
| 390 | 392 | @tagName(options.root_module.optimize orelse .Debug), |
| ... | ... | @@ -396,7 +398,7 @@ pub fn create(owner: *std.Build, options: Options) *Compile { |
| 396 | 398 | .target = target, |
| 397 | 399 | .output_mode = switch (options.kind) { |
| 398 | 400 | .lib => .Lib, |
| 399 | .obj => .Obj, | |
| 401 | .obj, .test_obj => .Obj, | |
| 400 | 402 | .exe, .@"test" => .Exe, |
| 401 | 403 | }, |
| 402 | 404 | .link_mode = options.linkage, |
| ... | ... | @@ -1053,6 +1055,7 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { |
| 1053 | 1055 | .exe => "build-exe", |
| 1054 | 1056 | .obj => "build-obj", |
| 1055 | 1057 | .@"test" => "test", |
| 1058 | .test_obj => "test-obj", | |
| 1056 | 1059 | }; |
| 1057 | 1060 | try zig_args.append(cmd); |
| 1058 | 1061 | |
| ... | ... | @@ -1222,9 +1225,9 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { |
| 1222 | 1225 | switch (other.kind) { |
| 1223 | 1226 | .exe => return step.fail("cannot link with an executable build artifact", .{}), |
| 1224 | 1227 | .@"test" => return step.fail("cannot link with a test", .{}), |
| 1225 | .obj => { | |
| 1228 | .obj, .test_obj => { | |
| 1226 | 1229 | const included_in_lib_or_obj = !my_responsibility and |
| 1227 | (dep_compile.kind == .lib or dep_compile.kind == .obj); | |
| 1230 | (dep_compile.kind == .lib or dep_compile.kind == .obj or dep_compile.kind == .test_obj); | |
| 1228 | 1231 | if (!already_linked and !included_in_lib_or_obj) { |
| 1229 | 1232 | try zig_args.append(other.getEmittedBin().getPath2(b, step)); |
| 1230 | 1233 | total_linker_objects += 1; |
lib/std/Build/Step/InstallArtifact.zig+1-1| ... | ... | @@ -56,7 +56,7 @@ pub fn create(owner: *std.Build, artifact: *Step.Compile, options: Options) *Ins |
| 56 | 56 | const dest_dir: ?InstallDir = switch (options.dest_dir) { |
| 57 | 57 | .disabled => null, |
| 58 | 58 | .default => switch (artifact.kind) { |
| 59 | .obj => @panic("object files have no standard installation procedure"), | |
| 59 | .obj, .test_obj => @panic("object files have no standard installation procedure"), | |
| 60 | 60 | .exe, .@"test" => .bin, |
| 61 | 61 | .lib => if (artifact.isDll()) .bin else .lib, |
| 62 | 62 | }, |
src/main.zig+52-35| ... | ... | @@ -278,6 +278,9 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { |
| 278 | 278 | } else if (mem.eql(u8, cmd, "test")) { |
| 279 | 279 | dev.check(.test_command); |
| 280 | 280 | return buildOutputType(gpa, arena, args, .zig_test); |
| 281 | } else if (mem.eql(u8, cmd, "test-obj")) { | |
| 282 | dev.check(.test_command); | |
| 283 | return buildOutputType(gpa, arena, args, .zig_test_obj); | |
| 281 | 284 | } else if (mem.eql(u8, cmd, "run")) { |
| 282 | 285 | dev.check(.run_command); |
| 283 | 286 | return buildOutputType(gpa, arena, args, .run); |
| ... | ... | @@ -764,6 +767,7 @@ const ArgMode = union(enum) { |
| 764 | 767 | cpp, |
| 765 | 768 | translate_c, |
| 766 | 769 | zig_test, |
| 770 | zig_test_obj, | |
| 767 | 771 | run, |
| 768 | 772 | }; |
| 769 | 773 | |
| ... | ... | @@ -975,7 +979,10 @@ fn buildOutputType( |
| 975 | 979 | .dynamic_linker = null, |
| 976 | 980 | .modules = .{}, |
| 977 | 981 | .opts = .{ |
| 978 | .is_test = arg_mode == .zig_test, | |
| 982 | .is_test = switch (arg_mode) { | |
| 983 | .zig_test, .zig_test_obj => true, | |
| 984 | .build, .cc, .cpp, .translate_c, .run => false, | |
| 985 | }, | |
| 979 | 986 | // Populated while parsing CLI args. |
| 980 | 987 | .output_mode = undefined, |
| 981 | 988 | // Populated in the call to `createModule` for the root module. |
| ... | ... | @@ -1030,7 +1037,7 @@ fn buildOutputType( |
| 1030 | 1037 | var n_jobs: ?u32 = null; |
| 1031 | 1038 | |
| 1032 | 1039 | switch (arg_mode) { |
| 1033 | .build, .translate_c, .zig_test, .run => { | |
| 1040 | .build, .translate_c, .zig_test, .zig_test_obj, .run => { | |
| 1034 | 1041 | switch (arg_mode) { |
| 1035 | 1042 | .build => |m| { |
| 1036 | 1043 | create_module.opts.output_mode = m; |
| ... | ... | @@ -1042,6 +1049,9 @@ fn buildOutputType( |
| 1042 | 1049 | .zig_test, .run => { |
| 1043 | 1050 | create_module.opts.output_mode = .Exe; |
| 1044 | 1051 | }, |
| 1052 | .zig_test_obj => { | |
| 1053 | create_module.opts.output_mode = .Obj; | |
| 1054 | }, | |
| 1045 | 1055 | else => unreachable, |
| 1046 | 1056 | } |
| 1047 | 1057 | |
| ... | ... | @@ -2834,6 +2844,10 @@ fn buildOutputType( |
| 2834 | 2844 | }, |
| 2835 | 2845 | } |
| 2836 | 2846 | |
| 2847 | if (arg_mode == .zig_test_obj and !test_no_exec and listen == .none) { | |
| 2848 | fatal("test-obj requires --test-no-exec", .{}); | |
| 2849 | } | |
| 2850 | ||
| 2837 | 2851 | if (arg_mode == .translate_c and create_module.c_source_files.items.len != 1) { |
| 2838 | 2852 | fatal("translate-c expects exactly 1 source file (found {d})", .{create_module.c_source_files.items.len}); |
| 2839 | 2853 | } |
| ... | ... | @@ -2903,10 +2917,10 @@ fn buildOutputType( |
| 2903 | 2917 | create_module.opts.any_error_tracing = true; |
| 2904 | 2918 | |
| 2905 | 2919 | const src_path = try introspect.resolvePath(arena, unresolved_src_path); |
| 2906 | const name = if (arg_mode == .zig_test) | |
| 2907 | "test" | |
| 2908 | else | |
| 2909 | fs.path.stem(fs.path.basename(src_path)); | |
| 2920 | const name = switch (arg_mode) { | |
| 2921 | .zig_test => "test", | |
| 2922 | .build, .cc, .cpp, .translate_c, .zig_test_obj, .run => fs.path.stem(fs.path.basename(src_path)), | |
| 2923 | }; | |
| 2910 | 2924 | |
| 2911 | 2925 | try create_module.modules.put(arena, name, .{ |
| 2912 | 2926 | .paths = .{ |
| ... | ... | @@ -2935,7 +2949,7 @@ fn buildOutputType( |
| 2935 | 2949 | rc_source_files_owner_index = create_module.rc_source_files.items.len; |
| 2936 | 2950 | } |
| 2937 | 2951 | |
| 2938 | if (!create_module.opts.have_zcu and arg_mode == .zig_test) { | |
| 2952 | if (!create_module.opts.have_zcu and create_module.opts.is_test) { | |
| 2939 | 2953 | fatal("`zig test` expects a zig source file argument", .{}); |
| 2940 | 2954 | } |
| 2941 | 2955 | |
| ... | ... | @@ -3037,16 +3051,36 @@ fn buildOutputType( |
| 3037 | 3051 | break :m null; |
| 3038 | 3052 | }; |
| 3039 | 3053 | |
| 3040 | const root_mod = if (arg_mode == .zig_test) root_mod: { | |
| 3041 | const test_mod = if (test_runner_path) |test_runner| test_mod: { | |
| 3042 | const test_mod = try Package.Module.create(arena, .{ | |
| 3054 | const root_mod = switch (arg_mode) { | |
| 3055 | .zig_test, .zig_test_obj => root_mod: { | |
| 3056 | const test_mod = if (test_runner_path) |test_runner| test_mod: { | |
| 3057 | const test_mod = try Package.Module.create(arena, .{ | |
| 3058 | .global_cache_directory = global_cache_directory, | |
| 3059 | .paths = .{ | |
| 3060 | .root = .{ | |
| 3061 | .root_dir = Cache.Directory.cwd(), | |
| 3062 | .sub_path = fs.path.dirname(test_runner) orelse "", | |
| 3063 | }, | |
| 3064 | .root_src_path = fs.path.basename(test_runner), | |
| 3065 | }, | |
| 3066 | .fully_qualified_name = "root", | |
| 3067 | .cc_argv = &.{}, | |
| 3068 | .inherited = .{}, | |
| 3069 | .global = create_module.resolved_options, | |
| 3070 | .parent = main_mod, | |
| 3071 | .builtin_mod = main_mod.getBuiltinDependency(), | |
| 3072 | .builtin_modules = null, // `builtin_mod` is specified | |
| 3073 | }); | |
| 3074 | test_mod.deps = try main_mod.deps.clone(arena); | |
| 3075 | break :test_mod test_mod; | |
| 3076 | } else try Package.Module.create(arena, .{ | |
| 3043 | 3077 | .global_cache_directory = global_cache_directory, |
| 3044 | 3078 | .paths = .{ |
| 3045 | 3079 | .root = .{ |
| 3046 | .root_dir = Cache.Directory.cwd(), | |
| 3047 | .sub_path = fs.path.dirname(test_runner) orelse "", | |
| 3080 | .root_dir = zig_lib_directory, | |
| 3081 | .sub_path = "compiler", | |
| 3048 | 3082 | }, |
| 3049 | .root_src_path = fs.path.basename(test_runner), | |
| 3083 | .root_src_path = "test_runner.zig", | |
| 3050 | 3084 | }, |
| 3051 | 3085 | .fully_qualified_name = "root", |
| 3052 | 3086 | .cc_argv = &.{}, |
| ... | ... | @@ -3056,28 +3090,11 @@ fn buildOutputType( |
| 3056 | 3090 | .builtin_mod = main_mod.getBuiltinDependency(), |
| 3057 | 3091 | .builtin_modules = null, // `builtin_mod` is specified |
| 3058 | 3092 | }); |
| 3059 | test_mod.deps = try main_mod.deps.clone(arena); | |
| 3060 | break :test_mod test_mod; | |
| 3061 | } else try Package.Module.create(arena, .{ | |
| 3062 | .global_cache_directory = global_cache_directory, | |
| 3063 | .paths = .{ | |
| 3064 | .root = .{ | |
| 3065 | .root_dir = zig_lib_directory, | |
| 3066 | .sub_path = "compiler", | |
| 3067 | }, | |
| 3068 | .root_src_path = "test_runner.zig", | |
| 3069 | }, | |
| 3070 | .fully_qualified_name = "root", | |
| 3071 | .cc_argv = &.{}, | |
| 3072 | .inherited = .{}, | |
| 3073 | .global = create_module.resolved_options, | |
| 3074 | .parent = main_mod, | |
| 3075 | .builtin_mod = main_mod.getBuiltinDependency(), | |
| 3076 | .builtin_modules = null, // `builtin_mod` is specified | |
| 3077 | }); | |
| 3078 | 3093 | |
| 3079 | break :root_mod test_mod; | |
| 3080 | } else main_mod; | |
| 3094 | break :root_mod test_mod; | |
| 3095 | }, | |
| 3096 | else => main_mod, | |
| 3097 | }; | |
| 3081 | 3098 | |
| 3082 | 3099 | const target = main_mod.resolved_target.result; |
| 3083 | 3100 | |
| ... | ... | @@ -3202,7 +3219,7 @@ fn buildOutputType( |
| 3202 | 3219 | .directory = blk: { |
| 3203 | 3220 | switch (arg_mode) { |
| 3204 | 3221 | .run, .zig_test => break :blk null, |
| 3205 | else => { | |
| 3222 | .build, .cc, .cpp, .translate_c, .zig_test_obj => { | |
| 3206 | 3223 | if (output_to_cache) { |
| 3207 | 3224 | break :blk null; |
| 3208 | 3225 | } else { |
test/standalone/build.zig.zon+3| ... | ... | @@ -5,6 +5,9 @@ |
| 5 | 5 | .simple = .{ |
| 6 | 6 | .path = "simple", |
| 7 | 7 | }, |
| 8 | .test_obj_link_run = .{ | |
| 9 | .path = "test_obj_link_run", | |
| 10 | }, | |
| 8 | 11 | .test_runner_path = .{ |
| 9 | 12 | .path = "test_runner_path", |
| 10 | 13 | }, |
test/standalone/test_obj_link_run/build.zig created+32| ... | ... | @@ -0,0 +1,32 @@ |
| 1 | pub fn build(b: *std.Build) void { | |
| 2 | // To avoid having to explicitly link required system libraries into the final test | |
| 3 | // executable (e.g. ntdll on Windows), we'll just link everything with libc here. | |
| 4 | ||
| 5 | const test_obj = b.addTest(.{ | |
| 6 | .emit_object = true, | |
| 7 | .root_module = b.createModule(.{ | |
| 8 | .root_source_file = b.path("src/main.zig"), | |
| 9 | .target = b.graph.host, | |
| 10 | .link_libc = true, | |
| 11 | }), | |
| 12 | }); | |
| 13 | ||
| 14 | const test_exe_mod = b.createModule(.{ | |
| 15 | .root_source_file = null, | |
| 16 | .target = b.graph.host, | |
| 17 | .link_libc = true, | |
| 18 | }); | |
| 19 | test_exe_mod.addObject(test_obj); | |
| 20 | const test_exe = b.addExecutable(.{ | |
| 21 | .name = "test", | |
| 22 | .root_module = test_exe_mod, | |
| 23 | }); | |
| 24 | ||
| 25 | const test_step = b.step("test", "Test the program"); | |
| 26 | b.default_step = test_step; | |
| 27 | ||
| 28 | const test_run = b.addRunArtifact(test_exe); | |
| 29 | test_step.dependOn(&test_run.step); | |
| 30 | } | |
| 31 | ||
| 32 | const std = @import("std"); |
test/standalone/test_obj_link_run/src/main.zig created+17| ... | ... | @@ -0,0 +1,17 @@ |
| 1 | test { | |
| 2 | try std.testing.expect(true); | |
| 3 | } | |
| 4 | ||
| 5 | test "equality" { | |
| 6 | try std.testing.expect(one() == 1); | |
| 7 | } | |
| 8 | ||
| 9 | test "arithmetic" { | |
| 10 | try std.testing.expect(one() + 2 == 3); | |
| 11 | } | |
| 12 | ||
| 13 | fn one() u32 { | |
| 14 | return 1; | |
| 15 | } | |
| 16 | ||
| 17 | const std = @import("std"); |