aboutsummaryrefslogtreecommitdiff
path: root/src/cli/test_command.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/cli/test_command.zig')
-rw-r--r--src/cli/test_command.zig397
1 files changed, 397 insertions, 0 deletions
diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig
new file mode 100644
index 000000000..60d03392a
--- /dev/null
+++ b/src/cli/test_command.zig
@@ -0,0 +1,397 @@
+const _global = @import("../global.zig");
+const string = _global.string;
+const Output = _global.Output;
+const Global = _global.Global;
+const Environment = _global.Environment;
+const strings = _global.strings;
+const MutableString = _global.MutableString;
+const stringZ = _global.stringZ;
+const default_allocator = _global.default_allocator;
+const C = _global.C;
+const std = @import("std");
+
+const lex = @import("../js_lexer.zig");
+const logger = @import("../logger.zig");
+
+const FileSystem = @import("../fs.zig").FileSystem;
+const options = @import("../options.zig");
+const js_parser = @import("../js_parser.zig");
+const json_parser = @import("../json_parser.zig");
+const js_printer = @import("../js_printer.zig");
+const js_ast = @import("../js_ast.zig");
+const linker = @import("../linker.zig");
+const panicky = @import("../panic_handler.zig");
+const sync = @import("../sync.zig");
+const Api = @import("../api/schema.zig").Api;
+const resolve_path = @import("../resolver/resolve_path.zig");
+const configureTransformOptionsForBun = @import("../javascript/jsc/config.zig").configureTransformOptionsForBun;
+const Command = @import("../cli.zig").Command;
+const bundler = @import("../bundler.zig");
+const NodeModuleBundle = @import("../node_module_bundle.zig").NodeModuleBundle;
+const DotEnv = @import("../env_loader.zig");
+const which = @import("../which.zig").which;
+const Run = @import("../bun_js.zig").Run;
+var path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
+var path_buf2: [std.fs.MAX_PATH_BYTES]u8 = undefined;
+const PathString = _global.PathString;
+
+const JSC = @import("javascript_core");
+const Jest = JSC.Jest;
+const TestRunner = JSC.Jest.TestRunner;
+const Test = TestRunner.Test;
+pub const CommandLineReporter = struct {
+ jest: TestRunner,
+ callback: TestRunner.Callback,
+ last_dot: u32 = 0,
+ summary: Summary = Summary{},
+
+ pub const Summary = struct {
+ pass: u32 = 0,
+ expectations: u32 = 0,
+ fail: u32 = 0,
+ };
+
+ const DotColorMap = std.EnumMap(TestRunner.Test.Status, string);
+ const dots: DotColorMap = brk: {
+ var map: DotColorMap = DotColorMap.init(.{});
+ map.put(TestRunner.Test.Status.pending, Output.RESET ++ Output.ED ++ Output.color_map.get("yellow").? ++ "." ++ Output.RESET);
+ map.put(TestRunner.Test.Status.pass, Output.RESET ++ Output.ED ++ Output.color_map.get("green").? ++ "." ++ Output.RESET);
+ map.put(TestRunner.Test.Status.fail, Output.RESET ++ Output.ED ++ Output.color_map.get("red").? ++ "." ++ Output.RESET);
+ break :brk map;
+ };
+
+ fn updateDots(this: *CommandLineReporter) void {
+ const statuses = this.jest.tests.items(.status);
+ var writer = Output.errorWriter();
+ writer.writeAll("\r") catch unreachable;
+ if (Output.enable_ansi_colors_stderr) {
+ for (statuses) |status| {
+ writer.writeAll(dots.get(status).?) catch unreachable;
+ }
+ } else {
+ for (statuses) |_| {
+ writer.writeAll(".") catch unreachable;
+ }
+ }
+ }
+
+ pub fn handleUpdateCount(cb: *TestRunner.Callback, _: u32, _: u32) void {
+ _ = cb;
+ }
+
+ pub fn handleTestStart(_: *TestRunner.Callback, _: Test.ID) void {
+ // var this: *CommandLineReporter = @fieldParentPtr(CommandLineReporter, "callback", cb);
+ }
+ pub fn handleTestPass(cb: *TestRunner.Callback, _: Test.ID, expectations: u32) void {
+ var this: *CommandLineReporter = @fieldParentPtr(CommandLineReporter, "callback", cb);
+ // this.updateDots();
+ this.summary.pass += 1;
+ this.summary.expectations += expectations;
+ }
+ pub fn handleTestFail(cb: *TestRunner.Callback, test_id: Test.ID, _: string, _: string, _: u32) void {
+ // var this: *CommandLineReporter = @fieldParentPtr(CommandLineReporter, "callback", cb);
+ var this: *CommandLineReporter = @fieldParentPtr(CommandLineReporter, "callback", cb);
+ // this.updateDots();
+ this.summary.fail += 1;
+ _ = test_id;
+ }
+};
+
+const Scanner = struct {
+ const Fifo = std.fifo.LinearFifo(ScanEntry, .Dynamic);
+ exclusion_names: []const []const u8 = &.{},
+ filter_names: []const []const u8 = &.{},
+ dirs_to_scan: Fifo,
+ results: std.ArrayList(_global.PathString),
+ fs: *FileSystem,
+ open_dir_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined,
+ scan_dir_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined,
+ options: *options.BundleOptions,
+ has_iterated: bool = false,
+
+ const ScanEntry = struct {
+ relative_dir: _global.StoredFileDescriptorType,
+ dir_path: string,
+ name: strings.StringOrTinyString,
+ };
+
+ fn readDirWithName(this: *Scanner, name: string, handle: ?std.fs.Dir) !*FileSystem.RealFS.EntriesOption {
+ return try this.fs.fs.readDirectoryWithIterator(name, handle, *Scanner, this);
+ }
+
+ pub fn scan(this: *Scanner, path_literal: string) void {
+ var parts = &[_]string{ this.fs.top_level_dir, path_literal };
+ const path = this.fs.absBuf(parts, &this.scan_dir_buf);
+ var root = this.readDirWithName(path, null) catch |err| {
+ if (err == error.NotDir) {
+ if (this.isTestFile(path)) {
+ this.results.append(_global.PathString.init(this.fs.filename_store.append(@TypeOf(path), path) catch unreachable)) catch unreachable;
+ }
+ }
+
+ return;
+ };
+
+ // you typed "." and we already scanned it
+ if (!this.has_iterated) {
+ if (@as(FileSystem.RealFS.EntriesOption.Tag, root.*) == .entries) {
+ var iter = root.entries.data.iterator();
+ const fd = root.entries.fd;
+ while (iter.next()) |entry| {
+ this.next(entry.value, fd);
+ }
+ }
+ }
+
+ while (this.dirs_to_scan.readItem()) |entry| {
+ var dir = std.fs.Dir{ .fd = entry.relative_dir };
+ var parts2 = &[_]string{ entry.dir_path, entry.name.slice() };
+ var path2 = this.fs.absBuf(parts2, &this.open_dir_buf);
+ this.open_dir_buf[path2.len] = 0;
+ var pathZ = this.open_dir_buf[path2.len - entry.name.slice().len .. path2.len :0];
+ var child_dir = dir.openDirZ(pathZ, .{ .iterate = true }) catch continue;
+ path2 = this.fs.dirname_store.append(string, path2) catch unreachable;
+ FileSystem.setMaxFd(child_dir.fd);
+ _ = this.readDirWithName(path2, child_dir) catch continue;
+ }
+ }
+
+ const test_name_suffixes = [_]string{
+ ".test",
+ "_test",
+ ".spec",
+ "_spec",
+ };
+
+ pub fn couldBeTestFile(this: *Scanner, name: string) bool {
+ const extname = std.fs.path.extension(name);
+ if (!this.options.loader(extname).isJavaScriptLike()) return false;
+ const name_without_extension = name[0 .. name.len - extname.len];
+ inline for (test_name_suffixes) |suffix| {
+ if (strings.endsWithComptime(name_without_extension, suffix)) return true;
+ }
+
+ return false;
+ }
+
+ pub fn doesAbsolutePathMatchFilter(this: *Scanner, name: string) bool {
+ if (this.filter_names.len == 0) return true;
+
+ for (this.filter_names) |filter_name| {
+ if (strings.contains(name, filter_name)) return true;
+ }
+
+ return false;
+ }
+
+ pub fn isTestFile(this: *Scanner, name: string) bool {
+ return this.couldBeTestFile(name) and this.doesAbsolutePathMatchFilter(name);
+ }
+
+ pub fn next(this: *Scanner, entry: *FileSystem.Entry, fd: _global.StoredFileDescriptorType) void {
+ const name = entry.base_lowercase();
+ this.has_iterated = true;
+ switch (entry.kind(&this.fs.fs)) {
+ .dir => {
+ if (strings.eqlComptime(name, "node_modules") or strings.eqlComptime(name, ".git")) {
+ return;
+ }
+
+ for (this.exclusion_names) |exclude_name| {
+ if (strings.eql(exclude_name, name)) return;
+ }
+
+ this.dirs_to_scan.writeItem(.{
+ .relative_dir = fd,
+ .name = entry.base_,
+ .dir_path = entry.dir,
+ }) catch unreachable;
+ },
+ .file => {
+ // already seen it!
+ if (!entry.abs_path.isEmpty()) return;
+
+ if (!this.couldBeTestFile(name)) return;
+
+ var parts = &[_]string{ entry.dir, entry.base() };
+ const path = this.fs.absBuf(parts, &this.open_dir_buf);
+
+ if (!this.doesAbsolutePathMatchFilter(path)) return;
+
+ entry.abs_path = _global.PathString.init(this.fs.filename_store.append(@TypeOf(path), path) catch unreachable);
+ this.results.append(entry.abs_path) catch unreachable;
+ },
+ }
+ }
+};
+
+pub const TestCommand = struct {
+ pub const name = "wiptest";
+ pub fn exec(ctx: Command.Context) !void {
+ var env_loader = brk: {
+ var map = try ctx.allocator.create(DotEnv.Map);
+ map.* = DotEnv.Map.init(ctx.allocator);
+
+ var loader = try ctx.allocator.create(DotEnv.Loader);
+ loader.* = DotEnv.Loader.init(map, ctx.allocator);
+ break :brk loader;
+ };
+ JSC.C.JSCInitialize();
+ var reporter = try ctx.allocator.create(CommandLineReporter);
+ reporter.* = CommandLineReporter{
+ .jest = TestRunner{
+ .allocator = ctx.allocator,
+ .log = ctx.log,
+ .callback = undefined,
+ },
+ .callback = undefined,
+ };
+ reporter.callback = TestRunner.Callback{
+ .onUpdateCount = CommandLineReporter.handleUpdateCount,
+ .onTestStart = CommandLineReporter.handleTestStart,
+ .onTestPass = CommandLineReporter.handleTestPass,
+ .onTestFail = CommandLineReporter.handleTestFail,
+ };
+ reporter.jest.callback = &reporter.callback;
+ Jest.Jest.runner = &reporter.jest;
+
+ js_ast.Expr.Data.Store.create(default_allocator);
+ js_ast.Stmt.Data.Store.create(default_allocator);
+ var vm = try JSC.VirtualMachine.init(ctx.allocator, ctx.args, null, ctx.log, env_loader);
+ vm.argv = ctx.positionals;
+
+ try vm.bundler.configureDefines();
+
+ var scanner = Scanner{
+ .dirs_to_scan = Scanner.Fifo.init(ctx.allocator),
+ .options = &vm.bundler.options,
+ .fs = vm.bundler.fs,
+ .filter_names = ctx.positionals[1..],
+ .results = std.ArrayList(PathString).init(ctx.allocator),
+ };
+
+ scanner.scan(scanner.fs.top_level_dir);
+ scanner.dirs_to_scan.deinit();
+
+ const test_files = scanner.results.toOwnedSlice();
+
+ // vm.bundler.fs.fs.readDirectory(_dir: string, _handle: ?std.fs.Dir)
+ runAllTests(reporter, vm, test_files, ctx.allocator);
+
+ Output.pretty("\n", .{});
+ Output.flush();
+
+ Output.prettyError("\n", .{});
+
+ if (reporter.summary.pass > 0) {
+ Output.prettyError("<r><green>", .{});
+ }
+
+ Output.prettyError(" {d:5>} pass<r>\n", .{reporter.summary.pass});
+
+ if (reporter.summary.fail > 0) {
+ Output.prettyError("<r><red>", .{});
+ } else {
+ Output.prettyError("<r><d>", .{});
+ }
+
+ Output.prettyError(" {d:5>} fail<r>\n", .{reporter.summary.fail});
+
+ if (reporter.summary.fail == 0 and reporter.summary.expectations > 0) {
+ Output.prettyError("<r><green>", .{});
+ } else {
+ Output.prettyError("<r>", .{});
+ }
+ Output.prettyError(" {d:5>} expectations\n", .{reporter.summary.expectations});
+
+ Output.prettyError(
+ \\ Ran {d} tests across {d} files
+ , .{
+ reporter.summary.fail + reporter.summary.pass,
+ test_files.len,
+ });
+ Output.printStartEnd(ctx.start_time, std.time.nanoTimestamp());
+ Output.prettyError("\n", .{});
+
+ Output.flush();
+
+ if (reporter.summary.fail > 0) {
+ std.os.exit(1);
+ }
+ }
+
+ pub fn runAllTests(
+ reporter_: *CommandLineReporter,
+ vm_: *JSC.VirtualMachine,
+ files_: []const PathString,
+ allocator_: std.mem.Allocator,
+ ) void {
+ const Context = struct {
+ reporter: *CommandLineReporter,
+ vm: *JSC.VirtualMachine,
+ files: []const PathString,
+ allocator: std.mem.Allocator,
+ pub fn begin(this: *@This()) void {
+ var reporter = this.reporter;
+ var vm = this.vm;
+ var files = this.files;
+ var allocator = this.allocator;
+ for (files) |file_name| {
+ TestCommand.run(reporter, vm, file_name.slice(), allocator) catch {};
+ }
+ }
+ };
+ var ctx = Context{ .reporter = reporter_, .vm = vm_, .files = files_, .allocator = allocator_ };
+ vm_.runWithAPILock(Context, &ctx, Context.begin);
+ }
+
+ pub fn run(
+ reporter: *CommandLineReporter,
+ vm: *JSC.VirtualMachine,
+ file_name: string,
+ _: std.mem.Allocator,
+ ) !void {
+ defer {
+ js_ast.Expr.Data.Store.reset();
+ js_ast.Stmt.Data.Store.reset();
+
+ if (vm.log.errors > 0) {
+ if (Output.enable_ansi_colors) {
+ vm.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true) catch {};
+ } else {
+ vm.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false) catch {};
+ }
+ vm.log.msgs.clearRetainingCapacity();
+ vm.log.errors = 0;
+ }
+
+ Output.flush();
+ }
+
+ var file_start = reporter.jest.files.len;
+ var resolution = try vm.bundler.resolveEntryPoint(file_name);
+
+ var promise = try vm.loadEntryPoint(resolution.path_pair.primary.text);
+
+ while (promise.status(vm.global.vm()) == .Pending) {
+ vm.tick();
+ }
+
+ var result = promise.result(vm.global.vm());
+ if (result.isError() or
+ result.isAggregateError(vm.global) or
+ result.isException(vm.global.vm()))
+ {
+ vm.defaultErrorHandler(result, null);
+ }
+
+ reporter.updateDots();
+
+ var modules: []*Jest.DescribeScope = reporter.jest.files.items(.module_scope)[file_start..];
+ for (modules) |module| {
+ module.runTests(vm.global.ref());
+ }
+
+ reporter.updateDots();
+ }
+};