aboutsummaryrefslogtreecommitdiff
path: root/src/bun.js/test/snapshot.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/bun.js/test/snapshot.zig')
-rw-r--r--src/bun.js/test/snapshot.zig284
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;
+ }
+};