diff options
Diffstat (limited to 'src/bun.js/test/snapshot.zig')
-rw-r--r-- | src/bun.js/test/snapshot.zig | 284 |
1 files changed, 284 insertions, 0 deletions
diff --git a/src/bun.js/test/snapshot.zig b/src/bun.js/test/snapshot.zig new file mode 100644 index 000000000..12c7b3c36 --- /dev/null +++ b/src/bun.js/test/snapshot.zig @@ -0,0 +1,284 @@ +const std = @import("std"); +const bun = @import("root").bun; +const default_allocator = bun.default_allocator; +const string = bun.string; +const MutableString = bun.MutableString; +const strings = bun.strings; +const logger = bun.logger; +const jest = @import("./jest.zig"); +const Jest = jest.Jest; +const TestRunner = jest.TestRunner; +const js_parser = bun.js_parser; +const js_ast = bun.JSAst; +const JSC = bun.JSC; +const JSValue = JSC.JSValue; +const VirtualMachine = JSC.VirtualMachine; +const Expect = @import("./expect.zig").Expect; + +pub const Snapshots = struct { + const file_header = "// Bun Snapshot v1, https://goo.gl/fbAQLP\n"; + pub const ValuesHashMap = std.HashMap(usize, string, bun.IdentityContext(usize), std.hash_map.default_max_load_percentage); + + allocator: std.mem.Allocator, + update_snapshots: bool, + total: usize = 0, + added: usize = 0, + passed: usize = 0, + failed: usize = 0, + + file_buf: *std.ArrayList(u8), + values: *ValuesHashMap, + counts: *bun.StringHashMap(usize), + _current_file: ?File = null, + snapshot_dir_path: ?string = null, + + const File = struct { + id: TestRunner.File.ID, + file: std.fs.File, + }; + + pub fn getOrPut(this: *Snapshots, expect: *Expect, value: JSValue, hint: string, globalObject: *JSC.JSGlobalObject) !?string { + switch (try this.getSnapshotFile(expect.scope.file_id)) { + .result => {}, + .err => |err| { + return switch (err.syscall) { + .mkdir => error.FailedToMakeSnapshotDirectory, + .open => error.FailedToOpenSnapshotFile, + else => error.SnapshotFailed, + }; + }, + } + + const snapshot_name = try expect.getSnapshotName(this.allocator, hint); + this.total += 1; + + var count_entry = try this.counts.getOrPut(snapshot_name); + const counter = brk: { + if (count_entry.found_existing) { + this.allocator.free(snapshot_name); + count_entry.value_ptr.* += 1; + break :brk count_entry.value_ptr.*; + } + count_entry.value_ptr.* = 1; + break :brk count_entry.value_ptr.*; + }; + + const name = count_entry.key_ptr.*; + + var counter_string_buf = [_]u8{0} ** 32; + var counter_string = try std.fmt.bufPrint(&counter_string_buf, "{d}", .{counter}); + + var name_with_counter = try this.allocator.alloc(u8, name.len + 1 + counter_string.len); + defer this.allocator.free(name_with_counter); + bun.copy(u8, name_with_counter[0..name.len], name); + name_with_counter[name.len] = ' '; + bun.copy(u8, name_with_counter[name.len + 1 ..], counter_string); + + const name_hash = bun.hash(name_with_counter); + if (this.values.get(name_hash)) |expected| { + return expected; + } + + // doesn't exist. append to file bytes and add to hashmap. + var pretty_value = try MutableString.init(this.allocator, 0); + try value.jestSnapshotPrettyFormat(&pretty_value, globalObject); + + const serialized_length = "\nexports[`".len + name_with_counter.len + "`] = `".len + pretty_value.list.items.len + "`;\n".len; + try this.file_buf.ensureUnusedCapacity(serialized_length); + this.file_buf.appendSliceAssumeCapacity("\nexports[`"); + this.file_buf.appendSliceAssumeCapacity(name_with_counter); + this.file_buf.appendSliceAssumeCapacity("`] = `"); + this.file_buf.appendSliceAssumeCapacity(pretty_value.list.items); + this.file_buf.appendSliceAssumeCapacity("`;\n"); + + this.added += 1; + try this.values.put(name_hash, pretty_value.toOwnedSlice()); + return null; + } + + pub fn parseFile(this: *Snapshots) !void { + if (this.file_buf.items.len == 0) return; + + const vm = VirtualMachine.get(); + var opts = js_parser.Parser.Options.init(vm.bundler.options.jsx, .js); + var temp_log = logger.Log.init(this.allocator); + + const test_file = Jest.runner.?.files.get(this._current_file.?.id); + const test_filename = test_file.source.path.name.filename; + const dir_path = test_file.source.path.name.dirWithTrailingSlash(); + + var snapshot_file_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + var remain: []u8 = snapshot_file_path_buf[0..bun.MAX_PATH_BYTES]; + bun.copy(u8, remain, dir_path); + remain = remain[dir_path.len..]; + bun.copy(u8, remain, "__snapshots__/"); + remain = remain["__snapshots__/".len..]; + bun.copy(u8, remain, test_filename); + remain = remain[test_filename.len..]; + bun.copy(u8, remain, ".snap"); + remain = remain[".snap".len..]; + remain[0] = 0; + const snapshot_file_path = snapshot_file_path_buf[0 .. snapshot_file_path_buf.len - remain.len :0]; + + const source = logger.Source.initPathString(snapshot_file_path, this.file_buf.items); + + var parser = try js_parser.Parser.init( + opts, + &temp_log, + &source, + vm.bundler.options.define, + this.allocator, + ); + + var parse_result = try parser.parse(); + var ast = if (parse_result == .ast) parse_result.ast else return error.ParseError; + defer ast.deinit(); + + if (ast.exports_ref.isNull()) return; + const exports_ref = ast.exports_ref; + + // TODO: when common js transform changes, keep this updated or add flag to support this version + + const export_default = brk: { + for (ast.parts.slice()) |part| { + for (part.stmts) |stmt| { + if (stmt.data == .s_export_default and stmt.data.s_export_default.value == .expr) { + break :brk stmt.data.s_export_default.value.expr; + } + } + } + + return; + }; + + if (export_default.data == .e_call) { + const function_call = export_default.data.e_call; + if (function_call.args.len == 2 and function_call.args.ptr[0].data == .e_function) { + const arg_function_stmts = function_call.args.ptr[0].data.e_function.func.body.stmts; + for (arg_function_stmts) |stmt| { + switch (stmt.data) { + .s_expr => |expr| { + if (expr.value.data == .e_binary and expr.value.data.e_binary.op == .bin_assign) { + const left = expr.value.data.e_binary.left; + if (left.data == .e_index and left.data.e_index.index.data == .e_string and left.data.e_index.target.data == .e_identifier) { + const target: js_ast.E.Identifier = left.data.e_index.target.data.e_identifier; + var index: *js_ast.E.String = left.data.e_index.index.data.e_string; + if (target.ref.eql(exports_ref) and expr.value.data.e_binary.right.data == .e_string) { + const key = index.slice(this.allocator); + var value_string = expr.value.data.e_binary.right.data.e_string; + const value = value_string.slice(this.allocator); + defer { + if (!index.isUTF8()) this.allocator.free(key); + if (!value_string.isUTF8()) this.allocator.free(value); + } + const value_clone = try this.allocator.alloc(u8, value.len); + bun.copy(u8, value_clone, value); + const name_hash = bun.hash(key); + try this.values.put(name_hash, value_clone); + } + } + } + }, + else => {}, + } + } + } + } + } + + pub fn writeSnapshotFile(this: *Snapshots) !void { + if (this._current_file) |_file| { + var file = _file; + file.file.writeAll(this.file_buf.items) catch { + return error.FailedToWriteSnapshotFile; + }; + file.file.close(); + this.file_buf.clearAndFree(); + + var value_itr = this.values.valueIterator(); + while (value_itr.next()) |value| { + this.allocator.free(value.*); + } + this.values.clearAndFree(); + + var count_key_itr = this.counts.keyIterator(); + while (count_key_itr.next()) |key| { + this.allocator.free(key.*); + } + this.counts.clearAndFree(); + } + } + + fn getSnapshotFile(this: *Snapshots, file_id: TestRunner.File.ID) !JSC.Maybe(void) { + if (this._current_file == null or this._current_file.?.id != file_id) { + try this.writeSnapshotFile(); + + const test_file = Jest.runner.?.files.get(file_id); + const test_filename = test_file.source.path.name.filename; + const dir_path = test_file.source.path.name.dirWithTrailingSlash(); + + var snapshot_file_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + var remain: []u8 = snapshot_file_path_buf[0..bun.MAX_PATH_BYTES]; + bun.copy(u8, remain, dir_path); + remain = remain[dir_path.len..]; + bun.copy(u8, remain, "__snapshots__/"); + remain = remain["__snapshots__/".len..]; + + if (this.snapshot_dir_path == null or !strings.eqlLong(dir_path, this.snapshot_dir_path.?, true)) { + remain[0] = 0; + const snapshot_dir_path = snapshot_file_path_buf[0 .. snapshot_file_path_buf.len - remain.len :0]; + switch (JSC.Node.Syscall.mkdir(snapshot_dir_path, 0o777)) { + .result => this.snapshot_dir_path = dir_path, + .err => |err| { + switch (err.getErrno()) { + std.os.E.EXIST => this.snapshot_dir_path = dir_path, + else => return JSC.Maybe(void){ + .err = err, + }, + } + }, + } + } + + bun.copy(u8, remain, test_filename); + remain = remain[test_filename.len..]; + bun.copy(u8, remain, ".snap"); + remain = remain[".snap".len..]; + remain[0] = 0; + const snapshot_file_path = snapshot_file_path_buf[0 .. snapshot_file_path_buf.len - remain.len :0]; + + var flags: JSC.Node.Mode = std.os.O.CREAT | std.os.O.RDWR; + if (this.update_snapshots) flags |= std.os.O.TRUNC; + const fd = switch (JSC.Node.Syscall.open(snapshot_file_path, flags, 0o644)) { + .result => |_fd| _fd, + .err => |err| return JSC.Maybe(void){ + .err = err, + }, + }; + + var file: File = .{ + .id = file_id, + .file = .{ .handle = fd }, + }; + + if (this.update_snapshots) { + try this.file_buf.appendSlice(file_header); + } else { + const length = try file.file.getEndPos(); + if (length == 0) { + try this.file_buf.appendSlice(file_header); + } else { + const buf = try this.allocator.alloc(u8, length); + _ = try file.file.preadAll(buf, 0); + try this.file_buf.appendSlice(buf); + this.allocator.free(buf); + } + } + + this._current_file = file; + try this.parseFile(); + } + + return JSC.Maybe(void).success; + } +}; |