UNPKG

7.15 kBPlain TextView Raw
1import { ProcessCov, ScriptCov } from "@c88/v8-coverage";
2import assert from "assert";
3import cp from "child_process";
4import cri from "chrome-remote-interface";
5import { ChildProcessProxy, observeSpawn, ObserveSpawnOptions, SpawnEvent } from "demurgos-spawn-wrap";
6import Protocol from "devtools-protocol";
7import events from "events";
8import { SourceType } from "istanbulize";
9import { CoverageFilter } from "./filter";
10
11const DEBUGGER_URI_RE: RegExp = /ws:\/\/.*?:(\d+)\//;
12// In milliseconds (1s)
13const GET_DEBUGGER_PORT_TIMEOUT: number = 1000;
14// In milliseconds (10s)
15const GET_COVERAGE_TIMEOUT: number = 10000;
16
17export interface ScriptMeta {
18 sourceText: string;
19 sourceType: SourceType;
20 sourceMapUrl?: string;
21}
22
23export interface RichScriptCov extends ScriptCov, ScriptMeta {
24}
25
26export interface RichProcessCov extends ProcessCov {
27 result: RichScriptCov[];
28}
29
30export interface SpawnInspectedOptions extends ObserveSpawnOptions {
31 filter?: CoverageFilter;
32
33 onRootProcess?(process: cp.ChildProcess): any;
34}
35
36export async function spawnInspected(
37 file: string,
38 args: ReadonlyArray<string>,
39 options: SpawnInspectedOptions,
40): Promise<RichProcessCov[]> {
41 const processCovs: RichProcessCov[] = [];
42
43 return new Promise<RichProcessCov[]>((resolve, reject) => {
44 observeSpawn(file, args, options)
45 .subscribe(
46 async (ev: SpawnEvent) => {
47 try {
48 if (ev.rootProcess !== undefined && options.onRootProcess !== undefined) {
49 options.onRootProcess(ev.rootProcess);
50 }
51 const args: ReadonlyArray<string> = ["--inspect=0", ...ev.args];
52 const proxy: ChildProcessProxy = ev.proxySpawn(args);
53 const debuggerPort: number = await getDebuggerPort(proxy);
54 const processCov: RichProcessCov = await getCoverage(debuggerPort, options.filter);
55 processCovs.push(processCov);
56 } catch (err) {
57 reject(err);
58 }
59 },
60 reject,
61 () => resolve(processCovs),
62 );
63 });
64}
65
66export async function getDebuggerPort(proc: ChildProcessProxy): Promise<number> {
67 return new Promise<number>((resolve, reject) => {
68 const timeoutId: NodeJS.Timer = setTimeout(onTimeout, GET_DEBUGGER_PORT_TIMEOUT * 100);
69 let stderrBuffer: Buffer = Buffer.alloc(0);
70 proc.stderr.on("data", onStderrData);
71 proc.stderr.on("close", onClose);
72
73 function onStderrData(chunk: Buffer): void {
74 stderrBuffer = Buffer.concat([stderrBuffer, chunk]);
75 const stderrStr: string = stderrBuffer.toString("UTF-8");
76 const match: RegExpExecArray | null = DEBUGGER_URI_RE.exec(stderrStr);
77 if (match === null) {
78 return;
79 }
80 const result: number = parseInt(match[1], 10);
81 removeListeners();
82 resolve(result);
83 }
84
85 function onClose(code: number | null, signal: string | null): void {
86 removeListeners();
87 reject(new Error(`Unable to hook inspector (early exit, ${code}, ${signal})`));
88 }
89
90 function onTimeout(): void {
91 removeListeners();
92 reject(new Error("Unable to hook inspector (timeout)"));
93 // proc.kill();
94 }
95
96 function removeListeners(): void {
97 proc.stderr.removeListener("data", onStderrData);
98 proc.stderr.removeListener("close", onClose);
99 clearTimeout(timeoutId);
100 }
101 });
102}
103
104async function getCoverage(port: number, filter?: CoverageFilter): Promise<RichProcessCov> {
105 return new Promise<RichProcessCov>(async (resolve, reject) => {
106 const timeoutId: NodeJS.Timer = setTimeout(onTimeout, GET_COVERAGE_TIMEOUT);
107 let client: any;
108 let mainExecutionContextId: Protocol.Runtime.ExecutionContextId | undefined;
109 const scriptIdToMeta: Map<Protocol.Runtime.ScriptId, Partial<ScriptMeta>> = new Map();
110 let state: string = "WaitingForMainContext"; // TODO: enum
111 try {
112 client = await cri({port});
113
114 await client.Profiler.enable();
115 await client.Profiler.startPreciseCoverage({callCount: true, detailed: true});
116 await client.Debugger.enable();
117
118 (client as any as events.EventEmitter).once("Runtime.executionContextCreated", onMainContextCreation);
119 (client as any as events.EventEmitter).on("Runtime.executionContextDestroyed", onContextDestruction);
120 (client as any as events.EventEmitter).on("Debugger.scriptParsed", onScriptParsed);
121
122 await client.Runtime.enable();
123 } catch (err) {
124 removeListeners();
125 reject(err);
126 }
127
128 function onMainContextCreation(ev: Protocol.Runtime.ExecutionContextCreatedEvent) {
129 assert(state === "WaitingForMainContext");
130 mainExecutionContextId = ev.context.id;
131 state = "WaitingForMainContextDestruction";
132 }
133
134 function onScriptParsed(ev: Protocol.Debugger.ScriptParsedEvent) {
135 const collect: boolean = filter !== undefined ? filter(ev) : true;
136 if (collect) {
137 let sourceType: SourceType = SourceType.Script;
138 if (ev.isModule !== undefined) {
139 sourceType = ev.isModule ? SourceType.Module : SourceType.Script;
140 }
141 let sourceMapUrl: string | undefined;
142 if (ev.sourceMapURL !== undefined && ev.sourceMapURL !== "") {
143 sourceMapUrl = ev.sourceMapURL;
144 }
145 scriptIdToMeta.set(
146 ev.scriptId,
147 {
148 sourceType,
149 sourceMapUrl,
150 },
151 );
152 }
153 }
154
155 async function onContextDestruction(ev: Protocol.Runtime.ExecutionContextDestroyedEvent): Promise<void> {
156 assert(state === "WaitingForMainContextDestruction");
157 if (ev.executionContextId !== mainExecutionContextId) {
158 return;
159 }
160 state = "WaitingForCoverage";
161
162 try {
163 // await client.Profiler.stopPreciseCoverage();
164 await client.HeapProfiler.collectGarbage();
165 const {result: scriptCovs} = await client.Profiler.takePreciseCoverage();
166 const result: RichScriptCov[] = [];
167 for (const scriptCov of scriptCovs) {
168 const meta: Partial<ScriptMeta> | undefined = scriptIdToMeta.get(scriptCov.scriptId);
169 if (meta === undefined) {
170 // `undefined` means that the script was filtered out.
171 continue;
172 }
173 const {scriptSource} = await client.Debugger.getScriptSource({scriptId: scriptCov.scriptId});
174 result.push({
175 ...scriptCov,
176 sourceText: scriptSource,
177 ...meta,
178 } as RichScriptCov);
179 }
180 resolve({result});
181 } catch (err) {
182 reject(err);
183 } finally {
184 removeListeners();
185 }
186 }
187
188 function onTimeout(): void {
189 removeListeners();
190 reject(new Error("Unable to get V8 coverage (timeout)"));
191 }
192
193 function removeListeners(): void {
194 (client as any as events.EventEmitter).removeListener("Runtime.executionContextCreated", onMainContextCreation);
195 (client as any as events.EventEmitter).removeListener("Runtime.executionContextDestroyed", onContextDestruction);
196 (client as any as events.EventEmitter).removeListener("Runtime.scriptParsed", onScriptParsed);
197 clearTimeout(timeoutId);
198 (client as any).close();
199 }
200 });
201}
202
\No newline at end of file