lib/choir/src/composition/module/serialization/test.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const fixture = @import("../../fixture/root.zig");
  2 const std = @import("std");
  3 const serialization = @import("root.zig");
  4 const model = @import("../root.zig");
  5 const source_model = @import("../../source/root.zig");
  6 const abi = @import("../../root.zig").abi;
  7 const artifact = @import("../../../backends/root.zig").artifact;
  8 const product = @import("../../../product/root.zig");
  9 
 10 const format = serialization.format;
 11 const serialize = serialization.serialize;
 12 const deserialize = serialization.deserialize;
 13 
 14 test {
 15     std.testing.refAllDecls(serialization);
 16     _ = serialization.reference;
 17     _ = serialization.codec;
 18 }
 19 
 20 fn testModule(allocator: std.mem.Allocator) !model.CompositionModule {
 21     const records = try fixture.Records.init(allocator);
 22     defer records.deinit();
 23     var partitioned = try source_model.PartitionedModule.init(allocator, "hybrid.persisted");
 24     defer partitioned.deinit();
 25     try partitioned.addBoundary(.{
 26         .id = .{ .value = 1 },
 27         .name = "lhs",
 28         .element_type = .f32,
 29         .dimensions = &.{8},
 30         .byte_size = 32,
 31         .access = .read_write,
 32         .ownership = .borrowed,
 33         .alias = .disjoint,
 34         .provenance = .{ .path = "persisted.chic", .symbol = "lhs", .line = 9, .column = 2 },
 35     });
 36     try partitioned.addBoundary(.{
 37         .id = .{ .value = 2 },
 38         .name = "rhs",
 39         .element_type = .f32,
 40         .dimensions = &.{8},
 41         .byte_size = 32,
 42         .access = .read_write,
 43         .ownership = .borrowed,
 44         .alias = .{ .boundary = .{ .value = 1 } },
 45         .provenance = .{ .path = "persisted.chic", .symbol = "rhs", .line = 10, .column = 2 },
 46     });
 47     try partitioned.addBoundary(.{
 48         .id = .{ .value = 3 },
 49         .name = "result",
 50         .element_type = .key,
 51         .dimensions = &.{4},
 52         .byte_size = 32,
 53         .access = .write,
 54         .ownership = .produced,
 55         .alias = .disjoint,
 56         .provenance = .{ .path = "persisted.chic", .symbol = "result", .line = 11, .column = 3 },
 57     });
 58     try partitioned.addPartition(.{
 59         .id = .{ .value = 10 },
 60         .name = "host",
 61         .pipeline = .choir,
 62         .outputs = &.{.{ .value = 1 }},
 63         .product = .{ .producer = "composition-fixture", .source = "101", .stage = "hybrid.host.input", .variant = "default" },
 64         .effect = .unknown,
 65         .provenance = .{ .path = "persisted.chic", .symbol = "host", .line = 7, .column = 3 },
 66     });
 67     try partitioned.addPartition(.{
 68         .id = .{ .value = 11 },
 69         .name = "kernel",
 70         .pipeline = .accy,
 71         .inputs = &.{ .{ .value = 1 }, .{ .value = 2 } },
 72         .outputs = &.{.{ .value = 3 }},
 73         .product = .{ .producer = "composition-fixture", .source = "103", .stage = "hybrid.kernel.input", .variant = "default" },
 74         .effect = .none,
 75         .provenance = .{ .path = "persisted.chic", .symbol = "kernel", .line = 11, .column = 5 },
 76     });
 77     try partitioned.addCallSite(.{
 78         .id = .{ .value = 30 },
 79         .name = "host/kernel",
 80         .caller = .{ .value = 10 },
 81         .callee = .{ .value = 11 },
 82         .inputs = &.{ .{ .value = 1 }, .{ .value = 2 } },
 83         .outputs = &.{.{ .value = 3 }},
 84         .provenance = .{ .path = "persisted.chic", .symbol = "kernel", .line = 12, .column = 9 },
 85     });
 86 
 87     var host_artifact = try artifact.Artifact.init(allocator, .{
 88         .kind = .machine_code,
 89         .producer = "choir-x64",
 90         .target = .{
 91             .architecture = .x86_64,
 92             .triple = "x86_64-unknown-linux-gnu",
 93             .vendor = "unknown",
 94             .os = "linux",
 95             .environment = "gnu",
 96             .cpu = "x86-64-v3",
 97             .features = &.{ "sse2", "+avx2" },
 98         },
 99         .abi = .{
100             .name = "sysv",
101             .calling_convention = "c",
102             .object_format = "elf",
103             .pointer_width_bits = 64,
104             .endianness = .little,
105         },
106     });
107     defer host_artifact.deinit();
108     try host_artifact.options.add(.{ .key = "checked", .value = .{ .flag = true } });
109     try host_artifact.options.add(.{ .key = "bias", .value = .{ .signed = -17 } });
110     try host_artifact.options.add(.{ .key = "seed", .value = .{ .unsigned = 0xfedcba9876543210 } });
111     try host_artifact.options.add(.{ .key = "mode", .value = .{ .text = "durable" } });
112     try host_artifact.options.add(.{ .key = "opaque", .value = .{ .bytes = &.{ 0, 255, 7 } } });
113     try host_artifact.linkage.addRequired(.{ .name = abi.invoke_symbol, .kind = .runtime, .binding = .external });
114     try host_artifact.linkage.addRequired(.{ .name = "weak_data", .kind = .data, .binding = .weak });
115     try host_artifact.linkage.addProvided(.{ .name = "host_entry", .kind = .function, .binding = .external });
116     try host_artifact.linkage.addProvided(.{ .name = "local_data", .kind = .data, .binding = .local });
117     try host_artifact.linkage.addRelocation(.{
118         .offset = 2,
119         .symbol = abi.invoke_symbol,
120         .kind = .call,
121         .addend = -4,
122         .width_bits = 64,
123     });
124     try host_artifact.payload.addDebugRecord(.{ .name = ".debug_line", .kind = .dwarf_section, .bytes = &.{ 1, 0, 2, 0 } });
125     try host_artifact.payload.addBuffer(.{ .name = "host_entry", .format = .machine_code, .bytes = &.{ 0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xd0, 0xc3 }, .alignment = 32 });
126     try host_artifact.payload.addTextDump(.{ .name = "lowered", .contents = "func @host_entry" });
127     try host_artifact.verification.replace(host_artifact.allocator, .{ .state = .failed, .stage = "machine-verify", .message = "preserved diagnostic" });
128 
129     var kernel_artifact = try artifact.Artifact.init(allocator, .{
130         .kind = .object_file,
131         .producer = "accy-cpu",
132         .target = .{ .architecture = .x86_64 },
133     });
134     defer kernel_artifact.deinit();
135     try kernel_artifact.linkage.addProvided(.{ .name = "kernel_entry", .kind = .function, .binding = .external });
136     try kernel_artifact.payload.addBuffer(.{ .name = "kernel_entry", .format = .object_file, .bytes = &.{ 0x7f, 'E', 'L', 'F' }, .alignment = 8 });
137     try kernel_artifact.verification.replace(kernel_artifact.allocator, .{ .state = .skipped });
138 
139     var module = try model.CompositionModule.init(allocator, &partitioned);
140     errdefer module.deinit();
141     try module.addFragment(.{
142         .id = .{ .value = 20 },
143         .partition = .{ .value = 10 },
144         .pipeline = .choir,
145         .pipeline_input = try records.key("hybrid.host.input", "101", "201"),
146 
147         .artifacts = &.{host_artifact},
148         .exports = &.{.{ .name = "host", .symbol = "host_entry", .abi_version = abi.version }},
149         .imports = &.{.{ .name = "invoke", .symbol = abi.invoke_symbol, .abi_version = abi.version, .kind = .runtime }},
150         .provenance = .{ .path = "persisted.chic", .symbol = "host_entry", .line = 20, .column = 4 },
151     });
152     try module.addFragment(.{
153         .id = .{ .value = 21 },
154         .partition = .{ .value = 11 },
155         .pipeline = .accy,
156         .pipeline_input = try records.key("hybrid.kernel.input", "103", "203"),
157 
158         .artifacts = &.{kernel_artifact},
159         .exports = &.{.{ .name = "kernel", .symbol = "kernel_entry", .abi_version = abi.version }},
160         .provenance = .{ .path = "persisted.chic", .symbol = "kernel_entry", .line = 24, .column = 8 },
161     });
162     try module.addFragment(.{
163         .id = .{ .value = 22 },
164         .partition = .{ .value = 11 },
165         .pipeline = .accy,
166         .pipeline_input = try records.key("hybrid.kernel.input", "103", "203"),
167 
168         .artifacts = &.{kernel_artifact},
169         .exports = &.{.{ .name = "kernel", .symbol = "kernel_entry", .abi_version = abi.version }},
170         .provenance = .{ .path = "persisted.chic", .symbol = "kernel_entry", .line = 24, .column = 8 },
171     });
172     try module.addVariant(.{
173         .id = .{ .value = 1 },
174         .choice = .{
175             .target = product.productRef("composition-fixture", "301", "hybrid.target", "default"),
176             .policy = product.productRef("composition-fixture", "302", "hybrid.policy", "default"),
177         },
178         .fragments = &.{ .{ .value = 20 }, .{ .value = 21 } },
179         .call_sites = &.{.{
180             .id = .{ .value = 30 },
181             .name = "host/kernel",
182             .caller = .{ .value = 20 },
183             .caller_export = "host",
184             .runtime_import = "invoke",
185             .callee = .{ .value = 21 },
186             .callee_export = "kernel",
187             .inputs = &.{ .{ .value = 1 }, .{ .value = 2 } },
188             .outputs = &.{.{ .value = 3 }},
189             .abi_version = abi.version,
190             .provenance = .{ .path = "persisted.chic", .symbol = "kernel", .line = 12, .column = 9 },
191         }},
192     });
193     try module.addVariant(.{
194         .id = .{ .value = 2 },
195         .choice = .{
196             .target = product.productRef("composition-fixture", "303", "hybrid.target", "default"),
197             .policy = product.productRef("composition-fixture", "302", "hybrid.policy", "default"),
198         },
199         .fragments = &.{ .{ .value = 20 }, .{ .value = 22 } },
200         .call_sites = &.{.{
201             .id = .{ .value = 30 },
202             .name = "host/kernel",
203             .caller = .{ .value = 20 },
204             .caller_export = "host",
205             .runtime_import = "invoke",
206             .callee = .{ .value = 22 },
207             .callee_export = "kernel",
208             .inputs = &.{ .{ .value = 1 }, .{ .value = 2 } },
209             .outputs = &.{.{ .value = 3 }},
210             .abi_version = abi.version,
211             .provenance = .{ .path = "persisted.chic", .symbol = "kernel", .line = 12, .column = 9 },
212         }},
213     });
214     try module.verify();
215     return module;
216 }
217 
218 fn expectProductEqual(expected: product.ProductKey, actual: product.ProductKey) !void {
219     try std.testing.expect(expected.ref.eql(actual.ref));
220     try std.testing.expectEqualSlices(u8, expected.record.bytes(), actual.record.bytes());
221 }
222 
223 fn expectProvenanceEqual(expected: source_model.Provenance, actual: source_model.Provenance) !void {
224     try std.testing.expectEqualStrings(expected.path, actual.path);
225     try std.testing.expectEqualStrings(expected.symbol, actual.symbol);
226     try std.testing.expectEqual(expected.line, actual.line);
227     try std.testing.expectEqual(expected.column, actual.column);
228 }
229 
230 fn expectBoundaryIdsEqual(expected: []const source_model.BoundaryId, actual: []const source_model.BoundaryId) !void {
231     try std.testing.expectEqual(expected.len, actual.len);
232     for (expected, actual) |expected_id, actual_id| try std.testing.expectEqual(expected_id.value, actual_id.value);
233 }
234 
235 fn expectModuleEqual(expected: *const model.CompositionModule, actual: *const model.CompositionModule) !void {
236     try std.testing.expectEqualStrings(expected.source_module.name, actual.source_module.name);
237     try std.testing.expectEqual(expected.source_module.boundaries.items.len, actual.source_module.boundaries.items.len);
238     for (expected.source_module.boundaries.items, actual.source_module.boundaries.items) |expected_boundary, actual_boundary| {
239         try std.testing.expectEqual(expected_boundary.id.value, actual_boundary.id.value);
240         try std.testing.expectEqualStrings(expected_boundary.name, actual_boundary.name);
241         try std.testing.expectEqual(expected_boundary.element_type, actual_boundary.element_type);
242         try std.testing.expectEqualSlices(u64, expected_boundary.dimensions, actual_boundary.dimensions);
243         try std.testing.expectEqual(expected_boundary.byte_size, actual_boundary.byte_size);
244         try std.testing.expectEqual(expected_boundary.access, actual_boundary.access);
245         try std.testing.expectEqual(expected_boundary.ownership, actual_boundary.ownership);
246         try std.testing.expect(expected_boundary.alias.eql(actual_boundary.alias));
247         try expectProvenanceEqual(expected_boundary.provenance, actual_boundary.provenance);
248     }
249 
250     try std.testing.expectEqual(expected.source_module.partitions.items.len, actual.source_module.partitions.items.len);
251     for (expected.source_module.partitions.items, actual.source_module.partitions.items) |expected_partition, actual_partition| {
252         try std.testing.expectEqual(expected_partition.id.value, actual_partition.id.value);
253         try std.testing.expectEqualStrings(expected_partition.name, actual_partition.name);
254         try std.testing.expectEqual(expected_partition.pipeline, actual_partition.pipeline);
255         try expectBoundaryIdsEqual(expected_partition.inputs, actual_partition.inputs);
256         try expectBoundaryIdsEqual(expected_partition.outputs, actual_partition.outputs);
257         try std.testing.expect(expected_partition.product.eql(actual_partition.product));
258         try std.testing.expectEqual(expected_partition.effect, actual_partition.effect);
259         try expectProvenanceEqual(expected_partition.provenance, actual_partition.provenance);
260     }
261 
262     try std.testing.expectEqual(expected.source_module.call_sites.items.len, actual.source_module.call_sites.items.len);
263     for (expected.source_module.call_sites.items, actual.source_module.call_sites.items) |expected_call, actual_call| {
264         try std.testing.expectEqual(expected_call.id.value, actual_call.id.value);
265         try std.testing.expectEqualStrings(expected_call.name, actual_call.name);
266         try std.testing.expectEqual(expected_call.caller.value, actual_call.caller.value);
267         try std.testing.expectEqual(expected_call.callee.value, actual_call.callee.value);
268         try expectBoundaryIdsEqual(expected_call.inputs, actual_call.inputs);
269         try expectBoundaryIdsEqual(expected_call.outputs, actual_call.outputs);
270         try expectProvenanceEqual(expected_call.provenance, actual_call.provenance);
271     }
272 
273     try std.testing.expectEqual(expected.fragments.items.len, actual.fragments.items.len);
274     for (expected.fragments.items, actual.fragments.items) |expected_fragment, actual_fragment| {
275         try std.testing.expectEqual(expected_fragment.id.value, actual_fragment.id.value);
276         try std.testing.expectEqual(expected_fragment.partition.value, actual_fragment.partition.value);
277         try std.testing.expectEqual(expected_fragment.pipeline, actual_fragment.pipeline);
278         try expectProductEqual(expected_fragment.pipeline_input, actual_fragment.pipeline_input);
279         try std.testing.expectEqual(expected_fragment.artifacts.len, actual_fragment.artifacts.len);
280         for (expected_fragment.artifacts, actual_fragment.artifacts) |expected_artifact, actual_artifact| {
281             try std.testing.expect(expected_artifact.eql(actual_artifact));
282         }
283         try std.testing.expectEqual(expected_fragment.exports.len, actual_fragment.exports.len);
284         for (expected_fragment.exports, actual_fragment.exports) |expected_export, actual_export| {
285             try std.testing.expectEqualStrings(expected_export.name, actual_export.name);
286             try std.testing.expectEqualStrings(expected_export.symbol, actual_export.symbol);
287             try std.testing.expectEqual(expected_export.abi_version, actual_export.abi_version);
288         }
289         try std.testing.expectEqual(expected_fragment.imports.len, actual_fragment.imports.len);
290         for (expected_fragment.imports, actual_fragment.imports) |expected_import, actual_import| {
291             try std.testing.expectEqualStrings(expected_import.name, actual_import.name);
292             try std.testing.expectEqualStrings(expected_import.symbol, actual_import.symbol);
293             try std.testing.expectEqual(expected_import.abi_version, actual_import.abi_version);
294             try std.testing.expectEqual(expected_import.kind, actual_import.kind);
295         }
296         try expectProvenanceEqual(expected_fragment.provenance, actual_fragment.provenance);
297     }
298 
299     try std.testing.expectEqual(expected.variants.items.len, actual.variants.items.len);
300     for (expected.variants.items, actual.variants.items) |expected_variant, actual_variant| {
301         try std.testing.expectEqual(expected_variant.id.value, actual_variant.id.value);
302         try std.testing.expect(expected_variant.choice.target.eql(actual_variant.choice.target));
303         try std.testing.expect(expected_variant.choice.policy.eql(actual_variant.choice.policy));
304         try std.testing.expectEqual(expected_variant.fragments.len, actual_variant.fragments.len);
305         for (expected_variant.fragments, actual_variant.fragments) |expected_fragment, actual_fragment| {
306             try std.testing.expectEqual(expected_fragment.value, actual_fragment.value);
307         }
308         try std.testing.expectEqual(expected_variant.call_sites.len, actual_variant.call_sites.len);
309         for (expected_variant.call_sites, actual_variant.call_sites) |expected_call, actual_call| {
310             try std.testing.expectEqual(expected_call.id.value, actual_call.id.value);
311             try std.testing.expectEqualStrings(expected_call.name, actual_call.name);
312             try std.testing.expectEqual(expected_call.caller.value, actual_call.caller.value);
313             try std.testing.expectEqualStrings(expected_call.caller_export, actual_call.caller_export);
314             try std.testing.expectEqualStrings(expected_call.runtime_import, actual_call.runtime_import);
315             try std.testing.expectEqual(expected_call.callee.value, actual_call.callee.value);
316             try std.testing.expectEqualStrings(expected_call.callee_export, actual_call.callee_export);
317             try expectBoundaryIdsEqual(expected_call.inputs, actual_call.inputs);
318             try expectBoundaryIdsEqual(expected_call.outputs, actual_call.outputs);
319             try std.testing.expectEqual(expected_call.abi_version, actual_call.abi_version);
320             try expectProvenanceEqual(expected_call.provenance, actual_call.provenance);
321         }
322     }
323 }
324 
325 test "composition module binary serialization round-trips every owned field" {
326     const testing = std.testing;
327     var original = try testModule(testing.allocator);
328     var original_owned = true;
329     defer if (original_owned) original.deinit();
330     const encoded = try serialize(testing.allocator, &original);
331     defer testing.allocator.free(encoded);
332     try testing.expectEqual(@as(usize, 2885), encoded.len);
333     try testing.expectEqual(@as(u32, 3643165904), format.checksum(encoded));
334 
335     var decoded = try deserialize(testing.allocator, encoded);
336     defer decoded.deinit();
337     try expectModuleEqual(&original, &decoded);
338     try testing.expectEqual(@as(usize, 2), decoded.variants.items.len);
339     const first_variant = decoded.selectVariant(.{
340         .target = product.productRef("composition-fixture", "301", "hybrid.target", "default"),
341         .policy = product.productRef("composition-fixture", "302", "hybrid.policy", "default"),
342     }) orelse return error.TestExpectedResult;
343     const second_variant = decoded.selectVariant(.{
344         .target = product.productRef("composition-fixture", "303", "hybrid.target", "default"),
345         .policy = product.productRef("composition-fixture", "302", "hybrid.policy", "default"),
346     }) orelse return error.TestExpectedResult;
347     try testing.expectEqualSlices(
348         source_model.FragmentId,
349         &.{ .{ .value = 20 }, .{ .value = 21 } },
350         first_variant.fragments,
351     );
352     try testing.expectEqualSlices(
353         source_model.FragmentId,
354         &.{ .{ .value = 20 }, .{ .value = 22 } },
355         second_variant.fragments,
356     );
357     try testing.expectEqual(source_model.Effect.unknown, decoded.source_module.partitions.items[0].effect);
358     const reencoded = try serialize(testing.allocator, &decoded);
359     defer testing.allocator.free(reencoded);
360     try testing.expectEqualSlices(u8, encoded, reencoded);
361 
362     original.deinit();
363     original_owned = false;
364     @memset(encoded, 0);
365     const independent = try serialize(testing.allocator, &decoded);
366     defer testing.allocator.free(independent);
367     try testing.expectEqualSlices(u8, reencoded, independent);
368     try testing.expectEqualStrings("preserved diagnostic", decoded.fragments.items[0].artifacts[0].verification.message.?);
369 }
370 
371 test "composition module round trip keeps two compilers apart at one product ref" {
372     const testing = std.testing;
373     const records = try fixture.Records.init(testing.allocator);
374     defer records.deinit();
375     const left = try records.importUnder("hybrid.target", "301", "image", "toolchain-a");
376     const right = try records.importUnder("hybrid.target", "301", "image", "toolchain-b");
377     try testing.expectEqualSlices(u8, left.record.bytes(), right.record.bytes());
378     try testing.expect(left.ref.eql(right.ref));
379     try testing.expect(!left.eql(right));
380 
381     var manifests = serialization.reference.Manifests{};
382     var writer = format.Writer.init(testing.allocator);
383     defer writer.deinit();
384     try writer.writeBlob(left.record.bytes());
385     try writer.writeInt(u32, @intCast(try manifests.intern(left.record.compilerManifest())));
386     try writer.writeBlob(right.record.bytes());
387     try writer.writeInt(u32, @intCast(try manifests.intern(right.record.compilerManifest())));
388     try testing.expectEqual(@as(usize, 2), manifests.count);
389 
390     var table = format.Writer.init(testing.allocator);
391     defer table.deinit();
392     try serialization.reference.writeManifests(&table, &manifests);
393     var table_reader = try format.Reader.init(table.bytes.items);
394     const restored = try serialization.reference.readManifests(&table_reader);
395     try testing.expect(table_reader.atEnd());
396 
397     const owner = try serialization.reference.createStore(testing.allocator, 2);
398     defer owner.release();
399     var reader = try format.Reader.init(writer.bytes.items);
400     const first = try serialization.reference.readRecord(owner, &reader, &restored);
401     const second = try serialization.reference.readRecord(owner, &reader, &restored);
402     try testing.expect(reader.atEnd());
403     try testing.expect(first.ref.eql(second.ref));
404     try testing.expectEqualSlices(u8, first.record.bytes(), second.record.bytes());
405     try testing.expect(!first.eql(second));
406     try testing.expectEqualStrings("toolchain-a", first.record.compilerManifest());
407     try testing.expectEqualStrings("toolchain-b", second.record.compilerManifest());
408 }
409 
410 test "composition module refuses the record shape this format replaced" {
411     const testing = std.testing;
412     var original = try testModule(testing.allocator);
413     defer original.deinit();
414     const encoded = try serialize(testing.allocator, &original);
415     defer testing.allocator.free(encoded);
416     try testing.expectEqual(@as(u32, 6), format.version);
417     std.mem.writeInt(u32, encoded[format.magic.len..][0..4], 5, .little);
418     try testing.expectError(error.UnsupportedVersion, deserialize(testing.allocator, encoded));
419 }
420 
421 test "composition module binary deserialization rejects malformed inputs" {
422     const testing = std.testing;
423     var module = try testModule(testing.allocator);
424     defer module.deinit();
425     const encoded = try serialize(testing.allocator, &module);
426     defer testing.allocator.free(encoded);
427 
428     const invalid_magic = try testing.allocator.dupe(u8, encoded);
429     defer testing.allocator.free(invalid_magic);
430     invalid_magic[0] ^= 0xff;
431     try testing.expectError(error.InvalidMagic, deserialize(testing.allocator, invalid_magic));
432 
433     const unsupported_version = try testing.allocator.dupe(u8, encoded);
434     defer testing.allocator.free(unsupported_version);
435     std.mem.writeInt(u32, unsupported_version[format.magic.len..][0..4], format.version + 1, .little);
436     try testing.expectError(error.UnsupportedVersion, deserialize(testing.allocator, unsupported_version));
437 
438     const invalid_length = try testing.allocator.dupe(u8, encoded);
439     defer testing.allocator.free(invalid_length);
440     const length_offset = format.magic.len + @sizeOf(u32);
441     std.mem.writeInt(u64, invalid_length[length_offset..][0..@sizeOf(u64)], std.math.maxInt(u64), .little);
442     var length_allocator = testing.FailingAllocator.init(testing.allocator, .{ .fail_index = 0 });
443     try testing.expectError(error.LimitExceeded, deserialize(length_allocator.allocator(), invalid_length));
444 
445     const source_name_offset = format.header_size;
446     const boundary_count_offset = source_name_offset + @sizeOf(u32) + module.source_module.name.len;
447     const first_boundary = module.source_module.boundaries.items[0];
448     const first_element_type_offset = boundary_count_offset + @sizeOf(u32) + @sizeOf(u64) + @sizeOf(u32) + first_boundary.name.len;
449     const invalid_tag = try testing.allocator.dupe(u8, encoded);
450     defer testing.allocator.free(invalid_tag);
451     invalid_tag[first_element_type_offset] = 0xff;
452     refreshChecksum(invalid_tag);
453     try testing.expectError(error.InvalidTag, deserialize(testing.allocator, invalid_tag));
454 
455     try testing.expectError(error.Truncated, deserialize(testing.allocator, encoded[0 .. encoded.len - 1]));
456 
457     const trailing = try testing.allocator.alloc(u8, encoded.len + 1);
458     defer testing.allocator.free(trailing);
459     @memcpy(trailing[0..encoded.len], encoded);
460     trailing[encoded.len] = 0;
461     try testing.expectError(error.TrailingBytes, deserialize(testing.allocator, trailing));
462 
463     const count_bomb = try testing.allocator.dupe(u8, encoded);
464     defer testing.allocator.free(count_bomb);
465     const first_dimensions_count_offset = first_element_type_offset + @sizeOf(u8);
466     std.mem.writeInt(u32, count_bomb[first_dimensions_count_offset..][0..4], std.math.maxInt(u32), .little);
467     refreshChecksum(count_bomb);
468     try testing.expectError(error.LimitExceeded, deserialize(testing.allocator, count_bomb));
469 
470     const artifact_offset = std.mem.indexOf(u8, encoded, artifact.serialization.magic).?;
471     const artifact_payload_offset = artifact_offset + artifact.serialization.magic.len + @sizeOf(u32);
472 
473     const invalid_bool = try testing.allocator.dupe(u8, encoded);
474     defer testing.allocator.free(invalid_bool);
475     invalid_bool[artifact_payload_offset + @sizeOf(u8)] = 2;
476     refreshChecksum(invalid_bool);
477     try testing.expectError(error.InvalidValue, deserialize(testing.allocator, invalid_bool));
478 
479     const invalid_string = try testing.allocator.dupe(u8, encoded);
480     defer testing.allocator.free(invalid_string);
481     const producer_length_offset = artifact_payload_offset + @sizeOf(u8) + @sizeOf(u8);
482     std.mem.writeInt(u32, invalid_string[producer_length_offset..][0..@sizeOf(u32)], artifact.serialization.format.max_string_bytes + 1, .little);
483     refreshChecksum(invalid_string);
484     try testing.expectError(error.LimitExceeded, deserialize(testing.allocator, invalid_string));
485 
486     const invalid_blob = try testing.allocator.dupe(u8, encoded);
487     defer testing.allocator.free(invalid_blob);
488     const executable = module.fragments.items[0].artifacts[0].payload.buffers.items[0].bytes;
489     const executable_offset = std.mem.indexOf(u8, invalid_blob, executable).?;
490     const executable_length_offset = executable_offset - @sizeOf(u32);
491     std.mem.writeInt(u32, invalid_blob[executable_length_offset..][0..@sizeOf(u32)], artifact.serialization.format.max_blob_bytes + 1, .little);
492     refreshChecksum(invalid_blob);
493     try testing.expectError(error.LimitExceeded, deserialize(testing.allocator, invalid_blob));
494 
495     const executable_corruption = try testing.allocator.dupe(u8, encoded);
496     defer testing.allocator.free(executable_corruption);
497     const corruption_offset = std.mem.indexOf(u8, executable_corruption, executable).?;
498     executable_corruption[corruption_offset + executable.len / 2] ^= 0x80;
499     var checksum_allocator = testing.FailingAllocator.init(testing.allocator, .{ .fail_index = 0 });
500     try testing.expectError(error.InvalidChecksum, deserialize(checksum_allocator.allocator(), executable_corruption));
501 }
502 
503 fn refreshChecksum(bytes: []u8) void {
504     const payload = bytes[format.header_size .. bytes.len - format.checksum_size];
505     std.mem.writeInt(u32, bytes[bytes.len - format.checksum_size ..][0..format.checksum_size], format.checksum(payload), .little);
506 }
507 
508 test "composition module binary serialization survives every allocation failure" {
509     const testing = std.testing;
510     var module = try testModule(testing.allocator);
511     defer module.deinit();
512     const encoded = try serialize(testing.allocator, &module);
513     defer testing.allocator.free(encoded);
514 
515     const Harness = struct {
516         fn serializeRun(allocator: std.mem.Allocator, value: *const model.CompositionModule) !void {
517             const bytes = try serialize(allocator, value);
518             defer allocator.free(bytes);
519         }
520 
521         fn deserializeRun(allocator: std.mem.Allocator, bytes: []const u8) !void {
522             var decoded = try deserialize(allocator, bytes);
523             defer decoded.deinit();
524         }
525     };
526 
527     try testing.checkAllAllocationFailures(testing.allocator, Harness.serializeRun, .{&module});
528     try testing.checkAllAllocationFailures(testing.allocator, Harness.deserializeRun, .{encoded});
529 }