aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/bun-linux-aarch64.yml2
-rw-r--r--.github/workflows/bun-linux-build.yml4
-rw-r--r--.github/workflows/bun-mac-aarch64.yml16
-rw-r--r--.github/workflows/bun-mac-x64-baseline.yml16
-rw-r--r--.github/workflows/bun-mac-x64.yml16
-rw-r--r--Dockerfile2
-rwxr-xr-xbun.lockbbin72575 -> 72575 bytes
-rw-r--r--docs/nav.ts3
-rw-r--r--docs/test/coverage.md57
-rw-r--r--package.json2
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
-rw-r--r--test/harness.ts14
-rw-r--r--test/js/node/path/path.test.js1
32 files changed, 1211 insertions, 61 deletions
diff --git a/.github/workflows/bun-linux-aarch64.yml b/.github/workflows/bun-linux-aarch64.yml
index f40c0b5b5..d29fa0b7b 100644
--- a/.github/workflows/bun-linux-aarch64.yml
+++ b/.github/workflows/bun-linux-aarch64.yml
@@ -36,7 +36,7 @@ jobs:
arch: aarch64
build_arch: arm64
runner: linux-arm64
- webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-linux-arm64-lto.tar.gz"
+ webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-linux-arm64-lto.tar.gz"
webkit_basename: "bun-webkit-linux-arm64-lto"
build_machine_arch: aarch64
diff --git a/.github/workflows/bun-linux-build.yml b/.github/workflows/bun-linux-build.yml
index 02c9b94eb..f4b288d8e 100644
--- a/.github/workflows/bun-linux-build.yml
+++ b/.github/workflows/bun-linux-build.yml
@@ -46,7 +46,7 @@ jobs:
arch: x86_64
build_arch: amd64
runner: big-ubuntu
- webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-linux-amd64-lto.tar.gz"
+ webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-linux-amd64-lto.tar.gz"
webkit_basename: "bun-webkit-linux-amd64-lto"
build_machine_arch: x86_64
- cpu: nehalem
@@ -54,7 +54,7 @@ jobs:
arch: x86_64
build_arch: amd64
runner: big-ubuntu
- webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-linux-amd64-lto.tar.gz"
+ webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-linux-amd64-lto.tar.gz"
webkit_basename: "bun-webkit-linux-amd64-lto"
build_machine_arch: x86_64
diff --git a/.github/workflows/bun-mac-aarch64.yml b/.github/workflows/bun-mac-aarch64.yml
index 8946f109d..52258374e 100644
--- a/.github/workflows/bun-mac-aarch64.yml
+++ b/.github/workflows/bun-mac-aarch64.yml
@@ -117,7 +117,7 @@ jobs:
# obj: bun-obj-darwin-x64-baseline
# runner: macos-11
# artifact: bun-obj-darwin-x64-baseline
- # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
# dependencies: true
# compile_obj: false
# - cpu: haswell
@@ -126,7 +126,7 @@ jobs:
# obj: bun-obj-darwin-x64
# runner: macos-11
# artifact: bun-obj-darwin-x64
- # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
# dependencies: true
# compile_obj: false
# - cpu: nehalem
@@ -135,7 +135,7 @@ jobs:
# obj: bun-obj-darwin-x64-baseline
# runner: macos-11
# artifact: bun-obj-darwin-x64-baseline
- # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
# dependencies: false
# compile_obj: true
# - cpu: haswell
@@ -144,7 +144,7 @@ jobs:
# obj: bun-obj-darwin-x64
# runner: macos-11
# artifact: bun-obj-darwin-x64
- # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
# dependencies: false
# compile_obj: true
- cpu: native
@@ -152,7 +152,7 @@ jobs:
tag: bun-darwin-aarch64
obj: bun-obj-darwin-aarch64
artifact: bun-obj-darwin-aarch64
- webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-arm64-lto.tar.gz"
+ webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-arm64-lto.tar.gz"
runner: macos-arm64
dependencies: true
compile_obj: true
@@ -257,7 +257,7 @@ jobs:
# package: bun-darwin-x64
# runner: macos-11
# artifact: bun-obj-darwin-x64-baseline
- # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
# - cpu: haswell
# arch: x86_64
# tag: bun-darwin-x64
@@ -265,14 +265,14 @@ jobs:
# package: bun-darwin-x64
# runner: macos-11
# artifact: bun-obj-darwin-x64
- # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
- cpu: native
arch: aarch64
tag: bun-darwin-aarch64
obj: bun-obj-darwin-aarch64
package: bun-darwin-aarch64
artifact: bun-obj-darwin-aarch64
- webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-arm64-lto.tar.gz"
+ webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-arm64-lto.tar.gz"
runner: macos-arm64
steps:
- uses: actions/checkout@v3
diff --git a/.github/workflows/bun-mac-x64-baseline.yml b/.github/workflows/bun-mac-x64-baseline.yml
index 83feeaf20..049a96691 100644
--- a/.github/workflows/bun-mac-x64-baseline.yml
+++ b/.github/workflows/bun-mac-x64-baseline.yml
@@ -117,7 +117,7 @@ jobs:
obj: bun-obj-darwin-x64-baseline
runner: macos-11
artifact: bun-obj-darwin-x64-baseline
- webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
dependencies: true
compile_obj: false
# - cpu: haswell
@@ -126,7 +126,7 @@ jobs:
# obj: bun-obj-darwin-x64
# runner: macos-11
# artifact: bun-obj-darwin-x64
- # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
# dependencies: true
# compile_obj: false
- cpu: nehalem
@@ -135,7 +135,7 @@ jobs:
obj: bun-obj-darwin-x64-baseline
runner: macos-11
artifact: bun-obj-darwin-x64-baseline
- webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
dependencies: false
compile_obj: true
# - cpu: haswell
@@ -144,7 +144,7 @@ jobs:
# obj: bun-obj-darwin-x64
# runner: macos-11
# artifact: bun-obj-darwin-x64
- # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
# dependencies: false
# compile_obj: true
# - cpu: native
@@ -152,7 +152,7 @@ jobs:
# tag: bun-darwin-aarch64
# obj: bun-obj-darwin-aarch64
# artifact: bun-obj-darwin-aarch64
- # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
# runner: macos-arm64
# dependencies: true
# compile_obj: true
@@ -258,7 +258,7 @@ jobs:
package: bun-darwin-x64
runner: macos-11
artifact: bun-obj-darwin-x64-baseline
- webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
# - cpu: haswell
# arch: x86_64
# tag: bun-darwin-x64
@@ -266,14 +266,14 @@ jobs:
# package: bun-darwin-x64
# runner: macos-11
# artifact: bun-obj-darwin-x64
- # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
# - cpu: native
# arch: aarch64
# tag: bun-darwin-aarch64
# obj: bun-obj-darwin-aarch64
# package: bun-darwin-aarch64
# artifact: bun-obj-darwin-aarch64
- # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
# runner: macos-arm64
steps:
- uses: actions/checkout@v3
diff --git a/.github/workflows/bun-mac-x64.yml b/.github/workflows/bun-mac-x64.yml
index d76a4abb5..4289c578f 100644
--- a/.github/workflows/bun-mac-x64.yml
+++ b/.github/workflows/bun-mac-x64.yml
@@ -117,7 +117,7 @@ jobs:
# obj: bun-obj-darwin-x64-baseline
# runner: macos-11
# artifact: bun-obj-darwin-x64-baseline
- # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
# dependencies: true
# compile_obj: false
- cpu: haswell
@@ -126,7 +126,7 @@ jobs:
obj: bun-obj-darwin-x64
runner: macos-11
artifact: bun-obj-darwin-x64
- webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
dependencies: true
compile_obj: false
# - cpu: nehalem
@@ -135,7 +135,7 @@ jobs:
# obj: bun-obj-darwin-x64-baseline
# runner: macos-11
# artifact: bun-obj-darwin-x64-baseline
- # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
# dependencies: false
# compile_obj: true
- cpu: haswell
@@ -144,7 +144,7 @@ jobs:
obj: bun-obj-darwin-x64
runner: macos-11
artifact: bun-obj-darwin-x64
- webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
dependencies: false
compile_obj: true
# - cpu: native
@@ -152,7 +152,7 @@ jobs:
# tag: bun-darwin-aarch64
# obj: bun-obj-darwin-aarch64
# artifact: bun-obj-darwin-aarch64
- # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-arm64-lto.tar.gz"
+ # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-arm64-lto.tar.gz"
# runner: macos-arm64
# dependencies: true
# compile_obj: true
@@ -260,7 +260,7 @@ jobs:
# package: bun-darwin-x64
# runner: macos-11
# artifact: bun-obj-darwin-x64-baseline
- # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
- cpu: haswell
arch: x86_64
tag: bun-darwin-x64
@@ -268,14 +268,14 @@ jobs:
package: bun-darwin-x64
runner: macos-11
artifact: bun-obj-darwin-x64
- webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-amd64-lto.tar.gz"
+ webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-amd64-lto.tar.gz"
# - cpu: native
# arch: aarch64
# tag: bun-darwin-aarch64
# obj: bun-obj-darwin-aarch64
# package: bun-darwin-aarch64
# artifact: bun-obj-darwin-aarch64
- # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-1/bun-webkit-macos-arm64-lto.tar.gz"
+ # webkit_url: "https://github.com/oven-sh/WebKit/releases/download/2023-aug3-2/bun-webkit-macos-arm64-lto.tar.gz"
# runner: macos-arm64
steps:
- uses: actions/checkout@v3
diff --git a/Dockerfile b/Dockerfile
index 7e829ef30..9042ea802 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,7 +10,7 @@ ARG ARCH=x86_64
ARG BUILD_MACHINE_ARCH=x86_64
ARG TRIPLET=${ARCH}-linux-gnu
ARG BUILDARCH=amd64
-ARG WEBKIT_TAG=2023-aug3-1
+ARG WEBKIT_TAG=2023-aug3-2
ARG ZIG_TAG=jul1
ARG ZIG_VERSION="0.11.0-dev.4006+bf827d0b5"
ARG WEBKIT_BASENAME="bun-webkit-linux-$BUILDARCH"
diff --git a/bun.lockb b/bun.lockb
index 6ddea0ad3..76f780282 100755
--- a/bun.lockb
+++ b/bun.lockb
Binary files differ
diff --git a/docs/nav.ts b/docs/nav.ts
index 9eadf59be..ff74e16c3 100644
--- a/docs/nav.ts
+++ b/docs/nav.ts
@@ -207,6 +207,9 @@ export default {
page("test/dom", "DOM testing", {
description: "Write headless tests for UI and React/Vue/Svelte/Lit components with happy-dom",
}),
+ page("test/coverage", "Code coverage", {
+ description: "Generate code coverage reports with `bun test --coverage`",
+ }),
divider("Package runner"),
page("cli/bunx", "`bunx`", {
diff --git a/docs/test/coverage.md b/docs/test/coverage.md
new file mode 100644
index 000000000..abf8cf94c
--- /dev/null
+++ b/docs/test/coverage.md
@@ -0,0 +1,57 @@
+`bun:test` supports seeing which lines of code are covered by tests. To use this feature, pass `--coverage` to the CLI:
+
+```sh
+bun test --coverage
+```
+
+It will print out a coverage report to the console:
+
+```js
+-------------|---------|---------|-------------------
+File | % Funcs | % Lines | Uncovered Line #s
+-------------|---------|---------|-------------------
+All files | 38.89 | 42.11 |
+ index-0.ts | 33.33 | 36.84 | 10-15,19-24
+ index-1.ts | 33.33 | 36.84 | 10-15,19-24
+ index-10.ts | 33.33 | 36.84 | 10-15,19-24
+ index-2.ts | 33.33 | 36.84 | 10-15,19-24
+ index-3.ts | 33.33 | 36.84 | 10-15,19-24
+ index-4.ts | 33.33 | 36.84 | 10-15,19-24
+ index-5.ts | 33.33 | 36.84 | 10-15,19-24
+ index-6.ts | 33.33 | 36.84 | 10-15,19-24
+ index-7.ts | 33.33 | 36.84 | 10-15,19-24
+ index-8.ts | 33.33 | 36.84 | 10-15,19-24
+ index-9.ts | 33.33 | 36.84 | 10-15,19-24
+ index.ts | 100.00 | 100.00 |
+-------------|---------|---------|-------------------
+```
+
+If coverage is below a threshold, `bun:test` will exit with a non-zero exit code to indicate the failure.
+
+### Configuring coverage
+
+`bunfig.toml` supports configuring coverage:
+
+```toml
+[test]
+
+# Always enable coverage
+coverage = true
+
+# Anything less than 90% coverage will fail the test
+# coverageThreshold = 0.9
+coverageThreshold = { line = 0.9, function = 0.9 }
+
+
+# Don't include .test.* files in coverage reports
+coverageSkipTestFiles = true
+
+# Disable sourcemap support in coverage reports
+# By default, coverage reports will automatically use Bun's internal sourcemap.
+# You probably don't want to configure this
+# coverageIgnoreSourcemaps = false
+```
+
+`coverageThreshold` can be either a number or an object with `line` and `function` keys. When a number, it is treated as both the line and function threshold.
+
+Coverage support was added in Bun v0.7.2.
diff --git a/package.json b/package.json
index 5cb6cf8e7..4697689f9 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
"@types/react": "^18.0.25",
"@typescript-eslint/eslint-plugin": "^5.31.0",
"@typescript-eslint/parser": "^5.31.0",
- "bun-webkit": "0.0.1-9d9172d3242b40f16fa980ead750dc9ab248065f"
+ "bun-webkit": "0.0.1-74609640b2a7c5a1588b824f870d1b0ff91bfd8e"
},
"version": "0.0.0",
"prettier": "./.prettierrc.cjs"
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(),
diff --git a/test/harness.ts b/test/harness.ts
index ab3ae5ca2..2da12e094 100644
--- a/test/harness.ts
+++ b/test/harness.ts
@@ -1,8 +1,4 @@
import { gc as bunGC, unsafe, which } from "bun";
-import { heapStats } from "bun:jsc";
-import path from "path";
-import fs from "fs";
-import os from "os";
export const bunEnv: any = {
...process.env,
@@ -43,6 +39,8 @@ export async function expectMaxObjectTypeCount(
count: number,
maxWait = 1000,
) {
+ var { heapStats } = require("bun:jsc");
+
gc();
if (heapStats().objectTypeCounts[type] <= count) return;
gc(true);
@@ -85,7 +83,11 @@ export function hideFromStackTrace(block: CallableFunction) {
}
export function tempDirWithFiles(basename: string, files: Record<string, string | Record<string, string>>) {
- const dir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), basename + "_"));
+ var fs = require("fs");
+ var path = require("path");
+ var { tmpdir } = require("os");
+
+ const dir = fs.mkdtempSync(path.join(fs.realpathSync(tmpdir()), basename + "_"));
for (const [name, contents] of Object.entries(files)) {
if (typeof contents === "object") {
fs.mkdirSync(path.join(dir, name));
@@ -100,6 +102,7 @@ export function tempDirWithFiles(basename: string, files: Record<string, string
}
export function bunRun(file: string, env?: Record<string, string>) {
+ var path = require("path");
const result = Bun.spawnSync([bunExe(), file], {
cwd: path.dirname(file),
env: {
@@ -116,6 +119,7 @@ export function bunRun(file: string, env?: Record<string, string>) {
}
export function bunTest(file: string, env?: Record<string, string>) {
+ var path = require("path");
const result = Bun.spawnSync([bunExe(), "test", path.basename(file)], {
cwd: path.dirname(file),
env: {
diff --git a/test/js/node/path/path.test.js b/test/js/node/path/path.test.js
index 103809d2e..fb6063968 100644
--- a/test/js/node/path/path.test.js
+++ b/test/js/node/path/path.test.js
@@ -547,7 +547,6 @@ it("path.normalize", () => {
// "..\\..\\..\\..\\baz"
// );
// strictEqual(path.win32.normalize("foo/bar\\baz"), "foo\\bar\\baz");
-
strictEqual(path.posix.normalize("./fixtures///b/../b/c.js"), "fixtures/b/c.js");
strictEqual(path.posix.normalize("/foo/../../../bar"), "/bar");
strictEqual(path.posix.normalize("a//b//../b"), "a/b");