authorgravatar for kubkon@jakubkonka.comJakub Konka <kubkon@jakubkonka.com> 2022-04-26 15:17:41+02:00
committergravatar for kubkon@jakubkonka.comJakub Konka <kubkon@jakubkonka.com> 2022-04-28 18:35:01+02:00
logd8d12d51ecd9e893755ced927c79eb9a6c036b31
treef1e6049881714664eb5af8ac715c8b9fbc53299f
parentd5fcb509881e1b022d2bcef303b53b4f67db1c9a

test: abstract away test manifest parser into separate struct


4 files changed, 340 insertions(+), 0 deletions(-)

src/test.zig+300
...@@ -72,6 +72,17 @@ test {...@@ -72,6 +72,17 @@ test {
72 }72 }
73 }73 }
7474
75 {
76 const dir_path = try std.fs.path.join(arena, &.{
77 std.fs.path.dirname(@src().file).?, "..", "test", "incremental",
78 });
79
80 var dir = try std.fs.cwd().openDir(dir_path, .{ .iterate = true });
81 defer dir.close();
82
83 ctx.addTestCasesFromDir(dir);
84 }
85
75 try @import("test_cases").addCases(&ctx);86 try @import("test_cases").addCases(&ctx);
7687
77 try ctx.run();88 try ctx.run();
...@@ -154,6 +165,125 @@ const ErrorMsg = union(enum) {...@@ -154,6 +165,125 @@ const ErrorMsg = union(enum) {
154 }165 }
155};166};
156167
168/// Manifest syntax example:
169/// (see https://github.com/ziglang/zig/issues/11288)
170///
171/// error
172/// backend=stage1,stage2
173/// output_mode=exe
174///
175/// :3:19: error: foo
176///
177/// run
178/// target=x86_64-linux,aarch64-macos
179///
180/// I am expected stdout! Hello!
181///
182/// cli
183///
184/// build test
185const TestManifest = struct {
186 @"type": Type,
187 config_map: std.StringHashMap([]const u8),
188 trailing_bytes: []const u8 = "",
189
190 const Type = enum {
191 @"error",
192 run,
193 cli,
194 };
195
196 const TrailingIterator = struct {
197 inner: std.mem.TokenIterator(u8),
198
199 fn next(self: *TrailingIterator) ?[]const u8 {
200 const next_inner = self.inner.next() orelse return null;
201 return std.mem.trim(u8, next_inner, " \t");
202 }
203 };
204
205 fn ConfigValueIterator(comptime T: type, comptime ParseFn: type) type {
206 return struct {
207 inner: std.mem.SplitIterator(u8),
208 parse_fn: ParseFn,
209
210 fn next(self: *@This()) ?T {
211 const next_raw = self.inner.next() orelse return null;
212 return self.parse_fn(next_raw);
213 }
214 };
215 }
216
217 fn parse(arena: Allocator, bytes: []const u8) !TestManifest {
218 var it = std.mem.tokenize(u8, bytes, "\r\n");
219
220 // First line is the test type
221 const tt: Type = blk: {
222 const line = it.next() orelse return error.MissingTestCaseType;
223 const raw = std.mem.trim(u8, line[2..], " \t");
224 if (std.mem.eql(u8, raw, "error")) {
225 break :blk .@"error";
226 } else if (std.mem.eql(u8, raw, "run")) {
227 break :blk .run;
228 } else if (std.mem.eql(u8, raw, "cli")) {
229 break :blk .cli;
230 } else {
231 std.log.warn("unknown test case type requested: {s}", .{raw});
232 return error.UnknownTestCaseType;
233 }
234 };
235
236 var manifest: TestManifest = .{
237 .@"type" = tt,
238 .config_map = std.StringHashMap([]const u8).init(arena),
239 };
240
241 // Any subsequent line until a blank comment line is key=value(s) pair
242 while (it.next()) |line| {
243 const trimmed = std.mem.trim(u8, line[2..], " \t");
244 if (trimmed.len == 0) break;
245
246 // Parse key=value(s)
247 var kv_it = std.mem.split(u8, trimmed, "=");
248 const key = kv_it.next() orelse return error.MissingKeyForConfig;
249 try manifest.config_map.putNoClobber(key, kv_it.next() orelse return error.MissingValuesForConfig);
250 }
251
252 // Finally, trailing is expected output
253 manifest.trailing_bytes = bytes[it.index..];
254
255 return manifest;
256 }
257
258 fn getConfigValues(
259 self: TestManifest,
260 key: []const u8,
261 comptime T: type,
262 parse_fn: anytype,
263 ) ?ConfigValueIterator(T, @TypeOf(parse_fn)) {
264 const bytes = self.config_map.get(key) orelse return null;
265 return ConfigValueIterator(T, @TypeOf(parse_fn)){
266 .inner = std.mem.split(u8, bytes, ","),
267 .parse_fn = parse_fn,
268 };
269 }
270
271 fn trailing(self: TestManifest) TrailingIterator {
272 return .{
273 .inner = std.mem.tokenize(u8, self.trailing_bytes, "\r\n"),
274 };
275 }
276
277 fn trailingAlloc(self: TestManifest, arena: Allocator) ![]const []const u8 {
278 var out = std.ArrayList([]const u8).init(arena);
279 var it = self.trailing();
280 while (it.next()) |line| {
281 try out.append(line);
282 }
283 return out.toOwnedSlice();
284 }
285};
286
157pub const TestContext = struct {287pub const TestContext = struct {
158 arena: Allocator,288 arena: Allocator,
159 cases: std.ArrayList(Case),289 cases: std.ArrayList(Case),
...@@ -197,6 +327,10 @@ pub const TestContext = struct {...@@ -197,6 +327,10 @@ pub const TestContext = struct {
197 stage1,327 stage1,
198 stage2,328 stage2,
199 llvm,329 llvm,
330
331 fn parse(str: []const u8) ?Backend {
332 return std.meta.stringToEnum(Backend, str);
333 }
200 };334 };
201335
202 /// A `Case` consists of a list of `Update`. The same `Compilation` is used for each336 /// A `Case` consists of a list of `Update`. The same `Compilation` is used for each
...@@ -661,6 +795,10 @@ pub const TestContext = struct {...@@ -661,6 +795,10 @@ pub const TestContext = struct {
661 /// Execute all tests as incremental updates to a single compilation. Explicitly795 /// Execute all tests as incremental updates to a single compilation. Explicitly
662 /// incremental tests ("foo.0.zig", "foo.1.zig", etc.) still execute in order796 /// incremental tests ("foo.0.zig", "foo.1.zig", etc.) still execute in order
663 incremental,797 incremental,
798
799 fn parse(str: []const u8) ?Strategy {
800 return std.meta.stringToEnum(Strategy, str);
801 }
664 };802 };
665803
666 /// Adds a compile-error test for each file in the provided directory, using the804 /// Adds a compile-error test for each file in the provided directory, using the
...@@ -689,6 +827,15 @@ pub const TestContext = struct {...@@ -689,6 +827,15 @@ pub const TestContext = struct {
689 };827 };
690 }828 }
691829
830 pub fn addTestCasesFromDir(ctx: *TestContext, dir: std.fs.Dir) void {
831 var current_file: []const u8 = "none";
832 addTestCasesFromDirInner(ctx, dir, &current_file) catch |err| {
833 std.debug.panic("test harness failed to process file '{s}': {s}\n", .{
834 current_file, @errorName(err),
835 });
836 };
837 }
838
692 /// For a filename in the format "<filename>.X.<ext>" or "<filename>.<ext>", returns839 /// For a filename in the format "<filename>.X.<ext>" or "<filename>.<ext>", returns
693 /// "<filename>", "<ext>" and X parsed as a decimal number. If X is not present, or840 /// "<filename>", "<ext>" and X parsed as a decimal number. If X is not present, or
694 /// cannot be parsed as a decimal number, it is treated as part of <filename>841 /// cannot be parsed as a decimal number, it is treated as part of <filename>
...@@ -749,6 +896,159 @@ pub const TestContext = struct {...@@ -749,6 +896,159 @@ pub const TestContext = struct {
749 std.sort.sort([]const u8, filenames, Context{}, Context.lessThan);896 std.sort.sort([]const u8, filenames, Context{}, Context.lessThan);
750 }897 }
751898
899 fn addTestCasesFromDirInner(
900 ctx: *TestContext,
901 dir: std.fs.Dir,
902 /// This is kept up to date with the currently being processed file so
903 /// that if any errors occur the caller knows it happened during this file.
904 current_file: *[]const u8,
905 ) !void {
906 var opt_case: ?*Case = null;
907
908 var it = dir.iterate();
909 var filenames = std.ArrayList([]const u8).init(ctx.arena);
910 defer filenames.deinit();
911
912 while (try it.next()) |entry| {
913 if (entry.kind != .File) continue;
914
915 // Ignore stuff such as .swp files
916 switch (Compilation.classifyFileExt(entry.name)) {
917 .unknown => continue,
918 else => {},
919 }
920 try filenames.append(try ctx.arena.dupe(u8, entry.name));
921 }
922
923 // Sort filenames, so that incremental tests are contiguous and in-order
924 sortTestFilenames(filenames.items);
925
926 var prev_filename: []const u8 = "";
927 for (filenames.items) |filename| {
928 current_file.* = filename;
929
930 { // First, check if this file is part of an incremental update sequence
931
932 // Split filename into "<base_name>.<index>.<file_ext>"
933 const prev_parts = getTestFileNameParts(prev_filename);
934 const new_parts = getTestFileNameParts(filename);
935
936 // If base_name and file_ext match, these files are in the same test sequence
937 // and the new one should be the incremented version of the previous test
938 if (std.mem.eql(u8, prev_parts.base_name, new_parts.base_name) and
939 std.mem.eql(u8, prev_parts.file_ext, new_parts.file_ext))
940 {
941
942 // This is "foo.X.zig" followed by "foo.Y.zig". Make sure that X = Y + 1
943 if (prev_parts.test_index == null) return error.InvalidIncrementalTestIndex;
944 if (new_parts.test_index == null) return error.InvalidIncrementalTestIndex;
945 if (new_parts.test_index.? != prev_parts.test_index.? + 1) return error.InvalidIncrementalTestIndex;
946 } else {
947
948 // This is not the same test sequence, so the new file must be the first file
949 // in a new sequence ("*.0.zig") or an independent test file ("*.zig")
950 if (new_parts.test_index != null and new_parts.test_index.? != 0) return error.InvalidIncrementalTestIndex;
951
952 // if (strategy == .independent)
953 // opt_case = null; // Generate a new independent test case for this update
954 }
955 }
956 prev_filename = filename;
957
958 const max_file_size = 10 * 1024 * 1024;
959 const src = try dir.readFileAllocOptions(ctx.arena, filename, max_file_size, null, 1, 0);
960
961 // The manifest is the last contiguous block of comments in the file
962 // We scan for the beginning by searching backward for the first non-empty line that does not start with "//"
963 var manifest_start: ?usize = null;
964 var manifest_end: usize = src.len;
965 if (src.len > 0) {
966 var cursor: usize = src.len - 1;
967 while (true) {
968 // Move to beginning of line
969 while (cursor > 0 and src[cursor - 1] != '\n') cursor -= 1;
970
971 if (std.mem.startsWith(u8, src[cursor..], "//")) {
972 manifest_start = cursor; // Contiguous comment line, include in manifest
973 } else {
974 if (manifest_start != null) break; // Encountered non-comment line, end of manifest
975
976 // We ignore all-whitespace lines following the comment block, but anything else
977 // means that there is no manifest present.
978 if (std.mem.trim(u8, src[cursor..manifest_end], " \r\n\t").len == 0) {
979 manifest_end = cursor;
980 } else break; // If it's not whitespace, there is no manifest
981 }
982
983 // Move to previous line
984 if (cursor != 0) cursor -= 1 else break;
985 }
986 }
987
988 if (manifest_start) |start| {
989 // Parse the manifest
990 var mani = try TestManifest.parse(ctx.arena, src[start..manifest_end]);
991 const strategy = mani.getConfigValues("strategy", Strategy, Strategy.parse).?.next().?;
992 const backend = mani.getConfigValues("backend", Backend, Backend.parse).?.next().?;
993
994 switch (mani.@"type") {
995 .@"error" => {
996 const case = opt_case orelse case: {
997 const case = try ctx.cases.addOne();
998 case.* = .{
999 .name = "none",
1000 .target = .{},
1001 .backend = backend,
1002 .updates = std.ArrayList(TestContext.Update).init(ctx.cases.allocator),
1003 .is_test = false,
1004 .output_mode = .Obj,
1005 .files = std.ArrayList(TestContext.File).init(ctx.cases.allocator),
1006 };
1007 opt_case = case;
1008 break :case case;
1009 };
1010 const errors = try mani.trailingAlloc(ctx.arena);
1011
1012 switch (strategy) {
1013 .independent => {
1014 case.addError(src, errors);
1015 },
1016 .incremental => {
1017 case.addErrorNamed("update", src, errors);
1018 },
1019 }
1020 },
1021 .run => {
1022 const case = opt_case orelse case: {
1023 const case = try ctx.cases.addOne();
1024 case.* = .{
1025 .name = "none",
1026 .target = .{},
1027 .backend = backend,
1028 .updates = std.ArrayList(TestContext.Update).init(ctx.cases.allocator),
1029 .is_test = false,
1030 .output_mode = .Exe,
1031 .files = std.ArrayList(TestContext.File).init(ctx.cases.allocator),
1032 };
1033 opt_case = case;
1034 break :case case;
1035 };
1036
1037 var output = std.ArrayList(u8).init(ctx.arena);
1038 var trailing_it = mani.trailing();
1039 while (trailing_it.next()) |line| {
1040 try output.appendSlice(line);
1041 }
1042 case.addCompareOutput(src, output.toOwnedSlice());
1043 },
1044 .cli => @panic("TODO cli tests"),
1045 }
1046 } else {
1047 return error.MissingManifest;
1048 }
1049 }
1050 }
1051
752 fn addErrorCasesFromDirInner(1052 fn addErrorCasesFromDirInner(
753 ctx: *TestContext,1053 ctx: *TestContext,
754 name: []const u8,1054 name: []const u8,
test/incremental/add.0.zig created+12
...@@ -0,0 +1,12 @@
1pub fn main() void {
2 add(3, 4);
3}
4
5fn add(a: u32, b: u32) void {
6 if (a + b != 7) unreachable;
7}
8
9// run
10// backend=stage2
11// strategy=incremental
12//
test/incremental/add.1.zig created+14
...@@ -0,0 +1,14 @@
1pub fn main() void {
2 if (x - 7 != 0) unreachable;
3}
4
5fn add(a: u32, b: u32) u32 {
6 return a + b;
7}
8
9const x = add(3, 4);
10
11// run
12// backend=stage2
13// strategy=incremental
14//
test/incremental/add.2.zig created+14
...@@ -0,0 +1,14 @@
1pub fn main() void {
2 var x: usize = 3;
3 const y = add(1, 2, x);
4 if (y - 6 != 0) unreachable;
5}
6
7inline fn add(a: usize, b: usize, c: usize) usize {
8 return a + b + c;
9}
10
11// run
12// backend=stage2
13// strategy=incremental
14//