lib/choir/build/tests.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const build_api = @import("build_api");
3 const deps = @import("deps.zig");
4 const module = @import("module.zig");
5 const cfg_evidence_build_source = @embedFile("tests.zig");
6 const cfg_evidence_producer_source = @embedFile("../src/properties/evidence.zig");
7
8 pub const Graph = struct {
9 step: *std.Build.Step,
10 };
11
12 pub fn addUnit(
13 b: *std.Build,
14 configuration: deps.Configuration,
15 choir: *std.Build.Module,
16 abi: *std.Build.Module,
17 ) Graph {
18 const unit_tests = b.addTest(.{
19 .root_module = choir,
20 .filters = configuration.test_filters,
21 });
22 const abi_tests = b.addTest(.{
23 .root_module = abi,
24 .filters = configuration.test_filters,
25 });
26 const run_unit_tests = b.addRunArtifact(unit_tests);
27 const run_abi_tests = b.addRunArtifact(abi_tests);
28 const step = b.step("test", "Run Choir unit tests");
29 step.dependOn(&run_unit_tests.step);
30 step.dependOn(&run_abi_tests.step);
31 return .{ .step = step };
32 }
33
34 pub fn addRewriteObservations(
35 b: *std.Build,
36 options: build_api.ModuleOptions,
37 choir_tests: *std.Build.Module,
38 ) void {
39 const generator = b.addExecutable(.{
40 .name = "choir-update-rewrite-observations",
41 .root_module = b.createModule(options.createOptions(
42 b,
43 "build/observations.zig",
44 &.{.{ .name = "choir_tests", .module = choir_tests }},
45 )),
46 });
47 const run = b.addRunArtifact(generator);
48 run.setCwd(b.path("src/passes/observations/fixture"));
49 run.has_side_effects = true;
50 const step = b.step(
51 "update-rewrite-observations",
52 "Regenerate the four retained rewrite observations; review their diff",
53 );
54 step.dependOn(&run.step);
55 }
56
57 /// Builds the SPIR-V validation gate for the stage emitters under `options`,
58 /// which the gate's timings need to be optimized. The gate runs the spirv-val
59 /// that `-Dspirv-val` names and fails without one.
60 pub fn addStageValidation(
61 b: *std.Build,
62 options: build_api.ModuleOptions,
63 inputs: module.Inputs,
64 ) void {
65 const spirv_val = b.option(
66 []const u8,
67 "spirv-val",
68 "The spirv-val binary the stage validation gate runs",
69 );
70 const step = b.step(
71 "spirv-val",
72 "Validate every emitted vertex and fragment module with spirv-val",
73 );
74 const path = spirv_val orelse {
75 const missing = b.addFail(
76 "spirv-val needs -Dspirv-val=<pinned nixpkgs#spirv-tools>/bin/spirv-val",
77 );
78 step.dependOn(&missing.step);
79 return;
80 };
81 const tool = b.addExecutable(.{
82 .name = "choir-spirv-val",
83 .root_module = b.createModule(options.createOptions(
84 b,
85 "build/stages.zig",
86 &.{
87 .{ .name = "choir_tests", .module = module.createTestModule(b, options, inputs) },
88 .{ .name = "pretty", .module = inputs.pretty },
89 .{ .name = "sys", .module = inputs.sys },
90 },
91 )),
92 });
93 const run = b.addRunArtifact(tool);
94 run.addArg(path);
95 _ = run.addOutputDirectoryArg("spirv");
96 run.has_side_effects = true;
97 step.dependOn(&run.step);
98 }
99
100 /// Emits every stage module's SPIR-V into a directory that dependents reach as the named lazy
101 /// path `stage-spirv`. Only a dependent that asks for the path builds the emitter, so a package
102 /// that links `choir_abi` alone never builds the compiler.
103 pub fn addStageSpirv(
104 b: *std.Build,
105 options: build_api.ModuleOptions,
106 inputs: module.Inputs,
107 ) void {
108 const tool = b.addExecutable(.{
109 .name = "choir-stage-spirv",
110 .root_module = b.createModule(options.createOptions(
111 b,
112 "build/spirv.zig",
113 &.{
114 .{ .name = "choir_tests", .module = module.createTestModule(b, options, inputs) },
115 .{ .name = "pretty", .module = inputs.pretty },
116 },
117 )),
118 });
119 const run = b.addRunArtifact(tool);
120 b.addNamedLazyPath("stage-spirv", run.addOutputDirectoryArg("stage-spirv"));
121 }
122
123 /// Emits every stage module's MSL into a directory that dependents reach as the named lazy path
124 /// `stage-msl`, with the same laziness as `stage-spirv`.
125 pub fn addStageMsl(
126 b: *std.Build,
127 options: build_api.ModuleOptions,
128 inputs: module.Inputs,
129 ) void {
130 const tool = b.addExecutable(.{
131 .name = "choir-stage-msl",
132 .root_module = b.createModule(options.createOptions(
133 b,
134 "build/msl.zig",
135 &.{
136 .{ .name = "choir_tests", .module = module.createTestModule(b, options, inputs) },
137 .{ .name = "pretty", .module = inputs.pretty },
138 },
139 )),
140 });
141 const run = b.addRunArtifact(tool);
142 b.addNamedLazyPath("stage-msl", run.addOutputDirectoryArg("stage-msl"));
143 }
144
145 /// Emits every stage module as an x86_64 object of host functions into a directory that
146 /// dependents reach as the named lazy path `stage-cpu`, with the same laziness as `stage-spirv`.
147 pub fn addStageCpu(
148 b: *std.Build,
149 options: build_api.ModuleOptions,
150 inputs: module.Inputs,
151 ) void {
152 const tool = b.addExecutable(.{
153 .name = "choir-stage-cpu",
154 .root_module = b.createModule(options.createOptions(
155 b,
156 "build/cpu.zig",
157 &.{
158 .{ .name = "choir_tests", .module = module.createTestModule(b, options, inputs) },
159 .{ .name = "pretty", .module = inputs.pretty },
160 },
161 )),
162 });
163 const run = b.addRunArtifact(tool);
164 b.addNamedLazyPath("stage-cpu", run.addOutputDirectoryArg("stage-cpu"));
165 }
166
167 pub fn addProperties(
168 b: *std.Build,
169 options: build_api.ModuleOptions,
170 configuration: deps.Configuration,
171 dependencies: deps.Requested,
172 choir: *std.Build.Module,
173 ) void {
174 const property_module = b.createModule(options.createOptions(
175 b,
176 "src/properties/test.zig",
177 &.{
178 .{ .name = "alloc_arena", .module = dependencies.inputs.alloc_arena },
179 .{ .name = "choir", .module = choir },
180 .{ .name = "hypothesis", .module = dependencies.hypothesis },
181 .{ .name = "pretty", .module = dependencies.inputs.pretty },
182 },
183 ));
184 const property_tests = b.addTest(.{
185 .root_module = property_module,
186 .filters = configuration.test_filters,
187 });
188 const run_property_tests = b.addRunArtifact(property_tests);
189 const property_step = b.step("pbt", "Run Choir property tests");
190 property_step.dependOn(&run_property_tests.step);
191 }
192
193 pub fn addCfgEvidence(
194 b: *std.Build,
195 options: build_api.ModuleOptions,
196 dependencies: deps.Requested,
197 choir: *std.Build.Module,
198 ) void {
199 const repository = b.root.joinString(b.allocator, "../..") catch @panic("OOM");
200 const revision = b.addSystemCommand(&.{
201 "git",
202 "-C",
203 repository,
204 "rev-parse",
205 "--verify",
206 "HEAD^{commit}",
207 });
208 revision.setName("capture the Choir CFG evidence revision");
209 revision.has_side_effects = true;
210 const revision_file = revision.captureStdOut(.{ .trim_whitespace = .none });
211 const owned_status = b.addSystemCommand(&.{
212 "git",
213 "-C",
214 repository,
215 "status",
216 "--porcelain=v1",
217 "--untracked-files=all",
218 "--",
219 "lib/choir/src/core",
220 "lib/choir/src/properties/cfg.zig",
221 "lib/choir/src/properties/evidence.zig",
222 "lib/choir/build/tests.zig",
223 "lib/choir/build/register.zig",
224 "build/packages/registry/lib.zig",
225 ".github/workflows/ci-verification.yml",
226 });
227 owned_status.setName("capture the Choir CFG evidence owned-source status");
228 owned_status.has_side_effects = true;
229 const owned_status_file = owned_status.captureStdOut(.{ .trim_whitespace = .none });
230 const evidence_options = b.addOptions();
231 evidence_options.addOption(
232 []const u8,
233 "build_source_sha256",
234 sourceDigest(b, cfg_evidence_build_source),
235 );
236 evidence_options.addOption(
237 []const u8,
238 "producer_source_sha256",
239 sourceDigest(b, cfg_evidence_producer_source),
240 );
241 const evidence_module = b.createModule(options.createOptions(
242 b,
243 "src/properties/evidence.zig",
244 &.{
245 .{ .name = "alloc_arena", .module = dependencies.inputs.alloc_arena },
246 .{ .name = "choir", .module = choir },
247 .{ .name = "evidence_options", .module = evidence_options.createModule() },
248 .{ .name = "hypothesis", .module = dependencies.hypothesis },
249 .{ .name = "pretty", .module = dependencies.inputs.pretty },
250 .{ .name = "sys", .module = dependencies.inputs.sys },
251 },
252 ));
253 const evidence_tests = b.addTest(.{
254 .root_module = evidence_module,
255 .filters = &.{
256 "CFG evidence coverage",
257 "CFG evidence freshness",
258 "CFG evidence receipt",
259 },
260 });
261 const run_tests = b.addRunArtifact(evidence_tests);
262 const tests_step = b.step("cfg-evidence-tests", "Run focused Choir CFG evidence tests");
263 tests_step.dependOn(&run_tests.step);
264
265 const evidence_exe = b.addExecutable(.{
266 .name = "choir-cfg-evidence",
267 .root_module = evidence_module,
268 });
269 const run_evidence = b.addRunArtifact(evidence_exe);
270 run_evidence.step.dependOn(&run_tests.step);
271 run_evidence.addFileArg(revision_file);
272 run_evidence.addFileArg(owned_status_file);
273 const receipt = run_evidence.addOutputFileArg("choir-cfg-evidence.jsonl");
274 run_evidence.has_side_effects = true;
275 const install = b.addInstallFileWithDir(
276 receipt,
277 .prefix,
278 "verification/choir/choir-cfg-evidence.jsonl",
279 );
280 const evidence_step = b.step(
281 "cfg-evidence",
282 "Run the bounded Choir CFG property and emit evidence",
283 );
284 evidence_step.dependOn(&install.step);
285 b.addNamedLazyPath("cfg-evidence", receipt);
286 }
287
288 fn sourceDigest(b: *std.Build, source: []const u8) []const u8 {
289 var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined;
290 std.crypto.hash.sha2.Sha256.hash(source, &digest, .{});
291 const hex = std.fmt.bytesToHex(digest, .lower);
292 return b.allocator.dupe(u8, &hex) catch @panic("OOM");
293 }