| ... | ... | @@ -904,32 +904,12 @@ pub const TestContext = struct { |
| 904 | 904 | incremental, |
| 905 | 905 | }; |
| 906 | 906 | |
| 907 | | /// Adds a compile-error test for each file in the provided directory, using the |
| 908 | | /// selected backend and output mode. If `one_test_case_per_file` is true, a new |
| 909 | | /// test case is created for each file. Otherwise, a single test case is used for |
| 910 | | /// all tests. |
| 907 | /// Adds a test for each file in the provided directory, using the selected strategy. |
| 908 | /// Recurses nested directories. |
| 911 | 909 | /// |
| 912 | 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 |
| 914 | | /// a blank line, then one expected errors on each line in the form |
| 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 | | |
| 911 | /// the end of the file. The first line should be the test type, followed by a set of |
| 912 | /// key-value config values, followed by a blank line, then the expected output. |
| 933 | 913 | pub fn addTestCasesFromDir(ctx: *TestContext, dir: std.fs.Dir, strategy: Strategy) void { |
| 934 | 914 | var current_file: []const u8 = "none"; |
| 935 | 915 | ctx.addTestCasesFromDirInner(dir, strategy, &current_file) catch |err| { |
| ... | ... | @@ -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 | 1110 | fn init(gpa: Allocator, arena: Allocator) TestContext { |
| 1273 | 1111 | return .{ |
| 1274 | 1112 | .cases = std.ArrayList(Case).init(gpa), |