| ... | @@ -904,32 +904,12 @@ pub const TestContext = struct { | ... | @@ -904,32 +904,12 @@ pub const TestContext = struct { |
| 904 | incremental, | 904 | incremental, |
| 905 | }; | 905 | }; |
| 906 | | 906 | |
| 907 | /// Adds a compile-error test for each file in the provided directory, using the | 907 | /// Adds a test for each file in the provided directory, using the selected strategy. |
| 908 | /// selected backend and output mode. If `one_test_case_per_file` is true, a new | 908 | /// Recurses nested directories. |
| 909 | /// test case is created for each file. Otherwise, a single test case is used for | | |
| 910 | /// all tests. | | |
| 911 | /// | 909 | /// |
| 912 | /// Each file should include a test manifest as a contiguous block of comments at | 910 | /// Each file should include a test manifest as a contiguous block of comments at |
| 913 | /// the end of the file. The first line should be the test case name, followed by | 911 | /// the end of the file. The first line should be the test type, followed by a set of |
| 914 | /// a blank line, then one expected errors on each line in the form | 912 | /// key-value config values, followed by a blank line, then the expected output. |
| 915 | /// `:line:column: error: message` | | |
| 916 | pub fn addErrorCasesFromDir( | | |
| 917 | ctx: *TestContext, | | |
| 918 | name: []const u8, | | |
| 919 | dir: std.fs.Dir, | | |
| 920 | backend: Backend, | | |
| 921 | output_mode: std.builtin.OutputMode, | | |
| 922 | is_test: bool, | | |
| 923 | strategy: Strategy, | | |
| 924 | ) void { | | |
| 925 | var current_file: []const u8 = "none"; | | |
| 926 | addErrorCasesFromDirInner(ctx, name, dir, backend, output_mode, is_test, strategy, &current_file) catch |err| { | | |
| 927 | std.debug.panic("test harness failed to process file '{s}': {s}\n", .{ | | |
| 928 | current_file, @errorName(err), | | |
| 929 | }); | | |
| 930 | }; | | |
| 931 | } | | |
| 932 | | | |
| 933 | pub fn addTestCasesFromDir(ctx: *TestContext, dir: std.fs.Dir, strategy: Strategy) void { | 913 | pub fn addTestCasesFromDir(ctx: *TestContext, dir: std.fs.Dir, strategy: Strategy) void { |
| 934 | var current_file: []const u8 = "none"; | 914 | var current_file: []const u8 = "none"; |
| 935 | ctx.addTestCasesFromDirInner(dir, strategy, &current_file) catch |err| { | 915 | ctx.addTestCasesFromDirInner(dir, strategy, &current_file) catch |err| { |
| ... | @@ -1127,148 +1107,6 @@ pub const TestContext = struct { | ... | @@ -1127,148 +1107,6 @@ pub const TestContext = struct { |
| 1127 | } | 1107 | } |
| 1128 | } | 1108 | } |
| 1129 | | 1109 | |
| 1130 | fn addErrorCasesFromDirInner( | | |
| 1131 | ctx: *TestContext, | | |
| 1132 | name: []const u8, | | |
| 1133 | dir: std.fs.Dir, | | |
| 1134 | backend: Backend, | | |
| 1135 | output_mode: std.builtin.OutputMode, | | |
| 1136 | is_test: bool, | | |
| 1137 | strategy: Strategy, | | |
| 1138 | /// This is kept up to date with the currently being processed file so | | |
| 1139 | /// that if any errors occur the caller knows it happened during this file. | | |
| 1140 | current_file: *[]const u8, | | |
| 1141 | ) !void { | | |
| 1142 | var opt_case: ?*Case = null; | | |
| 1143 | | | |
| 1144 | var it = dir.iterate(); | | |
| 1145 | var filenames = std.ArrayList([]const u8).init(ctx.arena); | | |
| 1146 | defer filenames.deinit(); | | |
| 1147 | | | |
| 1148 | while (try it.next()) |entry| { | | |
| 1149 | if (entry.kind != .File) continue; | | |
| 1150 | | | |
| 1151 | // Ignore stuff such as .swp files | | |
| 1152 | switch (Compilation.classifyFileExt(entry.name)) { | | |
| 1153 | .unknown => continue, | | |
| 1154 | else => {}, | | |
| 1155 | } | | |
| 1156 | try filenames.append(try ctx.arena.dupe(u8, entry.name)); | | |
| 1157 | } | | |
| 1158 | | | |
| 1159 | // Sort filenames, so that incremental tests are contiguous and in-order | | |
| 1160 | sortTestFilenames(filenames.items); | | |
| 1161 | | | |
| 1162 | var prev_filename: []const u8 = ""; | | |
| 1163 | for (filenames.items) |filename| { | | |
| 1164 | current_file.* = filename; | | |
| 1165 | | | |
| 1166 | { // First, check if this file is part of an incremental update sequence | | |
| 1167 | | | |
| 1168 | // Split filename into "<base_name>.<index>.<file_ext>" | | |
| 1169 | const prev_parts = getTestFileNameParts(prev_filename); | | |
| 1170 | const new_parts = getTestFileNameParts(filename); | | |
| 1171 | | | |
| 1172 | // If base_name and file_ext match, these files are in the same test sequence | | |
| 1173 | // and the new one should be the incremented version of the previous test | | |
| 1174 | if (std.mem.eql(u8, prev_parts.base_name, new_parts.base_name) and | | |
| 1175 | std.mem.eql(u8, prev_parts.file_ext, new_parts.file_ext)) | | |
| 1176 | { | | |
| 1177 | | | |
| 1178 | // This is "foo.X.zig" followed by "foo.Y.zig". Make sure that X = Y + 1 | | |
| 1179 | if (prev_parts.test_index == null) return error.InvalidIncrementalTestIndex; | | |
| 1180 | if (new_parts.test_index == null) return error.InvalidIncrementalTestIndex; | | |
| 1181 | if (new_parts.test_index.? != prev_parts.test_index.? + 1) return error.InvalidIncrementalTestIndex; | | |
| 1182 | } else { | | |
| 1183 | | | |
| 1184 | // This is not the same test sequence, so the new file must be the first file | | |
| 1185 | // in a new sequence ("*.0.zig") or an independent test file ("*.zig") | | |
| 1186 | if (new_parts.test_index != null and new_parts.test_index.? != 0) return error.InvalidIncrementalTestIndex; | | |
| 1187 | | | |
| 1188 | if (strategy == .independent) | | |
| 1189 | opt_case = null; // Generate a new independent test case for this update | | |
| 1190 | } | | |
| 1191 | } | | |
| 1192 | prev_filename = filename; | | |
| 1193 | | | |
| 1194 | const max_file_size = 10 * 1024 * 1024; | | |
| 1195 | const src = try dir.readFileAllocOptions(ctx.arena, filename, max_file_size, null, 1, 0); | | |
| 1196 | | | |
| 1197 | // The manifest is the last contiguous block of comments in the file | | |
| 1198 | // We scan for the beginning by searching backward for the first non-empty line that does not start with "//" | | |
| 1199 | var manifest_start: ?usize = null; | | |
| 1200 | var manifest_end: usize = src.len; | | |
| 1201 | if (src.len > 0) { | | |
| 1202 | var cursor: usize = src.len - 1; | | |
| 1203 | while (true) { | | |
| 1204 | // Move to beginning of line | | |
| 1205 | while (cursor > 0 and src[cursor - 1] != '\n') cursor -= 1; | | |
| 1206 | | | |
| 1207 | if (std.mem.startsWith(u8, src[cursor..], "//")) { | | |
| 1208 | manifest_start = cursor; // Contiguous comment line, include in manifest | | |
| 1209 | } else { | | |
| 1210 | if (manifest_start != null) break; // Encountered non-comment line, end of manifest | | |
| 1211 | | | |
| 1212 | // We ignore all-whitespace lines following the comment block, but anything else | | |
| 1213 | // means that there is no manifest present. | | |
| 1214 | if (std.mem.trim(u8, src[cursor..manifest_end], " \r\n\t").len == 0) { | | |
| 1215 | manifest_end = cursor; | | |
| 1216 | } else break; // If it's not whitespace, there is no manifest | | |
| 1217 | } | | |
| 1218 | | | |
| 1219 | // Move to previous line | | |
| 1220 | if (cursor != 0) cursor -= 1 else break; | | |
| 1221 | } | | |
| 1222 | } | | |
| 1223 | | | |
| 1224 | var errors = std.ArrayList([]const u8).init(ctx.arena); | | |
| 1225 | | | |
| 1226 | if (manifest_start) |start| { | | |
| 1227 | // Due to the above processing, we know that this is a contiguous block of comments | | |
| 1228 | // and do not need to re-validate the leading "//" on each line | | |
| 1229 | var manifest_it = std.mem.tokenize(u8, src[start..manifest_end], "\r\n"); | | |
| 1230 | | | |
| 1231 | // First line is the test case name | | |
| 1232 | const first_line = manifest_it.next() orelse return error.MissingTestCaseName; | | |
| 1233 | const case_name = try std.mem.concat(ctx.arena, u8, &.{ name, ": ", std.mem.trim(u8, first_line[2..], " \t") }); | | |
| 1234 | | | |
| 1235 | // If the second line is present, it should be blank | | |
| 1236 | if (manifest_it.next()) |second_line| { | | |
| 1237 | if (std.mem.trim(u8, second_line[2..], " \t").len != 0) return error.SecondLineNotBlank; | | |
| 1238 | } | | |
| 1239 | | | |
| 1240 | // All following lines are expected error messages | | |
| 1241 | while (manifest_it.next()) |line| try errors.append(try ctx.arena.dupe(u8, std.mem.trim(u8, line[2..], " \t"))); | | |
| 1242 | | | |
| 1243 | const case = opt_case orelse case: { | | |
| 1244 | ctx.cases.append(TestContext.Case{ | | |
| 1245 | .name = name, | | |
| 1246 | .target = .{}, | | |
| 1247 | .backend = backend, | | |
| 1248 | .updates = std.ArrayList(TestContext.Update).init(ctx.cases.allocator), | | |
| 1249 | .is_test = is_test, | | |
| 1250 | .output_mode = output_mode, | | |
| 1251 | .files = std.ArrayList(TestContext.File).init(ctx.cases.allocator), | | |
| 1252 | }) catch @panic("out of memory"); | | |
| 1253 | const case = &ctx.cases.items[ctx.cases.items.len - 1]; | | |
| 1254 | opt_case = case; | | |
| 1255 | break :case case; | | |
| 1256 | }; | | |
| 1257 | switch (strategy) { | | |
| 1258 | .independent => { | | |
| 1259 | case.name = case_name; | | |
| 1260 | case.addError(src, errors.items); | | |
| 1261 | }, | | |
| 1262 | .incremental => { | | |
| 1263 | case.addErrorNamed(case_name, src, errors.items); | | |
| 1264 | }, | | |
| 1265 | } | | |
| 1266 | } else { | | |
| 1267 | return error.MissingManifest; | | |
| 1268 | } | | |
| 1269 | } | | |
| 1270 | } | | |
| 1271 | | | |
| 1272 | fn init(gpa: Allocator, arena: Allocator) TestContext { | 1110 | fn init(gpa: Allocator, arena: Allocator) TestContext { |
| 1273 | return .{ | 1111 | return .{ |
| 1274 | .cases = std.ArrayList(Case).init(gpa), | 1112 | .cases = std.ArrayList(Case).init(gpa), |