Skip to content

Commit 2b74b54

Browse files
committed
test: ESM source map registration and preamble line-shift
Verifies that the ESM loader's source map handling produces correct output: separate (not inline) maps, VLQ mappings shifted by the preamble line count, registration call embedded in the preamble, and the __jazzer_registerSourceMap global feeding into SourceMapRegistry.
1 parent 74b07b0 commit 2b74b54

1 file changed

Lines changed: 176 additions & 0 deletions

File tree

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
* Copyright 2026 Code Intelligence GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { PluginItem, transformSync } from "@babel/core";
18+
19+
import { esmCodeCoverage } from "./plugins/esmCodeCoverage";
20+
import { compareHooks } from "./plugins/compareHooks";
21+
import { SourceMap, SourceMapRegistry } from "./SourceMapRegistry";
22+
23+
const COUNTER_ARRAY = "__jazzer_cov";
24+
25+
/**
26+
* Replicate the ESM loader's instrumentModule logic so we can test
27+
* the source map handling without running a real loader thread.
28+
*/
29+
function instrumentModule(
30+
code: string,
31+
filename: string,
32+
extraPlugins: PluginItem[] = [],
33+
): { source: string; map: SourceMap | null } | null {
34+
const fuzzerCoverage = esmCodeCoverage();
35+
const plugins: PluginItem[] = [
36+
fuzzerCoverage.plugin,
37+
compareHooks,
38+
...extraPlugins,
39+
];
40+
41+
const transformed = transformSync(code, {
42+
filename,
43+
sourceFileName: filename,
44+
sourceMaps: true,
45+
plugins,
46+
sourceType: "module",
47+
});
48+
49+
const edges = fuzzerCoverage.edgeCount();
50+
if (edges === 0 || !transformed?.code) {
51+
return null;
52+
}
53+
54+
const preambleLines = [
55+
`const ${COUNTER_ARRAY} = Fuzzer.coverageTracker.createModuleCounters(${edges});`,
56+
];
57+
58+
let shiftedMap: SourceMap | null = null;
59+
if (transformed.map) {
60+
const preambleOffset = preambleLines.length + 1;
61+
shiftedMap = {
62+
...transformed.map,
63+
mappings: ";".repeat(preambleOffset) + transformed.map.mappings,
64+
} as SourceMap;
65+
preambleLines.push(
66+
`__jazzer_registerSourceMap(${JSON.stringify(filename)}, ${JSON.stringify(shiftedMap)});`,
67+
);
68+
}
69+
70+
return {
71+
source: preambleLines.join("\n") + "\n" + transformed.code,
72+
map: shiftedMap,
73+
};
74+
}
75+
76+
describe("ESM source map handling", () => {
77+
it("should produce a separate source map, not an inline one", () => {
78+
const result = instrumentModule(
79+
"export function greet() { return 'hi'; }",
80+
"/app/greet.mjs",
81+
);
82+
83+
expect(result).not.toBeNull();
84+
expect(result!.source).not.toContain("sourceMappingURL=data:");
85+
expect(result!.map).not.toBeNull();
86+
expect(result!.map!.version).toBe(3);
87+
});
88+
89+
it("should shift mappings by the number of preamble lines", () => {
90+
const result = instrumentModule(
91+
"export function greet() { return 'hi'; }",
92+
"/app/greet.mjs",
93+
);
94+
95+
expect(result!.map).not.toBeNull();
96+
const mappings = result!.map!.mappings;
97+
98+
// The preamble has 2 lines (counter allocation + source map registration).
99+
// Each prepended ";" represents an unmapped generated line.
100+
expect(mappings.startsWith(";;")).toBe(true);
101+
102+
// The real mappings follow — they should not be empty.
103+
const realMappings = mappings.replace(/^;+/, "");
104+
expect(realMappings.length).toBeGreaterThan(0);
105+
});
106+
107+
it("should embed a registration call in the preamble", () => {
108+
const filename = "/app/target.mjs";
109+
const result = instrumentModule(
110+
"export function check(s) { if (s === 'x') throw new Error(); }",
111+
filename,
112+
);
113+
114+
const lines = result!.source.split("\n");
115+
116+
// Line 1: counter allocation
117+
expect(lines[0]).toContain("Fuzzer.coverageTracker.createModuleCounters");
118+
119+
// Line 2: source map registration with the correct filename
120+
expect(lines[1]).toContain("__jazzer_registerSourceMap");
121+
expect(lines[1]).toContain(JSON.stringify(filename));
122+
123+
// The registration call should contain valid JSON for the source map
124+
const match = lines[1].match(/__jazzer_registerSourceMap\([^,]+, (.+)\);$/);
125+
expect(match).not.toBeNull();
126+
const embeddedMap = JSON.parse(match![1]);
127+
expect(embeddedMap.version).toBe(3);
128+
expect(embeddedMap.sources).toContain(filename);
129+
});
130+
131+
it("should register maps with SourceMapRegistry via the global", () => {
132+
const registry = new SourceMapRegistry();
133+
const filename = "/app/module.mjs";
134+
const fakeMap: SourceMap = {
135+
version: 3,
136+
sources: [filename],
137+
names: [],
138+
mappings: "AAAA",
139+
file: filename,
140+
};
141+
142+
// Simulate what Instrumentor.init() installs
143+
(globalThis as Record<string, unknown>).__jazzer_registerSourceMap = (
144+
f: string,
145+
m: SourceMap,
146+
) => registry.registerSourceMap(f, m);
147+
148+
// Simulate what the preamble does at module evaluation time
149+
const register = (globalThis as Record<string, unknown>)
150+
.__jazzer_registerSourceMap as (f: string, m: SourceMap) => void;
151+
register(filename, fakeMap);
152+
153+
expect(registry.getSourceMap(filename)).toEqual(fakeMap);
154+
155+
// Cleanup
156+
delete (globalThis as Record<string, unknown>).__jazzer_registerSourceMap;
157+
});
158+
159+
it("should preserve original source file in the map", () => {
160+
const filename = "/project/src/lib.mjs";
161+
const result = instrumentModule(
162+
[
163+
"export function add(a, b) {",
164+
" return a + b;",
165+
"}",
166+
"export function sub(a, b) {",
167+
" return a - b;",
168+
"}",
169+
].join("\n"),
170+
filename,
171+
);
172+
173+
expect(result!.map!.sources).toContain(filename);
174+
expect(result!.map!.mappings.split(";").length).toBeGreaterThan(2);
175+
});
176+
});

0 commit comments

Comments
 (0)