1 | import { ProcessCov, ScriptCov } from "@c88/v8-coverage";
|
2 | import assert from "assert";
|
3 | import cp from "child_process";
|
4 | import cri from "chrome-remote-interface";
|
5 | import Protocol from "devtools-protocol";
|
6 | import events from "events";
|
7 | import { SourceType } from "istanbulize";
|
8 | import { InspectorClient, InspectorServer } from "node-inspector-server";
|
9 | import { CoverageFilter } from "./filter";
|
10 |
|
11 | export interface ScriptMeta {
|
12 | sourceText: string;
|
13 | sourceType: SourceType;
|
14 | sourceMapUrl?: string;
|
15 | }
|
16 |
|
17 | export interface RichScriptCov extends ScriptCov, ScriptMeta {
|
18 | }
|
19 |
|
20 | export interface RichProcessCov extends ProcessCov {
|
21 | result: RichScriptCov[];
|
22 | }
|
23 |
|
24 | export interface SpawnInspectedOptions extends cp.SpawnOptions {
|
25 | filter?: CoverageFilter;
|
26 |
|
27 | timeout?: number;
|
28 |
|
29 | onRootProcess?(process: cp.ChildProcess): any;
|
30 | }
|
31 |
|
32 | export 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 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
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 |
|
73 | async 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";
|
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 |
|
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 |
|
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 |
|
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 |