UNPKG

6.21 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 Protocol from "devtools-protocol";
6import events from "events";
7import { SourceType } from "istanbulize";
8import { InspectorClient, InspectorServer } from "node-inspector-server";
9import { CoverageFilter } from "./filter";
10
11export interface ScriptMeta {
12 sourceText: string;
13 sourceType: SourceType;
14 sourceMapUrl?: string;
15}
16
17export interface RichScriptCov extends ScriptCov, ScriptMeta {
18}
19
20export interface RichProcessCov extends ProcessCov {
21 result: RichScriptCov[];
22}
23
24export interface SpawnInspectedOptions extends cp.SpawnOptions {
25 filter?: CoverageFilter;
26
27 timeout?: number;
28
29 onRootProcess?(process: cp.ChildProcess): any;
30}
31
32export async function spawnInspected(
33 file: string,
34 args: ReadonlyArray<string>,
35 options: SpawnInspectedOptions,
36): Promise<RichProcessCov[]> {
37 const processCovs: RichProcessCov[] = [];
38
39 const srv: InspectorServer = await InspectorServer.open();
40
41 return new Promise<RichProcessCov[]>((resolve, reject) => {
42 srv
43 .subscribe(
44 async (ev: InspectorClient) => {
45 try {
46 // if (ev.rootProcess !== undefined && options.onRootProcess !== undefined) {
47 // options.onRootProcess(ev.rootProcess);
48 // }
49 // const args: ReadonlyArray<string> = ["--inspect=0", ...ev.args];
50 // const proxy: ChildProcessProxy = ev.proxySpawn(args);
51 // const debuggerPort: number = await getDebuggerPort(proxy);
52 const processCov: RichProcessCov = await getCoverage(ev.url, options.filter, options.timeout);
53 processCovs.push(processCov);
54 } catch (err) {
55 reject(err);
56 }
57 },
58 reject,
59 () => resolve(processCovs),
60 );
61
62 const child: cp.ChildProcess = srv.spawn(file, args, options);
63 if (options.onRootProcess !== undefined) {
64 options.onRootProcess(child);
65 }
66
67 child.on("close", () => {
68 srv.closeSync();
69 });
70 });
71}
72
73async function getCoverage(url: string, filter?: CoverageFilter, timeout?: number): Promise<RichProcessCov> {
74 return new Promise<RichProcessCov>(async (resolve, reject) => {
75 const timeoutId: NodeJS.Timer | undefined = timeout !== undefined ? setTimeout(onTimeout, timeout) : undefined;
76 let session: any;
77 let mainExecutionContextId: Protocol.Runtime.ExecutionContextId | undefined;
78 const scriptIdToMeta: Map<Protocol.Runtime.ScriptId, Partial<ScriptMeta>> = new Map();
79 let state: string = "WaitingForMainContext"; // TODO: enum
80 try {
81 session = await cri({target: url});
82 (session as any as events.EventEmitter).once("Runtime.executionContextCreated", onMainContextCreation);
83 (session as any as events.EventEmitter).on("Runtime.executionContextDestroyed", onContextDestruction);
84 (session as any as events.EventEmitter).on("Debugger.scriptParsed", onScriptParsed);
85
86 await session.Profiler.enable();
87 await session.Profiler.startPreciseCoverage({callCount: true, detailed: true});
88 await session.Debugger.enable();
89 await session.Runtime.enable();
90 await session.Runtime.runIfWaitingForDebugger();
91 } catch (err) {
92 removeListeners();
93 reject(err);
94 }
95
96 function onMainContextCreation(ev: Protocol.Runtime.ExecutionContextCreatedEvent) {
97 assert(state === "WaitingForMainContext");
98 mainExecutionContextId = ev.context.id;
99 state = "WaitingForMainContextDestruction";
100 }
101
102 function onScriptParsed(ev: Protocol.Debugger.ScriptParsedEvent) {
103 const collect: boolean = filter !== undefined ? filter(ev) : true;
104 if (collect) {
105 let sourceType: SourceType = SourceType.Script;
106 if (ev.isModule !== undefined) {
107 sourceType = ev.isModule ? SourceType.Module : SourceType.Script;
108 }
109 let sourceMapUrl: string | undefined;
110 if (ev.sourceMapURL !== undefined && ev.sourceMapURL !== "") {
111 sourceMapUrl = ev.sourceMapURL;
112 }
113 scriptIdToMeta.set(
114 ev.scriptId,
115 {
116 sourceType,
117 sourceMapUrl,
118 },
119 );
120 }
121 }
122
123 async function onContextDestruction(ev: Protocol.Runtime.ExecutionContextDestroyedEvent): Promise<void> {
124 assert(state === "WaitingForMainContextDestruction");
125 if (ev.executionContextId !== mainExecutionContextId) {
126 return;
127 }
128 state = "WaitingForCoverage";
129
130 try {
131 // await session.Profiler.stopPreciseCoverage();
132 await session.HeapProfiler.collectGarbage();
133 const {result: scriptCovs} = await session.Profiler.takePreciseCoverage();
134 const result: RichScriptCov[] = [];
135 for (const scriptCov of scriptCovs) {
136 const meta: Partial<ScriptMeta> | undefined = scriptIdToMeta.get(scriptCov.scriptId);
137 if (meta === undefined) {
138 // `undefined` means that the script was filtered out.
139 continue;
140 }
141 const {scriptSource} = await session.Debugger.getScriptSource({scriptId: scriptCov.scriptId});
142 result.push({
143 ...scriptCov,
144 sourceText: scriptSource,
145 ...meta,
146 } as RichScriptCov);
147 }
148 resolve({result});
149 } catch (err) {
150 reject(err);
151 } finally {
152 removeListeners();
153 }
154 }
155
156 function onTimeout(): void {
157 removeListeners();
158 reject(new Error("Unable to get V8 coverage (timeout)"));
159 }
160
161 function removeListeners(): void {
162 if (session === undefined) {
163 // Failure before the session is created
164 return;
165 }
166
167 (session as any as events.EventEmitter).removeListener("Runtime.executionContextCreated", onMainContextCreation);
168 (session as any as events.EventEmitter).removeListener("Runtime.executionContextDestroyed", onContextDestruction);
169 (session as any as events.EventEmitter).removeListener("Runtime.scriptParsed", onScriptParsed);
170 if (timeoutId !== undefined) {
171 clearTimeout(timeoutId);
172 }
173 (session as any).close();
174 }
175 });
176}
177
\No newline at end of file