aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
m---------src/bun.js/WebKit0
-rw-r--r--src/bun.js/bindings/CodeCoverage.cpp44
-rw-r--r--src/bun.js/bindings/InternalModuleRegistry.cpp16
-rw-r--r--src/bun.js/bindings/ZigSourceProvider.cpp41
-rw-r--r--src/bun.js/bindings/ZigSourceProvider.h4
-rw-r--r--src/bun.js/bindings/bindings.cpp9
-rw-r--r--src/bun.js/bindings/bindings.zig12
-rw-r--r--src/bun.js/bindings/headers.h1
-rw-r--r--src/bun.js/bindings/headers.zig1
-rw-r--r--src/bun.js/javascript.zig28
-rw-r--r--src/bun.js/modules/BunJSCModule.h60
-rw-r--r--src/bun.js/test/jest.zig1
-rw-r--r--src/bun.zig5
-rw-r--r--src/bunfig.zig41
-rw-r--r--src/cli.zig7
-rw-r--r--src/cli/test_command.zig160
-rw-r--r--src/js_printer.zig1
-rw-r--r--src/options.zig2
-rw-r--r--src/sourcemap/CodeCoverage.zig646
-rw-r--r--src/sourcemap/sourcemap.zig60
20 files changed, 1113 insertions, 26 deletions
diff --git a/src/bun.js/WebKit b/src/bun.js/WebKit
-Subproject 9d9172d3242b40f16fa980ead750dc9ab248065
+Subproject 74609640b2a7c5a1588b824f870d1b0ff91bfd8
diff --git a/src/bun.js/bindings/CodeCoverage.cpp b/src/bun.js/bindings/CodeCoverage.cpp
new file mode 100644
index 000000000..1cb3b6ba2
--- /dev/null
+++ b/src/bun.js/bindings/CodeCoverage.cpp
@@ -0,0 +1,44 @@
+#include "root.h"
+#include "ZigSourceProvider.h"
+#include <JavaScriptCore/ControlFlowProfiler.h>
+
+using namespace JSC;
+
+extern "C" bool CodeCoverage__withBlocksAndFunctions(
+ JSC::VM* vmPtr,
+ JSC::SourceID sourceID,
+ void* ctx,
+ bool ignoreSourceMap,
+ void (*blockCallback)(void* ctx, JSC::BasicBlockRange* range, size_t len, size_t functionOffset, bool ignoreSourceMap))
+{
+
+ VM& vm = *vmPtr;
+
+ auto basicBlocks = vm.controlFlowProfiler()->getBasicBlocksForSourceIDWithoutFunctionRange(
+ sourceID, vm);
+
+ if (basicBlocks.isEmpty()) {
+ blockCallback(ctx, nullptr, 0, 0, ignoreSourceMap);
+ return true;
+ }
+
+ size_t functionStartOffset = basicBlocks.size();
+
+ const Vector<std::tuple<bool, unsigned, unsigned>>& functionRanges = vm.functionHasExecutedCache()->getFunctionRanges(sourceID);
+
+ basicBlocks.reserveCapacity(functionRanges.size() + basicBlocks.size());
+
+ for (const auto& functionRange : functionRanges) {
+ BasicBlockRange range;
+ range.m_hasExecuted = std::get<0>(functionRange);
+ range.m_startOffset = static_cast<int>(std::get<1>(functionRange));
+ range.m_endOffset = static_cast<int>(std::get<2>(functionRange));
+ range.m_executionCount = range.m_hasExecuted
+ ? 1
+ : 0; // This is a hack. We don't actually count this.
+ basicBlocks.append(range);
+ }
+
+ blockCallback(ctx, basicBlocks.data(), basicBlocks.size(), functionStartOffset, ignoreSourceMap);
+ return true;
+}
diff --git a/src/bun.js/bindings/InternalModuleRegistry.cpp b/src/bun.js/bindings/InternalModuleRegistry.cpp
index e6b574d7b..841360502 100644
--- a/src/bun.js/bindings/InternalModuleRegistry.cpp
+++ b/src/bun.js/bindings/InternalModuleRegistry.cpp
@@ -12,6 +12,20 @@
namespace Bun {
+extern "C" bool BunTest__shouldGenerateCodeCoverage(BunString sourceURL);
+extern "C" void ByteRangeMapping__generate(BunString sourceURL, BunString code, int sourceID);
+
+static void maybeAddCodeCoverage(JSC::VM& vm, const JSC::SourceCode& code)
+{
+#ifdef BUN_DEBUG
+ bool isCodeCoverageEnabled = !!vm.controlFlowProfiler();
+ bool shouldGenerateCodeCoverage = isCodeCoverageEnabled && BunTest__shouldGenerateCodeCoverage(Bun::toString(code.provider()->sourceURL()));
+ if (shouldGenerateCodeCoverage) {
+ ByteRangeMapping__generate(Bun::toString(code.provider()->sourceURL()), Bun::toString(code.provider()->source().toStringWithoutCopying()), code.provider()->asID());
+ }
+#endif
+}
+
// The `INTERNAL_MODULE_REGISTRY_GENERATE` macro handles inlining code to compile and run a
// JS builtin that acts as a module. In debug mode, we use a different implementation that reads
// from the developer's filesystem. This allows reloading code without recompiling bindings.
@@ -20,7 +34,7 @@ namespace Bun {
auto throwScope = DECLARE_THROW_SCOPE(vm); \
auto&& origin = SourceOrigin(WTF::URL(makeString("builtin://"_s, moduleName))); \
SourceCode source = JSC::makeSource(SOURCE, origin, moduleName); \
- \
+ maybeAddCodeCoverage(vm, source); \
JSFunction* func \
= JSFunction::create( \
vm, \
diff --git a/src/bun.js/bindings/ZigSourceProvider.cpp b/src/bun.js/bindings/ZigSourceProvider.cpp
index 2c448b5a6..d11c748da 100644
--- a/src/bun.js/bindings/ZigSourceProvider.cpp
+++ b/src/bun.js/bindings/ZigSourceProvider.cpp
@@ -58,11 +58,48 @@ static SourceOrigin toSourceOrigin(const String& sourceURL, bool isBuiltin)
return SourceOrigin(WTF::URL::fileURLWithFileSystemPath(sourceURL));
}
+void forEachSourceProvider(const WTF::Function<void(JSC::SourceID)>& func)
+{
+ // if (sourceProviderMap == nullptr) {
+ // return;
+ // }
+
+ // for (auto& pair : *sourceProviderMap) {
+ // auto sourceProvider = pair.value;
+ // if (sourceProvider) {
+ // func(sourceProvider);
+ // }
+ // }
+}
+extern "C" int ByteRangeMapping__getSourceID(void* mappings, BunString sourceURL);
+extern "C" void* ByteRangeMapping__find(BunString sourceURL);
+void* sourceMappingForSourceURL(const WTF::String& sourceURL)
+{
+ return ByteRangeMapping__find(Bun::toString(sourceURL));
+}
+
+extern "C" void ByteRangeMapping__generate(BunString sourceURL, BunString code, int sourceID);
+
+JSC::SourceID sourceIDForSourceURL(const WTF::String& sourceURL)
+{
+ void* mappings = ByteRangeMapping__find(Bun::toString(sourceURL));
+ if (!mappings) {
+ return 0;
+ }
+
+ return ByteRangeMapping__getSourceID(mappings, Bun::toString(sourceURL));
+}
+
+extern "C" bool BunTest__shouldGenerateCodeCoverage(BunString sourceURL);
+
Ref<SourceProvider> SourceProvider::create(Zig::GlobalObject* globalObject, ResolvedSource resolvedSource, JSC::SourceProviderSourceType sourceType, bool isBuiltin)
{
auto stringImpl = Bun::toWTFString(resolvedSource.source_code);
auto sourceURLString = toStringCopy(resolvedSource.source_url);
+ bool isCodeCoverageEnabled = !!globalObject->vm().controlFlowProfiler();
+
+ bool shouldGenerateCodeCoverage = isCodeCoverageEnabled && !isBuiltin && BunTest__shouldGenerateCodeCoverage(Bun::toString(sourceURLString));
auto provider = adoptRef(*new SourceProvider(
globalObject->isThreadLocalDefaultGlobalObject ? globalObject : nullptr,
@@ -71,6 +108,10 @@ Ref<SourceProvider> SourceProvider::create(Zig::GlobalObject* globalObject, Reso
sourceURLString.impl(), TextPosition(),
sourceType));
+ if (shouldGenerateCodeCoverage) {
+ ByteRangeMapping__generate(Bun::toString(provider->sourceURL()), Bun::toString(provider->source().toStringWithoutCopying()), provider->asID());
+ }
+
return provider;
}
diff --git a/src/bun.js/bindings/ZigSourceProvider.h b/src/bun.js/bindings/ZigSourceProvider.h
index c189cc454..364e6ee23 100644
--- a/src/bun.js/bindings/ZigSourceProvider.h
+++ b/src/bun.js/bindings/ZigSourceProvider.h
@@ -22,6 +22,10 @@ namespace Zig {
class GlobalObject;
+void forEachSourceProvider(WTF::Function<void(JSC::SourceID)>);
+JSC::SourceID sourceIDForSourceURL(const WTF::String& sourceURL);
+void* sourceMappingForSourceURL(const WTF::String& sourceURL);
+
class SourceProvider final : public JSC::SourceProvider {
WTF_MAKE_FAST_ALLOCATED;
using Base = JSC::SourceProvider;
diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp
index b0a291c2e..201fc0959 100644
--- a/src/bun.js/bindings/bindings.cpp
+++ b/src/bun.js/bindings/bindings.cpp
@@ -4552,3 +4552,12 @@ CPP_DECL void JSC__JSMap__set(JSC__JSMap* map, JSC__JSGlobalObject* arg1, JSC__J
{
map->set(arg1, JSC::JSValue::decode(JSValue2), JSC::JSValue::decode(JSValue3));
}
+
+CPP_DECL void JSC__VM__setControlFlowProfiler(JSC__VM* vm, bool isEnabled)
+{
+ if (isEnabled) {
+ vm->enableControlFlowProfiler();
+ } else {
+ vm->disableControlFlowProfiler();
+ }
+} \ No newline at end of file
diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig
index 4f533b9d9..3b6116a0d 100644
--- a/src/bun.js/bindings/bindings.zig
+++ b/src/bun.js/bindings/bindings.zig
@@ -102,8 +102,9 @@ pub const ZigString = extern struct {
};
pub fn fromBytes(slice_: []const u8) ZigString {
- if (!strings.isAllASCII(slice_))
- return fromUTF8(slice_);
+ if (!strings.isAllASCII(slice_)) {
+ return initUTF8(slice_);
+ }
return init(slice_);
}
@@ -4978,6 +4979,7 @@ pub const VM = extern struct {
SmallHeap = 0,
LargeHeap = 1,
};
+
pub fn create(heap_type: HeapType) *VM {
return cppFn("create", .{@intFromEnum(heap_type)});
}
@@ -4986,6 +4988,10 @@ pub const VM = extern struct {
return cppFn("deinit", .{ vm, global_object });
}
+ pub fn setControlFlowProfiler(vm: *VM, enabled: bool) void {
+ return cppFn("setControlFlowProfiler", .{ vm, enabled });
+ }
+
pub fn isJITEnabled() bool {
return cppFn("isJITEnabled", .{});
}
@@ -5093,7 +5099,7 @@ pub const VM = extern struct {
return cppFn("blockBytesAllocated", .{vm});
}
- pub const Extern = [_][]const u8{ "collectAsync", "externalMemorySize", "blockBytesAllocated", "heapSize", "releaseWeakRefs", "throwError", "deferGC", "holdAPILock", "runGC", "generateHeapSnapshot", "isJITEnabled", "deleteAllCode", "create", "deinit", "setExecutionForbidden", "executionForbidden", "isEntered", "throwError", "drainMicrotasks", "whenIdle", "shrinkFootprint", "setExecutionTimeLimit", "clearExecutionTimeLimit" };
+ pub const Extern = [_][]const u8{ "setControlFlowProfiler", "collectAsync", "externalMemorySize", "blockBytesAllocated", "heapSize", "releaseWeakRefs", "throwError", "deferGC", "holdAPILock", "runGC", "generateHeapSnapshot", "isJITEnabled", "deleteAllCode", "create", "deinit", "setExecutionForbidden", "executionForbidden", "isEntered", "throwError", "drainMicrotasks", "whenIdle", "shrinkFootprint", "setExecutionTimeLimit", "clearExecutionTimeLimit" };
};
pub const ThrowScope = extern struct {
diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h
index 05c708a48..9a6d1b72a 100644
--- a/src/bun.js/bindings/headers.h
+++ b/src/bun.js/bindings/headers.h
@@ -428,6 +428,7 @@ CPP_DECL bool JSC__VM__isEntered(JSC__VM* arg0);
CPP_DECL bool JSC__VM__isJITEnabled();
CPP_DECL void JSC__VM__releaseWeakRefs(JSC__VM* arg0);
CPP_DECL JSC__JSValue JSC__VM__runGC(JSC__VM* arg0, bool arg1);
+CPP_DECL void JSC__VM__setControlFlowProfiler(JSC__VM* arg0, bool arg1);
CPP_DECL void JSC__VM__setExecutionForbidden(JSC__VM* arg0, bool arg1);
CPP_DECL void JSC__VM__setExecutionTimeLimit(JSC__VM* arg0, double arg1);
CPP_DECL void JSC__VM__shrinkFootprint(JSC__VM* arg0);
diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig
index fbca33a30..d39793c07 100644
--- a/src/bun.js/bindings/headers.zig
+++ b/src/bun.js/bindings/headers.zig
@@ -322,6 +322,7 @@ pub extern fn JSC__VM__isEntered(arg0: *bindings.VM) bool;
pub extern fn JSC__VM__isJITEnabled(...) bool;
pub extern fn JSC__VM__releaseWeakRefs(arg0: *bindings.VM) void;
pub extern fn JSC__VM__runGC(arg0: *bindings.VM, arg1: bool) JSC__JSValue;
+pub extern fn JSC__VM__setControlFlowProfiler(arg0: *bindings.VM, arg1: bool) void;
pub extern fn JSC__VM__setExecutionForbidden(arg0: *bindings.VM, arg1: bool) void;
pub extern fn JSC__VM__setExecutionTimeLimit(arg0: *bindings.VM, arg1: f64) void;
pub extern fn JSC__VM__shrinkFootprint(arg0: *bindings.VM) void;
diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig
index 62b00cf42..1c8d91d52 100644
--- a/src/bun.js/javascript.zig
+++ b/src/bun.js/javascript.zig
@@ -128,6 +128,7 @@ pub fn OpaqueWrap(comptime Context: type, comptime Function: fn (this: *Context)
pub const bun_file_import_path = "/node_modules.server.bun";
const SourceMap = @import("../sourcemap/sourcemap.zig");
+const ParsedSourceMap = SourceMap.Mapping.ParsedSourceMap;
const MappingList = SourceMap.Mapping.List;
pub const SavedSourceMap = struct {
@@ -138,7 +139,7 @@ pub const SavedSourceMap = struct {
data: [*]u8,
pub fn vlq(this: SavedMappings) []u8 {
- return this.data[16..this.len()];
+ return this.data[24..this.len()];
}
pub inline fn len(this: SavedMappings) usize {
@@ -149,12 +150,13 @@ pub const SavedSourceMap = struct {
default_allocator.free(this.data[0..this.len()]);
}
- pub fn toMapping(this: SavedMappings, allocator: Allocator, path: string) anyerror!MappingList {
+ pub fn toMapping(this: SavedMappings, allocator: Allocator, path: string) anyerror!ParsedSourceMap {
const result = SourceMap.Mapping.parse(
allocator,
- this.data[16..this.len()],
+ this.data[24..this.len()],
@as(usize, @bitCast(this.data[8..16].*)),
1,
+ @as(usize, @bitCast(this.data[16..24].*)),
);
switch (result) {
.fail => |fail| {
@@ -183,7 +185,7 @@ pub const SavedSourceMap = struct {
}
};
- pub const Value = TaggedPointerUnion(.{ MappingList, SavedMappings });
+ pub const Value = TaggedPointerUnion(.{ ParsedSourceMap, SavedMappings });
pub const HashTable = std.HashMap(u64, *anyopaque, IdentityContext(u64), 80);
/// This is a pointer to the map located on the VirtualMachine struct
@@ -203,8 +205,8 @@ pub const SavedSourceMap = struct {
var entry = try this.map.getOrPut(bun.hash(source.path.text));
if (entry.found_existing) {
var value = Value.from(entry.value_ptr.*);
- if (value.get(MappingList)) |source_map_| {
- var source_map: *MappingList = source_map_;
+ if (value.get(ParsedSourceMap)) |source_map_| {
+ var source_map: *ParsedSourceMap = source_map_;
source_map.deinit(default_allocator);
} else if (value.get(SavedMappings)) |saved_mappings| {
var saved = SavedMappings{ .data = @as([*]u8, @ptrCast(saved_mappings)) };
@@ -216,16 +218,16 @@ pub const SavedSourceMap = struct {
entry.value_ptr.* = Value.init(bun.cast(*SavedMappings, mappings.list.items.ptr)).ptr();
}
- pub fn get(this: *SavedSourceMap, path: string) ?MappingList {
+ pub fn get(this: *SavedSourceMap, path: string) ?ParsedSourceMap {
var mapping = this.map.getEntry(bun.hash(path)) orelse return null;
switch (Value.from(mapping.value_ptr.*).tag()) {
- (@field(Value.Tag, @typeName(MappingList))) => {
- return Value.from(mapping.value_ptr.*).as(MappingList).*;
+ Value.Tag.ParsedSourceMap => {
+ return Value.from(mapping.value_ptr.*).as(ParsedSourceMap).*;
},
Value.Tag.SavedMappings => {
- var saved = SavedMappings{ .data = @as([*]u8, @ptrCast(Value.from(mapping.value_ptr.*).as(MappingList))) };
+ var saved = SavedMappings{ .data = @as([*]u8, @ptrCast(Value.from(mapping.value_ptr.*).as(ParsedSourceMap))) };
defer saved.deinit();
- var result = default_allocator.create(MappingList) catch unreachable;
+ var result = default_allocator.create(ParsedSourceMap) catch unreachable;
result.* = saved.toMapping(default_allocator, path) catch {
_ = this.map.remove(mapping.key_ptr.*);
return null;
@@ -246,8 +248,8 @@ pub const SavedSourceMap = struct {
this.mutex.lock();
defer this.mutex.unlock();
- var mappings = this.get(path) orelse return null;
- return SourceMap.Mapping.find(mappings, line, column);
+ const parsed_mappings = this.get(path) orelse return null;
+ return SourceMap.Mapping.find(parsed_mappings.mappings, line, column);
}
};
const uws = @import("root").bun.uws;
diff --git a/src/bun.js/modules/BunJSCModule.h b/src/bun.js/modules/BunJSCModule.h
index d7548dcdf..73823e16e 100644
--- a/src/bun.js/modules/BunJSCModule.h
+++ b/src/bun.js/modules/BunJSCModule.h
@@ -28,14 +28,17 @@
#include "wtf/text/WTFString.h"
#include "Process.h"
-
+#include <JavaScriptCore/SourceProviderCache.h>
#if ENABLE(REMOTE_INSPECTOR)
#include "JavaScriptCore/RemoteInspectorServer.h"
#endif
#include "JSDOMConvertBase.h"
+#include "ZigSourceProvider.h"
#include "mimalloc.h"
+#include "JavaScriptCore/ControlFlowProfiler.h"
+
using namespace JSC;
using namespace WTF;
using namespace WebCore;
@@ -650,6 +653,60 @@ JSC_DEFINE_HOST_FUNCTION(functionDeserialize, (JSGlobalObject * globalObject,
RELEASE_AND_RETURN(throwScope, JSValue::encode(result));
}
+extern "C" EncodedJSValue ByteRangeMapping__findExecutedLines(
+ JSC::JSGlobalObject *, BunString sourceURL, BasicBlockRange *ranges,
+ size_t len, size_t functionOffset, bool ignoreSourceMap);
+
+JSC_DEFINE_HOST_FUNCTION(functionCodeCoverageForFile,
+ (JSGlobalObject * globalObject,
+ CallFrame *callFrame)) {
+ VM &vm = globalObject->vm();
+ auto throwScope = DECLARE_THROW_SCOPE(vm);
+
+ String fileName = callFrame->argument(0).toWTFString(globalObject);
+ RETURN_IF_EXCEPTION(throwScope, encodedJSValue());
+ bool ignoreSourceMap = callFrame->argument(1).toBoolean(globalObject);
+
+ auto sourceID = Zig::sourceIDForSourceURL(fileName);
+ if (!sourceID) {
+ throwException(globalObject, throwScope,
+ createError(globalObject, "No source for file"_s));
+ return JSValue::encode(jsUndefined());
+ }
+
+ auto basicBlocks =
+ vm.controlFlowProfiler()->getBasicBlocksForSourceIDWithoutFunctionRange(
+ sourceID, vm);
+
+ if (basicBlocks.isEmpty()) {
+ return JSC::JSValue::encode(
+ JSC::constructEmptyArray(globalObject, nullptr, 0));
+ }
+
+ size_t functionStartOffset = basicBlocks.size();
+
+ const Vector<std::tuple<bool, unsigned, unsigned>> &functionRanges =
+ vm.functionHasExecutedCache()->getFunctionRanges(sourceID);
+
+ basicBlocks.reserveCapacity(functionRanges.size() + basicBlocks.size());
+
+ for (const auto &functionRange : functionRanges) {
+ BasicBlockRange range;
+ range.m_hasExecuted = std::get<0>(functionRange);
+ range.m_startOffset = static_cast<int>(std::get<1>(functionRange));
+ range.m_endOffset = static_cast<int>(std::get<2>(functionRange));
+ range.m_executionCount =
+ range.m_hasExecuted
+ ? 1
+ : 0; // This is a hack. We don't actually count this.
+ basicBlocks.append(range);
+ }
+
+ return ByteRangeMapping__findExecutedLines(
+ globalObject, Bun::toString(fileName), basicBlocks.data(),
+ basicBlocks.size(), functionStartOffset, ignoreSourceMap);
+}
+
// clang-format off
/* Source for BunJSCModuleTable.lut.h
@begin BunJSCModuleTable
@@ -718,6 +775,7 @@ DEFINE_NATIVE_MODULE(BunJSC)
putNativeFn(Identifier::fromString(vm, "getProtectedObjects"_s), functionGetProtectedObjects);
putNativeFn(Identifier::fromString(vm, "generateHeapSnapshotForDebugging"_s), functionGenerateHeapSnapshotForDebugging);
putNativeFn(Identifier::fromString(vm, "profile"_s), functionRunProfiler);
+ putNativeFn(Identifier::fromString(vm, "codeCoverageForFile"_s), functionCodeCoverageForFile);
putNativeFn(Identifier::fromString(vm, "setTimeZone"_s), functionSetTimeZone);
putNativeFn(Identifier::fromString(vm, "serialize"_s), functionSerialize);
putNativeFn(Identifier::fromString(vm, "deserialize"_s), functionDeserialize);
diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig
index aacf671ce..8691e5a2d 100644
--- a/src/bun.js/test/jest.zig
+++ b/src/bun.js/test/jest.zig
@@ -89,6 +89,7 @@ pub const TestRunner = struct {
test_timeout_timer: ?*bun.uws.Timer = null,
last_test_timeout_timer_duration: u32 = 0,
active_test_for_timeout: ?TestRunner.Test.ID = null,
+ test_options: *const bun.CLI.Command.TestOptions = undefined,
global_callbacks: struct {
beforeAll: std.ArrayListUnmanaged(JSC.JSValue) = .{},
diff --git a/src/bun.zig b/src/bun.zig
index 4dc5a296d..c26fb4781 100644
--- a/src/bun.zig
+++ b/src/bun.zig
@@ -1187,7 +1187,10 @@ pub const MultiArrayList = @import("./multi_array_list.zig").MultiArrayList;
pub const Joiner = @import("./string_joiner.zig");
pub const renamer = @import("./renamer.zig");
-pub const sourcemap = @import("./sourcemap/sourcemap.zig");
+pub const sourcemap = struct {
+ pub usingnamespace @import("./sourcemap/sourcemap.zig");
+ pub usingnamespace @import("./sourcemap/CodeCoverage.zig");
+};
pub fn asByteSlice(buffer: anytype) []const u8 {
return switch (@TypeOf(buffer)) {
diff --git a/src/bunfig.zig b/src/bunfig.zig
index 7302907d9..e1f1ca4b0 100644
--- a/src/bunfig.zig
+++ b/src/bunfig.zig
@@ -242,6 +242,47 @@ pub const Bunfig = struct {
try this.expect(expr, .e_boolean);
this.ctx.runtime_options.smol = expr.data.e_boolean.value;
}
+
+ if (test_.get("coverage")) |expr| {
+ try this.expect(expr, .e_boolean);
+ this.ctx.test_options.coverage.enabled = expr.data.e_boolean.value;
+ }
+
+ if (test_.get("coverageThreshold")) |expr| outer: {
+ if (expr.data == .e_number) {
+ this.ctx.test_options.coverage.fractions.functions = expr.data.e_number.value;
+ this.ctx.test_options.coverage.fractions.lines = expr.data.e_number.value;
+ this.ctx.test_options.coverage.fractions.stmts = expr.data.e_number.value;
+ break :outer;
+ }
+
+ try this.expect(expr, .e_object);
+ if (expr.get("functions")) |functions| {
+ try this.expect(functions, .e_number);
+ this.ctx.test_options.coverage.fractions.functions = functions.data.e_number.value;
+ }
+
+ if (expr.get("lines")) |lines| {
+ try this.expect(lines, .e_number);
+ this.ctx.test_options.coverage.fractions.lines = lines.data.e_number.value;
+ }
+
+ if (expr.get("statements")) |stmts| {
+ try this.expect(stmts, .e_number);
+ this.ctx.test_options.coverage.fractions.stmts = stmts.data.e_number.value;
+ }
+ }
+
+ // This mostly exists for debugging.
+ if (test_.get("coverageIgnoreSourcemaps")) |expr| {
+ try this.expect(expr, .e_boolean);
+ this.ctx.test_options.coverage.ignore_sourcemap = expr.data.e_boolean.value;
+ }
+
+ if (test_.get("coverageSkipTestFiles")) |expr| {
+ try this.expect(expr, .e_boolean);
+ this.ctx.test_options.coverage.skip_test_files = expr.data.e_boolean.value;
+ }
}
}
diff --git a/src/cli.zig b/src/cli.zig
index aa36c63fb..ca0993dbb 100644
--- a/src/cli.zig
+++ b/src/cli.zig
@@ -219,6 +219,7 @@ pub const Arguments = struct {
clap.parseParam("--rerun-each <NUMBER> Re-run each test file <NUMBER> times, helps catch certain bugs") catch unreachable,
clap.parseParam("--only Only run tests that are marked with \"test.only()\"") catch unreachable,
clap.parseParam("--todo Include tests that are marked with \"test.todo()\"") catch unreachable,
+ clap.parseParam("--coverage Generate a coverage profile") catch unreachable,
clap.parseParam("--bail <NUMBER>? Exit the test suite after <NUMBER> failures. If you do not specify a number, it defaults to 1.") catch unreachable,
clap.parseParam("-t, --test-name-pattern <STR> Run only tests with a name that matches the given regex.") catch unreachable,
};
@@ -387,6 +388,11 @@ pub const Arguments = struct {
};
}
}
+
+ if (!ctx.test_options.coverage.enabled) {
+ ctx.test_options.coverage.enabled = args.flag("--coverage");
+ }
+
if (args.option("--bail")) |bail| {
if (bail.len > 0) {
ctx.test_options.bail = std.fmt.parseInt(u32, bail, 10) catch |e| {
@@ -965,6 +971,7 @@ pub const Command = struct {
run_todo: bool = false,
only: bool = false,
bail: u32 = 0,
+ coverage: TestCommand.CodeCoverageOptions = .{},
test_filter_regex: ?*RegularExpression = null,
};
diff --git a/src/cli/test_command.zig b/src/cli/test_command.zig
index 819aceb19..c4d78f4d5 100644
--- a/src/cli/test_command.zig
+++ b/src/cli/test_command.zig
@@ -261,6 +261,103 @@ pub const CommandLineReporter = struct {
Output.prettyError("Ran {d} tests across {d} files. ", .{ tests, files });
Output.printStartEnd(bun.start_time, std.time.nanoTimestamp());
}
+
+ pub fn printCodeCoverage(this: *CommandLineReporter, vm: *JSC.VirtualMachine, opts: *TestCommand.CodeCoverageOptions, comptime enable_ansi_colors: bool) !void {
+ const trace = bun.tracy.traceNamed(@src(), "TestCommand.printCodeCoverage");
+ defer trace.end();
+
+ _ = this;
+ var map = bun.sourcemap.ByteRangeMapping.map orelse return;
+ var iter = map.valueIterator();
+ var max_filepath_length: usize = "All files".len;
+ const relative_dir = vm.bundler.fs.top_level_dir;
+
+ var byte_ranges = try std.ArrayList(bun.sourcemap.ByteRangeMapping).initCapacity(bun.default_allocator, map.count());
+
+ while (iter.next()) |entry| {
+ const value: bun.sourcemap.ByteRangeMapping = entry.*;
+ var utf8 = value.source_url.slice();
+ byte_ranges.appendAssumeCapacity(value);
+ max_filepath_length = @max(bun.path.relative(relative_dir, utf8).len, max_filepath_length);
+ }
+
+ if (byte_ranges.items.len == 0) {
+ return;
+ }
+
+ std.sort.block(bun.sourcemap.ByteRangeMapping, byte_ranges.items, void{}, bun.sourcemap.ByteRangeMapping.isLessThan);
+
+ iter = map.valueIterator();
+ var writer = Output.errorWriter();
+ var base_fraction = opts.fractions;
+ var failing = false;
+
+ writer.writeAll(Output.prettyFmt("<r><d>", enable_ansi_colors)) catch return;
+ writer.writeByteNTimes('-', max_filepath_length + 2) catch return;
+ writer.writeAll(Output.prettyFmt("|---------|---------|-------------------<r>\n", enable_ansi_colors)) catch return;
+ writer.writeAll("File") catch return;
+ writer.writeByteNTimes(' ', max_filepath_length - "File".len + 1) catch return;
+ // writer.writeAll(Output.prettyFmt(" <d>|<r> % Funcs <d>|<r> % Blocks <d>|<r> % Lines <d>|<r> Uncovered Line #s\n", enable_ansi_colors)) catch return;
+ writer.writeAll(Output.prettyFmt(" <d>|<r> % Funcs <d>|<r> % Lines <d>|<r> Uncovered Line #s\n", enable_ansi_colors)) catch return;
+ writer.writeAll(Output.prettyFmt("<d>", enable_ansi_colors)) catch return;
+ writer.writeByteNTimes('-', max_filepath_length + 2) catch return;
+ writer.writeAll(Output.prettyFmt("|---------|---------|-------------------<r>\n", enable_ansi_colors)) catch return;
+
+ var coverage_buffer = bun.MutableString.initEmpty(bun.default_allocator);
+ var coverage_buffer_buffer = coverage_buffer.bufferedWriter();
+ var coverage_writer = coverage_buffer_buffer.writer();
+
+ var avg = bun.sourcemap.CoverageFraction{
+ .functions = 0.0,
+ .lines = 0.0,
+ .stmts = 0.0,
+ };
+ var avg_count: f64 = 0;
+
+ for (byte_ranges.items) |*entry| {
+ var report = bun.sourcemap.CodeCoverageReport.generate(vm.global, bun.default_allocator, entry, opts.ignore_sourcemap) orelse continue;
+ defer report.deinit(bun.default_allocator);
+ var fraction = base_fraction;
+ report.writeFormat(max_filepath_length, &fraction, relative_dir, coverage_writer, enable_ansi_colors) catch continue;
+ avg.functions += fraction.functions;
+ avg.lines += fraction.lines;
+ avg.stmts += fraction.stmts;
+ avg_count += 1.0;
+ if (fraction.failing) {
+ failing = true;
+ }
+
+ coverage_writer.writeAll("\n") catch continue;
+ }
+
+ {
+ avg.functions /= avg_count;
+ avg.lines /= avg_count;
+ avg.stmts /= avg_count;
+
+ try bun.sourcemap.CodeCoverageReport.writeFormatWithValues(
+ "All files",
+ max_filepath_length,
+ avg,
+ base_fraction,
+ failing,
+ writer,
+ false,
+ enable_ansi_colors,
+ );
+
+ try writer.writeAll(Output.prettyFmt("<r><d> |<r>\n", enable_ansi_colors));
+ }
+
+ coverage_buffer_buffer.flush() catch return;
+ try writer.writeAll(coverage_buffer.list.items);
+ try writer.writeAll(Output.prettyFmt("<r><d>", enable_ansi_colors));
+ writer.writeByteNTimes('-', max_filepath_length + 2) catch return;
+ writer.writeAll(Output.prettyFmt("|---------|---------|-------------------<r>\n", enable_ansi_colors)) catch return;
+
+ opts.fractions.failing = failing;
+ Output.flush();
+ }
};
const Scanner = struct {
@@ -334,6 +431,42 @@ const Scanner = struct {
"_spec",
};
+ export fn BunTest__shouldGenerateCodeCoverage(test_name_str: bun.String) callconv(.C) bool {
+ var zig_slice: bun.JSC.ZigString.Slice = .{};
+ defer zig_slice.deinit();
+
+ // In this particular case, we don't actually care about non-ascii latin1 characters.
+ // so we skip the ascii check
+ const slice = if (test_name_str.is8Bit()) test_name_str.latin1() else brk: {
+ zig_slice = test_name_str.toUTF8(bun.default_allocator);
+ break :brk zig_slice.slice();
+ };
+
+ // always ignore node_modules.
+ if (strings.contains(slice, "/" ++ "node_modules" ++ "/")) {
+ return false;
+ }
+
+ const ext = std.fs.path.extension(slice);
+ const loader_by_ext = JSC.VirtualMachine.get().bundler.options.loader(ext);
+
+ // allow file loader just incase they use a custom loader with a non-standard extension
+ if (!(loader_by_ext.isJavaScriptLike() or loader_by_ext == .file)) {
+ return false;
+ }
+
+ if (jest.Jest.runner.?.test_options.coverage.skip_test_files) {
+ const name_without_extension = slice[0 .. slice.len - ext.len];
+ inline for (test_name_suffixes) |suffix| {
+ if (strings.endsWithComptime(name_without_extension, suffix)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
pub fn couldBeTestFile(this: *Scanner, name: string) bool {
const extname = std.fs.path.extension(name);
if (!this.options.loader(extname).isJavaScriptLike()) return false;
@@ -419,6 +552,13 @@ pub const TestCommand = struct {
pub const name = "test";
pub const old_name = "wiptest";
+ pub const CodeCoverageOptions = struct {
+ skip_test_files: bool = !Environment.allow_assert,
+ fractions: bun.sourcemap.CoverageFraction = .{},
+ ignore_sourcemap: bool = false,
+ enabled: bool = false,
+ };
+
pub fn exec(ctx: Command.Context) !void {
if (comptime is_bindgen) unreachable;
@@ -480,7 +620,7 @@ pub const TestCommand = struct {
reporter.repeat_count = @max(ctx.test_options.repeat_count, 1);
reporter.jest.callback = &reporter.callback;
jest.Jest.runner = &reporter.jest;
-
+ reporter.jest.test_options = &ctx.test_options;
js_ast.Expr.Data.Store.create(default_allocator);
js_ast.Stmt.Data.Store.create(default_allocator);
var vm = try JSC.VirtualMachine.init(
@@ -507,6 +647,14 @@ pub const TestCommand = struct {
vm.is_main_thread = true;
JSC.VirtualMachine.is_main_thread_vm = true;
+ if (ctx.test_options.coverage.enabled) {
+ vm.bundler.options.code_coverage = true;
+ vm.bundler.options.minify_syntax = false;
+ vm.bundler.options.minify_identifiers = false;
+ vm.bundler.options.minify_whitespace = false;
+ vm.global.vm().setControlFlowProfiler(true);
+ }
+
// For tests, we default to UTC time zone
// unless the user inputs TZ="", in which case we use local time zone
var TZ_NAME: string =
@@ -538,7 +686,6 @@ pub const TestCommand = struct {
scanner.scan(dir_to_scan);
scanner.dirs_to_scan.deinit();
-
const test_files = try scanner.results.toOwnedSlice();
if (test_files.len > 0) {
vm.hot_reload = ctx.debug.hot_reload;
@@ -549,6 +696,7 @@ pub const TestCommand = struct {
}
try jest.Jest.runner.?.snapshots.writeSnapshotFile();
+ var coverage = ctx.test_options.coverage;
if (reporter.summary.pass > 20) {
if (reporter.summary.skip > 0) {
@@ -604,6 +752,12 @@ pub const TestCommand = struct {
} else {
Output.prettyError("\n", .{});
+ if (coverage.enabled) {
+ switch (Output.enable_ansi_colors_stderr) {
+ inline else => |colors| reporter.printCodeCoverage(vm, &coverage, colors) catch {},
+ }
+ }
+
if (reporter.summary.pass > 0) {
Output.prettyError("<r><green>", .{});
}
@@ -688,7 +842,7 @@ pub const TestCommand = struct {
}
}
- if (reporter.summary.fail > 0) {
+ if (reporter.summary.fail > 0 or (coverage.enabled and coverage.fractions.failing)) {
Global.exit(1);
}
}
diff --git a/src/js_printer.zig b/src/js_printer.zig
index 61c13c25f..56b6d0fae 100644
--- a/src/js_printer.zig
+++ b/src/js_printer.zig
@@ -5650,6 +5650,7 @@ pub fn getSourceMapBuilder(
is_bun_platform,
),
.cover_lines_without_mappings = true,
+ .approximate_input_line_count = tree.approximate_newline_count,
.prepend_count = is_bun_platform and generate_source_map == .lazy,
.line_offset_tables = opts.line_offset_tables orelse brk: {
if (generate_source_map == .lazy) break :brk SourceMap.LineOffsetTable.generate(
diff --git a/src/options.zig b/src/options.zig
index 4f64f60c6..647c83786 100644
--- a/src/options.zig
+++ b/src/options.zig
@@ -1435,6 +1435,8 @@ pub const BundleOptions = struct {
minify_syntax: bool = false,
minify_identifiers: bool = false,
+ code_coverage: bool = false,
+
compile: bool = false,
/// This is a list of packages which even when require() is used, we will
diff --git a/src/sourcemap/CodeCoverage.zig b/src/sourcemap/CodeCoverage.zig
new file mode 100644
index 000000000..2b063bbbe
--- /dev/null
+++ b/src/sourcemap/CodeCoverage.zig
@@ -0,0 +1,646 @@
+const bun = @import("root").bun;
+const std = @import("std");
+const LineOffsetTable = bun.sourcemap.LineOffsetTable;
+const SourceMap = bun.sourcemap;
+const Bitset = bun.bit_set.DynamicBitSetUnmanaged;
+const Output = bun.Output;
+const prettyFmt = Output.prettyFmt;
+
+/// Our code coverage currently only deals with lines of code, not statements or branches.
+/// JSC doesn't expose function names in their coverage data, so we don't include that either :(.
+/// Since we only need to store line numbers, our job gets simpler
+///
+/// We can use two bitsets to store code coverage data for a given file
+/// 1. executable_lines
+/// 2. lines_which_have_executed
+///
+/// Not all lines of code are executable. Comments, whitespace, empty lines, etc. are not executable.
+/// It's not a problem for anyone if comments, whitespace, empty lines etc are not executed, so those should always be omitted from coverage reports
+///
+/// We use two bitsets since the typical size will be decently small,
+/// bitsets are simple and bitsets are relatively fast to construct and query
+///
+pub const CodeCoverageReport = struct {
+ source_url: bun.JSC.ZigString.Slice,
+ executable_lines: Bitset,
+ lines_which_have_executed: Bitset,
+ functions: std.ArrayListUnmanaged(Block),
+ functions_which_have_executed: Bitset,
+ stmts_which_have_executed: Bitset,
+ stmts: std.ArrayListUnmanaged(Block),
+ total_lines: u32 = 0,
+
+ pub const Block = struct {
+ start_line: u32 = 0,
+ end_line: u32 = 0,
+ };
+
+ pub fn linesCoverageFraction(this: *const CodeCoverageReport) f64 {
+ var intersected = this.executable_lines.clone(bun.default_allocator) catch @panic("OOM");
+ defer intersected.deinit(bun.default_allocator);
+ intersected.setIntersection(this.lines_which_have_executed);
+
+ const total_count: f64 = @floatFromInt(this.executable_lines.count());
+ if (total_count == 0) {
+ return 1.0;
+ }
+
+ const intersected_count: f64 = @floatFromInt(intersected.count());
+
+ return (intersected_count / total_count);
+ }
+
+ pub fn stmtsCoverageFraction(this: *const CodeCoverageReport) f64 {
+ const total_count: f64 = @floatFromInt(this.stmts.items.len);
+
+ if (total_count == 0) {
+ return 1.0;
+ }
+
+ return ((@as(f64, @floatFromInt(this.stmts_which_have_executed.count()))) / (total_count));
+ }
+
+ pub fn functionCoverageFraction(this: *const CodeCoverageReport) f64 {
+ const total_count: f64 = @floatFromInt(this.functions.items.len);
+ return (@as(f64, @floatFromInt(this.functions_which_have_executed.count())) / total_count);
+ }
+
+ pub fn writeFormatWithValues(
+ filename: []const u8,
+ max_filename_length: usize,
+ vals: CoverageFraction,
+ failing: CoverageFraction,
+ failed: bool,
+ writer: anytype,
+ indent_name: bool,
+ comptime enable_colors: bool,
+ ) !void {
+ if (comptime enable_colors) {
+ if (failed) {
+ try writer.writeAll(comptime prettyFmt("<r><b><red>", true));
+ } else {
+ try writer.writeAll(comptime prettyFmt("<r><b><green>", true));
+ }
+ }
+
+ if (indent_name) {
+ try writer.writeAll(" ");
+ }
+
+ try writer.writeAll(filename);
+ try writer.writeByteNTimes(' ', (max_filename_length - filename.len + @as(usize, @intFromBool(!indent_name))));
+ try writer.writeAll(comptime prettyFmt("<r><d> | <r>", enable_colors));
+
+ if (comptime enable_colors) {
+ if (vals.functions < failing.functions) {
+ try writer.writeAll(comptime prettyFmt("<b><red>", true));
+ } else {
+ try writer.writeAll(comptime prettyFmt("<b><green>", true));
+ }
+ }
+
+ try writer.print("{d: >7.2}", .{vals.functions * 100.0});
+ // try writer.writeAll(comptime prettyFmt("<r><d> | <r>", enable_colors));
+ // if (comptime enable_colors) {
+ // // if (vals.stmts < failing.stmts) {
+ // try writer.writeAll(comptime prettyFmt("<d>", true));
+ // // } else {
+ // // try writer.writeAll(comptime prettyFmt("<d>", true));
+ // // }
+ // }
+ // try writer.print("{d: >8.2}", .{vals.stmts * 100.0});
+ try writer.writeAll(comptime prettyFmt("<r><d> | <r>", enable_colors));
+
+ if (comptime enable_colors) {
+ if (vals.lines < failing.lines) {
+ try writer.writeAll(comptime prettyFmt("<b><red>", true));
+ } else {
+ try writer.writeAll(comptime prettyFmt("<b><green>", true));
+ }
+ }
+
+ try writer.print("{d: >7.2}", .{vals.lines * 100.0});
+ }
+
+ pub fn writeFormat(
+ report: *const CodeCoverageReport,
+ max_filename_length: usize,
+ fraction: *CoverageFraction,
+ base_path: []const u8,
+ writer: anytype,
+ comptime enable_colors: bool,
+ ) !void {
+ var failing = fraction.*;
+ const fns = report.functionCoverageFraction();
+ const lines = report.linesCoverageFraction();
+ const stmts = report.stmtsCoverageFraction();
+ fraction.functions = fns;
+ fraction.lines = lines;
+ fraction.stmts = stmts;
+
+ const failed = fns < failing.functions or lines < failing.lines; // or stmts < failing.stmts;
+ fraction.failing = failed;
+
+ var filename = report.source_url.slice();
+ if (base_path.len > 0) {
+ filename = bun.path.relative(base_path, filename);
+ }
+
+ try writeFormatWithValues(
+ filename,
+ max_filename_length,
+ fraction.*,
+ failing,
+ failed,
+ writer,
+ true,
+ enable_colors,
+ );
+
+ try writer.writeAll(comptime prettyFmt("<r><d> | <r>", enable_colors));
+
+ var executable_lines_that_havent_been_executed = report.lines_which_have_executed.clone(bun.default_allocator) catch @panic("OOM");
+ defer executable_lines_that_havent_been_executed.deinit(bun.default_allocator);
+ executable_lines_that_havent_been_executed.toggleAll();
+
+ // This sets statements in executed scopes
+ executable_lines_that_havent_been_executed.setIntersection(report.executable_lines);
+
+ var iter = executable_lines_that_havent_been_executed.iterator(.{});
+ var start_of_line_range: usize = 0;
+ var prev_line: usize = 0;
+ var is_first = true;
+
+ while (iter.next()) |next_line| {
+ if (next_line == (prev_line + 1)) {
+ prev_line = next_line;
+ continue;
+ } else if (is_first and start_of_line_range == 0 and prev_line == 0) {
+ start_of_line_range = next_line;
+ prev_line = next_line;
+ continue;
+ }
+
+ if (is_first) {
+ is_first = false;
+ } else {
+ try writer.print(comptime prettyFmt("<r><d>,<r>", enable_colors), .{});
+ }
+
+ if (start_of_line_range == prev_line) {
+ try writer.print(comptime prettyFmt("<red>{d}", enable_colors), .{start_of_line_range + 1});
+ } else {
+ try writer.print(comptime prettyFmt("<red>{d}-{d}", enable_colors), .{ start_of_line_range + 1, prev_line + 1 });
+ }
+
+ prev_line = next_line;
+ start_of_line_range = next_line;
+ }
+
+ if (prev_line != start_of_line_range) {
+ if (is_first) {
+ is_first = false;
+ } else {
+ try writer.print(comptime prettyFmt("<r><d>,<r>", enable_colors), .{});
+ }
+
+ if (start_of_line_range == prev_line) {
+ try writer.print(comptime prettyFmt("<red>{d}", enable_colors), .{start_of_line_range + 1});
+ } else {
+ try writer.print(comptime prettyFmt("<red>{d}-{d}", enable_colors), .{ start_of_line_range + 1, prev_line + 1 });
+ }
+ }
+ }
+
+ pub fn deinit(this: *CodeCoverageReport, allocator: std.mem.Allocator) void {
+ this.executable_lines.deinit(allocator);
+ this.lines_which_have_executed.deinit(allocator);
+ this.functions.deinit(allocator);
+ this.stmts.deinit(allocator);
+ this.functions_which_have_executed.deinit(allocator);
+ this.stmts_which_have_executed.deinit(allocator);
+ }
+
+ extern fn CodeCoverage__withBlocksAndFunctions(
+ *bun.JSC.VM,
+ i32,
+ *anyopaque,
+ bool,
+ *const fn (
+ *Generator,
+ [*]const BasicBlockRange,
+ usize,
+ usize,
+ bool,
+ ) callconv(.C) void,
+ ) bool;
+
+ const Generator = struct {
+ allocator: std.mem.Allocator,
+ byte_range_mapping: *ByteRangeMapping,
+ result: *?CodeCoverageReport,
+
+ pub fn do(
+ this: *@This(),
+ blocks_ptr: [*]const BasicBlockRange,
+ blocks_len: usize,
+ function_start_offset: usize,
+ ignore_sourcemap: bool,
+ ) callconv(.C) void {
+ const blocks: []const BasicBlockRange = blocks_ptr[0..function_start_offset];
+ var function_blocks: []const BasicBlockRange = blocks_ptr[function_start_offset..blocks_len];
+ if (function_blocks.len > 1) {
+ function_blocks = function_blocks[1..];
+ }
+
+ if (blocks.len == 0) {
+ return;
+ }
+
+ this.result.* = this.byte_range_mapping.generateCodeCoverageReportFromBlocks(
+ this.allocator,
+ this.byte_range_mapping.source_url,
+ blocks,
+ function_blocks,
+ ignore_sourcemap,
+ ) catch null;
+ }
+ };
+
+ pub fn generate(
+ globalThis: *bun.JSC.JSGlobalObject,
+ allocator: std.mem.Allocator,
+ byte_range_mapping: *ByteRangeMapping,
+ ignore_sourcemap_: bool,
+ ) ?CodeCoverageReport {
+ bun.JSC.markBinding(@src());
+ var vm = globalThis.vm();
+
+ var result: ?CodeCoverageReport = null;
+
+ var generator = Generator{
+ .result = &result,
+ .allocator = allocator,
+ .byte_range_mapping = byte_range_mapping,
+ };
+
+ if (!CodeCoverage__withBlocksAndFunctions(
+ vm,
+ byte_range_mapping.source_id,
+ &generator,
+ ignore_sourcemap_,
+ &Generator.do,
+ )) {
+ return null;
+ }
+
+ return result;
+ }
+};
+
+const BasicBlockRange = extern struct {
+ startOffset: c_int = 0,
+ endOffset: c_int = 0,
+ hasExecuted: bool = false,
+ executionCount: usize = 0,
+};
+
+pub const ByteRangeMapping = struct {
+ line_offset_table: LineOffsetTable.List = .{},
+ source_id: i32,
+ source_url: bun.JSC.ZigString.Slice,
+
+ pub fn isLessThan(_: void, a: ByteRangeMapping, b: ByteRangeMapping) bool {
+ return bun.strings.order(a.source_url.slice(), b.source_url.slice()) == .lt;
+ }
+
+ pub const HashMap = std.HashMap(u64, ByteRangeMapping, bun.IdentityContext(u64), std.hash_map.default_max_load_percentage);
+
+ pub fn deinit(this: *ByteRangeMapping) void {
+ this.line_offset_table.deinit(bun.default_allocator);
+ }
+
+ pub threadlocal var map: ?*HashMap = null;
+ pub fn generate(str: bun.String, source_contents_str: bun.String, source_id: i32) callconv(.C) void {
+ var _map = map orelse brk: {
+ map = bun.JSC.VirtualMachine.get().allocator.create(HashMap) catch @panic("OOM");
+ map.?.* = HashMap.init(bun.JSC.VirtualMachine.get().allocator);
+ break :brk map.?;
+ };
+ var slice = str.toUTF8(bun.default_allocator);
+ const hash = bun.hash(slice.slice());
+ var entry = _map.getOrPut(hash) catch @panic("Out of memory");
+ if (entry.found_existing) {
+ entry.value_ptr.deinit();
+ }
+
+ var source_contents = source_contents_str.toUTF8(bun.default_allocator);
+ defer source_contents.deinit();
+
+ entry.value_ptr.* = compute(source_contents.slice(), source_id, slice);
+ }
+
+ pub fn getSourceID(this: *ByteRangeMapping) callconv(.C) i32 {
+ return this.source_id;
+ }
+
+ pub fn find(path: bun.String) callconv(.C) ?*ByteRangeMapping {
+ var slice = path.toUTF8(bun.default_allocator);
+ defer slice.deinit();
+
+ var map_ = map orelse return null;
+ const hash = bun.hash(slice.slice());
+ var entry = map_.getPtr(hash) orelse return null;
+ return entry;
+ }
+
+ pub fn generateCodeCoverageReportFromBlocks(
+ this: *ByteRangeMapping,
+ allocator: std.mem.Allocator,
+ source_url: bun.JSC.ZigString.Slice,
+ blocks: []const BasicBlockRange,
+ function_blocks: []const BasicBlockRange,
+ ignore_sourcemap: bool,
+ ) !CodeCoverageReport {
+ var line_starts = this.line_offset_table.items(.byte_offset_to_start_of_line);
+
+ var executable_lines: Bitset = Bitset{};
+ var lines_which_have_executed: Bitset = Bitset{};
+ const parsed_mappings_ = bun.JSC.VirtualMachine.get().source_mappings.get(
+ source_url.slice(),
+ );
+
+ var functions = std.ArrayListUnmanaged(CodeCoverageReport.Block){};
+ try functions.ensureTotalCapacityPrecise(allocator, function_blocks.len);
+ errdefer functions.deinit(allocator);
+ var functions_which_have_executed: Bitset = try Bitset.initEmpty(allocator, function_blocks.len);
+ errdefer functions_which_have_executed.deinit(allocator);
+ var stmts_which_have_executed: Bitset = try Bitset.initEmpty(allocator, blocks.len);
+ errdefer stmts_which_have_executed.deinit(allocator);
+
+ var stmts = std.ArrayListUnmanaged(CodeCoverageReport.Block){};
+ try stmts.ensureTotalCapacityPrecise(allocator, function_blocks.len);
+ errdefer stmts.deinit(allocator);
+
+ errdefer executable_lines.deinit(allocator);
+ errdefer lines_which_have_executed.deinit(allocator);
+ var line_count: u32 = 0;
+
+ if (ignore_sourcemap or parsed_mappings_ == null) {
+ line_count = @truncate(line_starts.len);
+ executable_lines = try Bitset.initEmpty(allocator, line_count);
+ lines_which_have_executed = try Bitset.initEmpty(allocator, line_count);
+ for (blocks, 0..) |block, i| {
+ const min: usize = @intCast(@min(block.startOffset, block.endOffset));
+ const max: usize = @intCast(@max(block.startOffset, block.endOffset));
+ var min_line: u32 = std.math.maxInt(u32);
+ var max_line: u32 = 0;
+
+ const has_executed = block.hasExecuted or block.executionCount > 0;
+
+ for (min..max) |byte_offset| {
+ const new_line_index = LineOffsetTable.findIndex(line_starts, .{ .start = @intCast(byte_offset) }) orelse continue;
+ const line_start_byte_offset = line_starts[new_line_index];
+ if (line_start_byte_offset >= byte_offset) {
+ continue;
+ }
+
+ const line: u32 = @intCast(new_line_index);
+ min_line = @min(min_line, line);
+ max_line = @max(max_line, line);
+
+ executable_lines.set(@intCast(new_line_index));
+ if (has_executed) {
+ lines_which_have_executed.set(@intCast(new_line_index));
+ }
+ }
+
+ if (min_line != std.math.maxInt(u32)) {
+ if (has_executed)
+ stmts_which_have_executed.set(i);
+
+ try stmts.append(allocator, .{
+ .start_line = min_line,
+ .end_line = max_line,
+ });
+ }
+ }
+
+ for (function_blocks, 0..) |function, i| {
+ const min: usize = @intCast(@min(function.startOffset, function.endOffset));
+ const max: usize = @intCast(@max(function.startOffset, function.endOffset));
+ var min_line: u32 = std.math.maxInt(u32);
+ var max_line: u32 = 0;
+
+ for (min..max) |byte_offset| {
+ const new_line_index = LineOffsetTable.findIndex(line_starts, .{ .start = @intCast(byte_offset) }) orelse continue;
+ const line_start_byte_offset = line_starts[new_line_index];
+ if (line_start_byte_offset >= byte_offset) {
+ continue;
+ }
+
+ const line: u32 = @intCast(new_line_index);
+ min_line = @min(min_line, line);
+ max_line = @max(max_line, line);
+ }
+
+ const did_fn_execute = function.executionCount > 0 or function.hasExecuted;
+
+ // only mark the lines as executable if the function has not executed
+ // functions that have executed have non-executable lines in them and thats fine.
+ if (!did_fn_execute) {
+ const end = @min(max_line, line_count);
+ for (min_line..end) |line| {
+ executable_lines.set(line);
+ lines_which_have_executed.unset(line);
+ }
+ }
+
+ try functions.append(allocator, .{
+ .start_line = min_line,
+ .end_line = max_line,
+ });
+
+ if (did_fn_execute)
+ functions_which_have_executed.set(i);
+ }
+ } else if (parsed_mappings_) |parsed_mapping| {
+ line_count = @as(u32, @truncate(parsed_mapping.input_line_count)) + 1;
+ executable_lines = try Bitset.initEmpty(allocator, line_count);
+ lines_which_have_executed = try Bitset.initEmpty(allocator, line_count);
+
+ for (blocks, 0..) |block, i| {
+ const min: usize = @intCast(@min(block.startOffset, block.endOffset));
+ const max: usize = @intCast(@max(block.startOffset, block.endOffset));
+ var min_line: u32 = std.math.maxInt(u32);
+ var max_line: u32 = 0;
+ const has_executed = block.hasExecuted or block.executionCount > 0;
+
+ for (min..max) |byte_offset| {
+ const new_line_index = LineOffsetTable.findIndex(line_starts, .{ .start = @intCast(byte_offset) }) orelse continue;
+ const line_start_byte_offset = line_starts[new_line_index];
+ if (line_start_byte_offset >= byte_offset) {
+ continue;
+ }
+ const column_position = byte_offset -| line_start_byte_offset;
+
+ if (SourceMap.Mapping.find(parsed_mapping.mappings, @intCast(new_line_index), @intCast(column_position))) |point| {
+ if (point.original.lines < 0) continue;
+
+ const line: u32 = @as(u32, @intCast(point.original.lines));
+
+ executable_lines.set(line);
+ if (has_executed) {
+ lines_which_have_executed.set(line);
+ }
+
+ min_line = @min(min_line, line);
+ max_line = @max(max_line, line);
+ }
+ }
+
+ if (min_line != std.math.maxInt(u32)) {
+ try stmts.append(allocator, .{
+ .start_line = min_line,
+ .end_line = max_line,
+ });
+
+ if (has_executed)
+ stmts_which_have_executed.set(i);
+ }
+ }
+
+ for (function_blocks, 0..) |function, i| {
+ const min: usize = @intCast(@min(function.startOffset, function.endOffset));
+ const max: usize = @intCast(@max(function.startOffset, function.endOffset));
+ var min_line: u32 = std.math.maxInt(u32);
+ var max_line: u32 = 0;
+
+ for (min..max) |byte_offset| {
+ const new_line_index = LineOffsetTable.findIndex(line_starts, .{ .start = @intCast(byte_offset) }) orelse continue;
+ const line_start_byte_offset = line_starts[new_line_index];
+ if (line_start_byte_offset >= byte_offset) {
+ continue;
+ }
+
+ const column_position = byte_offset -| line_start_byte_offset;
+
+ if (SourceMap.Mapping.find(parsed_mapping.mappings, @intCast(new_line_index), @intCast(column_position))) |point| {
+ if (point.original.lines < 0) continue;
+
+ const line: u32 = @as(u32, @intCast(point.original.lines));
+ min_line = @min(min_line, line);
+ max_line = @max(max_line, line);
+ }
+ }
+
+ // no sourcemaps? ignore it
+ if (min_line == std.math.maxInt(u32) and max_line == 0) {
+ continue;
+ }
+
+ const did_fn_execute = function.executionCount > 0 or function.hasExecuted;
+
+ // only mark the lines as executable if the function has not executed
+ // functions that have executed have non-executable lines in them and thats fine.
+ if (!did_fn_execute) {
+ const end = @min(max_line, line_count);
+ for (min_line..end) |line| {
+ executable_lines.set(line);
+ lines_which_have_executed.unset(line);
+ }
+ }
+
+ try functions.append(allocator, .{
+ .start_line = min_line,
+ .end_line = max_line,
+ });
+ if (did_fn_execute)
+ functions_which_have_executed.set(i);
+ }
+ } else {
+ unreachable;
+ }
+
+ return CodeCoverageReport{
+ .source_url = source_url,
+ .functions = functions,
+ .executable_lines = executable_lines,
+ .lines_which_have_executed = lines_which_have_executed,
+ .total_lines = line_count,
+ .stmts = stmts,
+ .functions_which_have_executed = functions_which_have_executed,
+ .stmts_which_have_executed = stmts_which_have_executed,
+ };
+ }
+
+ pub fn findExecutedLines(
+ globalThis: *bun.JSC.JSGlobalObject,
+ source_url: bun.String,
+ blocks_ptr: [*]const BasicBlockRange,
+ blocks_len: usize,
+ function_start_offset: usize,
+ ignore_sourcemap: bool,
+ ) callconv(.C) bun.JSC.JSValue {
+ var this = ByteRangeMapping.find(source_url) orelse return bun.JSC.JSValue.null;
+
+ const blocks: []const BasicBlockRange = blocks_ptr[0..function_start_offset];
+ var function_blocks: []const BasicBlockRange = blocks_ptr[function_start_offset..blocks_len];
+ if (function_blocks.len > 1) {
+ function_blocks = function_blocks[1..];
+ }
+ var url_slice = source_url.toUTF8(bun.default_allocator);
+ defer url_slice.deinit();
+ var report = this.generateCodeCoverageReportFromBlocks(bun.default_allocator, url_slice, blocks, function_blocks, ignore_sourcemap) catch {
+ globalThis.throwOutOfMemory();
+ return .zero;
+ };
+ defer report.deinit(bun.default_allocator);
+
+ var coverage_fraction = CoverageFraction{};
+
+ var mutable_str = bun.MutableString.initEmpty(bun.default_allocator);
+ defer mutable_str.deinit();
+ var buffered_writer = mutable_str.bufferedWriter();
+ var writer = buffered_writer.writer();
+
+ report.writeFormat(source_url.utf8ByteLength(), &coverage_fraction, "", &writer, false) catch {
+ globalThis.throwOutOfMemory();
+ return .zero;
+ };
+
+ buffered_writer.flush() catch {
+ globalThis.throwOutOfMemory();
+ return .zero;
+ };
+
+ var str = bun.String.create(mutable_str.toOwnedSliceLeaky());
+ defer str.deref();
+ return str.toJS(globalThis);
+ }
+
+ pub fn compute(source_contents: []const u8, source_id: i32, source_url: bun.JSC.ZigString.Slice) ByteRangeMapping {
+ return ByteRangeMapping{
+ .line_offset_table = LineOffsetTable.generate(bun.JSC.VirtualMachine.get().allocator, source_contents, 0),
+ .source_id = source_id,
+ .source_url = source_url,
+ };
+ }
+};
+
+comptime {
+ @export(ByteRangeMapping.generate, .{ .name = "ByteRangeMapping__generate" });
+ @export(ByteRangeMapping.findExecutedLines, .{ .name = "ByteRangeMapping__findExecutedLines" });
+ @export(ByteRangeMapping.find, .{ .name = "ByteRangeMapping__find" });
+ @export(ByteRangeMapping.getSourceID, .{ .name = "ByteRangeMapping__getSourceID" });
+}
+
+pub const CoverageFraction = struct {
+ functions: f64 = 0.9,
+ lines: f64 = 0.9,
+
+ // This metric is less accurate right now
+ stmts: f64 = 0.75,
+
+ failing: bool = false,
+};
diff --git a/src/sourcemap/sourcemap.zig b/src/sourcemap/sourcemap.zig
index 59dc2f0c6..0bcb4021d 100644
--- a/src/sourcemap/sourcemap.zig
+++ b/src/sourcemap/sourcemap.zig
@@ -179,6 +179,7 @@ pub const Mapping = struct {
bytes: []const u8,
estimated_mapping_count: ?usize,
sources_count: i32,
+ input_line_count: usize,
) ParseResult {
var mapping = Mapping.List{};
if (estimated_mapping_count) |count| {
@@ -366,7 +367,12 @@ pub const Mapping = struct {
}) catch unreachable;
}
- return ParseResult{ .success = mapping };
+ return ParseResult{
+ .success = .{
+ .mappings = mapping,
+ .input_line_count = input_line_count,
+ },
+ };
}
pub const ParseResult = union(enum) {
@@ -386,7 +392,17 @@ pub const Mapping = struct {
};
}
},
- success: Mapping.List,
+ success: ParsedSourceMap,
+ };
+
+ pub const ParsedSourceMap = struct {
+ input_line_count: usize = 0,
+ mappings: Mapping.List = .{},
+
+ pub fn deinit(this: *ParsedSourceMap, allocator: std.mem.Allocator) void {
+ this.mappings.deinit(allocator);
+ allocator.destroy(this);
+ }
};
};
@@ -845,6 +861,38 @@ pub const LineOffsetTable = struct {
return @as(i32, @intCast(original_line)) - 1;
}
+ pub fn findIndex(byte_offsets_to_start_of_line: []const u32, loc: Logger.Loc) ?usize {
+ std.debug.assert(loc.start > -1); // checked by caller
+ var original_line: usize = 0;
+ const loc_start = @as(usize, @intCast(loc.start));
+
+ var count = @as(usize, @truncate(byte_offsets_to_start_of_line.len));
+ var i: usize = 0;
+ while (count > 0) {
+ const step = count / 2;
+ i = original_line + step;
+ const byte_offset = byte_offsets_to_start_of_line[i];
+ if (byte_offset == loc_start) {
+ return i;
+ }
+ if (i + 1 < byte_offsets_to_start_of_line.len) {
+ const next_byte_offset = byte_offsets_to_start_of_line[i + 1];
+ if (byte_offset < loc_start and loc_start < next_byte_offset) {
+ return i;
+ }
+ }
+
+ if (byte_offset < loc_start) {
+ original_line = i + 1;
+ count = count - step - 1;
+ } else {
+ count = step;
+ }
+ }
+
+ return null;
+ }
+
pub fn generate(allocator: std.mem.Allocator, contents: []const u8, approximate_line_count: i32) List {
var list = List{};
// Preallocate the top-level table using the approximate line count from the lexer
@@ -1142,6 +1190,7 @@ pub const Chunk = struct {
data: MutableString,
count: usize = 0,
offset: usize = 0,
+ approximate_input_line_count: usize = 0,
pub const Format = SourceMapFormat(VLQSourceMap);
@@ -1152,8 +1201,8 @@ pub const Chunk = struct {
// For bun.js, we store the number of mappings and how many bytes the final list is at the beginning of the array
if (prepend_count) {
- map.offset = 16;
- map.data.append(&[16]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }) catch unreachable;
+ map.offset = 24;
+ map.data.append(&([_]u8{0} ** 24)) catch unreachable;
}
return map;
@@ -1211,6 +1260,8 @@ pub const Chunk = struct {
line_starts_with_mapping: bool = false,
cover_lines_without_mappings: bool = false,
+ approximate_input_line_count: usize = 0,
+
/// When generating sourcemappings for bun, we store a count of how many mappings there were
prepend_count: bool = false,
@@ -1221,6 +1272,7 @@ pub const Chunk = struct {
if (b.prepend_count) {
b.source_map.getBuffer().list.items[0..8].* = @as([8]u8, @bitCast(b.source_map.getBuffer().list.items.len));
b.source_map.getBuffer().list.items[8..16].* = @as([8]u8, @bitCast(b.source_map.getCount()));
+ b.source_map.getBuffer().list.items[16..24].* = @as([8]u8, @bitCast(b.approximate_input_line_count));
}
return Chunk{
.buffer = b.source_map.getBuffer(),