microsoft/vscode-react-native

Public

mirrored from https://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
e6f8520eb54d0bbc6e3410ff39332bc322621490

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

test/debugger/appWorker.test.ts

389lines · modecode

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT license. See LICENSE file in the project root for details.
3
4import * as assert from "assert";
5import * as WebSocket from "ws";
6import * as path from "path";
7import * as Q from "q";
8import * as sinon from "sinon";
9import * as child_process from "child_process";
10
11import { MultipleLifetimesAppWorker } from "../../src/debugger/appWorker";
12import { ForkedAppWorker } from "../../src/debugger/forkedAppWorker";
13import * as ForkedAppWorkerModule from "../../src/debugger/forkedAppWorker";
14import * as packagerStatus from "../../src/common/packagerStatus";
15import { ScriptImporter, DownloadedScript } from "../../src/debugger/scriptImporter";
16
17suite("appWorker", function () {
18 suite("debuggerContext", function () {
19 const packagerPort = 8081;
20
21 suite("SandboxedAppWorker", function () {
22 const originalSpawn = child_process.spawn;
23 const sourcesStoragePath = path.resolve(__dirname, "assets");
24
25 // Inject 5 sec delay before shutting down to worker to give tests some time to execute
26 const WORKER_DELAY_SHUTDOWN = `setTimeout(() => {console.log("Shutting down")}, 5000)`;
27
28 let testWorker: ForkedAppWorker;
29 let spawnStub: Sinon.SinonStub;
30 let postReplyFunction = sinon.stub();
31
32 function workerWithScript(scriptBody: string): ForkedAppWorker {
33 const wrappedBody = [MultipleLifetimesAppWorker.WORKER_BOOTSTRAP,
34 scriptBody, MultipleLifetimesAppWorker.WORKER_DONE, WORKER_DELAY_SHUTDOWN].join("\n");
35
36 spawnStub = sinon.stub(child_process, "spawn", () =>
37 originalSpawn("node", ["-e", wrappedBody], { stdio: ["pipe", "pipe", "pipe", "ipc"] }));
38
39 testWorker = new ForkedAppWorker("localhost", packagerPort, sourcesStoragePath, "", postReplyFunction);
40 return testWorker;
41 }
42
43 teardown(function () {
44 // Reset everything
45 if (spawnStub) {
46 spawnStub.restore();
47 }
48 postReplyFunction.reset();
49 if (testWorker) {
50 testWorker.stop();
51 }
52 });
53
54 test("should execute scripts correctly and be able to invoke the callback", function () {
55 const expectedMessageResult = { success: true };
56 const startScriptContents = `var testResponse = ${JSON.stringify(expectedMessageResult)}; postMessage(testResponse);`;
57
58 return workerWithScript(startScriptContents).start()
59 .then(() =>
60 Q.delay(1000))
61 .then(() =>
62 assert(postReplyFunction.calledWithExactly(expectedMessageResult)));
63 });
64
65 test("should be able to import scripts", function () {
66 // NOTE: we're not able to mock reading script for import since this is performed by a
67 // separate node process and is out of control so we must provide a real script file
68 const scriptImportPath = path.resolve(sourcesStoragePath, "importScriptsTest.js").replace(/\\/g, "/");
69 const startScriptContents = `importScripts("${scriptImportPath}"); postMessage("postImport");`;
70
71 return workerWithScript(startScriptContents).start().then(() => {
72 // We have not yet finished importing the script, we should not have posted a response yet
73 assert(postReplyFunction.notCalled, "postReplyFuncton called before scripts imported");
74 return Q.delay(500);
75 }).then(() => {
76 assert(postReplyFunction.calledWith("postImport"), "postMessage after import not handled");
77 assert(postReplyFunction.calledWith("inImport"), "postMessage not registered from within import");
78 });
79 });
80
81 test("should correctly pass postMessage to the loaded script", function () {
82 const startScriptContents = `onmessage = postMessage;`;
83 const testMessage = { method: "test", success: true };
84
85 const worker = workerWithScript(startScriptContents);
86 return worker.start().then(() => {
87 assert(postReplyFunction.notCalled, "postRepyFunction called before message sent");
88 worker.postMessage(testMessage);
89 return Q.delay(1000);
90 }).then(() => {
91 assert(postReplyFunction.calledWith({ data: testMessage }), "No echo back from app");
92 });
93 });
94
95 test("should be able to require an installed node module via __debug__.require", function () {
96 const expectedMessageResult = { qString: Q.toString() };
97 const startScriptContents = `var Q = __debug__.require('q');
98 var testResponse = { qString: Q.toString() };
99 postMessage(testResponse);`;
100
101 return workerWithScript(startScriptContents).start()
102 .then(() => Q.delay(500))
103 .then(() =>
104 assert(postReplyFunction.calledWithExactly(expectedMessageResult)));
105 });
106
107 test("should download script from remote packager", async () => {
108 class MockAppWorker extends ForkedAppWorker {
109 public workerLoaded = Q.defer<void>();
110 public scriptImporter: ScriptImporter;
111 public debuggeeProcess: any = {
112 send: () => void 0,
113 };
114 }
115 const remotePackagerAddress = "1.2.3.4";
116 const remotePackagerPort = 1337;
117 const worker = new MockAppWorker(remotePackagerAddress, remotePackagerPort, sourcesStoragePath, "", postReplyFunction);
118 const downloadAppScriptStub = sinon.stub(worker.scriptImporter, "downloadAppScript");
119 const fakeDownloadedScript = <DownloadedScript>{ filepath: "/home/test/file" };
120 downloadAppScriptStub.returns(Q.resolve(fakeDownloadedScript));
121 const debuggeeProcessSendStub = sinon.stub(worker.debuggeeProcess, "send");
122 worker.workerLoaded.resolve(void 0);
123 const fakeMessage = {
124 method: "executeApplicationScript",
125 url: "http://localhost:8081/test-url",
126 };
127
128 await worker.postMessage(fakeMessage);
129
130 assert.equal(downloadAppScriptStub.calledOnce, true);
131 assert.equal(downloadAppScriptStub.firstCall.args[0], `http://${remotePackagerAddress}:${remotePackagerPort}/test-url`);
132 assert.equal(debuggeeProcessSendStub.calledOnce, true);
133 assert.deepEqual(debuggeeProcessSendStub.firstCall.args[0], {
134 data: {
135 ...fakeMessage,
136 url: fakeDownloadedScript.filepath,
137 },
138 });
139 });
140
141 test("debuggee process should pass its output to appWorker", () => {
142 class MockAppWorker extends ForkedAppWorker {
143 public getDebuggeeProcess() {
144 return this.debuggeeProcess;
145 }
146 }
147
148 const sourcesStoragePath = path.resolve(__dirname, "assets", "consoleLog");
149 const testWorker: MockAppWorker = new MockAppWorker("localhost", packagerPort, sourcesStoragePath, "", () => {});
150
151 let ws: WebSocket;
152 let waitForContinue = Q.defer();
153 let waitForCheckingOutput = Q.defer();
154 let debuggeeProcess: child_process.ChildProcess;
155
156 teardown((done) => {
157 if (ws) ws.close();
158 done();
159 });
160
161 const sendContinueToDebuggee = (wsDebuggerUrl: string, resolve: (value: {}) => void, reject: (reason: any) => void) => {
162 ws = new WebSocket(wsDebuggerUrl);
163 ws.on("open", function open() {
164 ws.send(JSON.stringify({
165 // id is just a random number, because debugging protocol requires it
166 "id": 100,
167 "method": "Runtime.runIfWaitingForDebugger",
168 }), (err: Error) => {
169 if (err) {
170 reject(err);
171 }
172 // Delay is needed for debuggee process to execute script
173 return Q.delay(1000).then(() => {
174 resolve({});
175 });
176 });
177 });
178 ws.on("error", (err) => {
179 // Suppress any errors from websocket client otherwise you'd get ECONNRESET or 400 errors
180 // for some reasons
181 });
182 };
183
184 return testWorker.start().then((port: number) => {
185 let output: string = "";
186 debuggeeProcess = testWorker.getDebuggeeProcess() as child_process.ChildProcess;
187 debuggeeProcess.stderr.on("data", (data: string) => {
188 // Two notices:
189 // 1. More correct way would be getting websocket debugger url by requesting GET http://localhost:debugPort/json/list
190 // but for some reason sometimes it returns ECONNRESET, so we have to find it in debug logs produced by debuggee
191 // 2. Debuggee process writes debug logs in stderr for some reasons
192 data = data.toString();
193 console.log(data);
194 // Looking for websocket url
195 // 1. Node v8+: ws://127.0.0.1:31732/7dd4c075-3222-4f31-8fb5-50cc5705dd21
196 let found = data.match(/(ws:\/\/.+$)/gm);
197 if (found) {
198 // Debuggee process which has been ran with --debug-brk will be stopped at 0 line,
199 // so we have to send it a command to continue execution of the script via websocket.
200 sendContinueToDebuggee(found[0], waitForContinue.resolve, waitForContinue.reject);
201 return;
202 }
203
204 // 2. Node v6: ws=127.0.0.1:31732/7dd4c075-3222-4f31-8fb5-50cc5705dd21
205 found = data.match(/(ws=.+$)/gm);
206 if (found) {
207 sendContinueToDebuggee(found[0].replace("ws=", "ws:\\\\"), waitForContinue.resolve, waitForContinue.reject);
208 return;
209 }
210 });
211 debuggeeProcess.stdout.on("data", (data: string) => {
212 output += data;
213 });
214 debuggeeProcess.on("exit", () => {
215 assert.notEqual(output, "");
216 assert.equal(output.trim(), "test output from debuggee process");
217 waitForCheckingOutput.resolve({});
218 });
219 return waitForContinue.promise;
220 }).then(() => {
221 debuggeeProcess.kill();
222 return waitForCheckingOutput.promise;
223 });
224 });
225 });
226
227 suite("MultipleLifetimesAppWorker", function () {
228 const sourcesStoragePath = path.resolve(__dirname, "assets");
229
230 let multipleLifetimesWorker: MultipleLifetimesAppWorker;
231 let sandboxedAppWorkerStub: Sinon.SinonStub;
232 let appWorkerModuleStub: Sinon.SinonStub;
233 let webSocket: Sinon.SinonStub;
234 let webSocketConstructor: Sinon.SinonStub;
235 let packagerIsRunning: Sinon.SinonStub;
236
237 let sendMessage: (message: string) => void;
238
239 let clock: Sinon.SinonFakeTimers;
240
241 setup(function () {
242 webSocket = sinon.createStubInstance(WebSocket);
243
244 sandboxedAppWorkerStub = sinon.createStubInstance(ForkedAppWorker);
245 appWorkerModuleStub = sinon.stub(ForkedAppWorkerModule, "ForkedAppWorker").returns(sandboxedAppWorkerStub);
246
247 const messageInvocation: Sinon.SinonStub = (<any>webSocket).on.withArgs("message");
248 sendMessage = (message: string) => messageInvocation.callArgWith(1, message);
249
250 webSocketConstructor = sinon.stub();
251 webSocketConstructor.returns(webSocket);
252 packagerIsRunning = sinon.stub(packagerStatus, "ensurePackagerRunning");
253 packagerIsRunning.returns(Q.resolve(true));
254 const attachRequestArguments = {
255 address: "localhost",
256 port: packagerPort,
257 };
258
259 multipleLifetimesWorker = new MultipleLifetimesAppWorker(attachRequestArguments, sourcesStoragePath, "", {
260 webSocketConstructor: webSocketConstructor,
261 });
262
263 sinon.stub(multipleLifetimesWorker, "downloadAndPatchDebuggerWorker").returns(Q.resolve({}));
264 });
265
266 teardown(function () {
267 // Reset everything
268 multipleLifetimesWorker.stop();
269 appWorkerModuleStub.restore();
270 packagerIsRunning.restore();
271
272 if (clock) {
273 clock.restore();
274 }
275 });
276
277 test("with packager running should construct a websocket connection to the correct endpoint and listen for events", function () {
278 return multipleLifetimesWorker.start().then(() => {
279 const websocketRegex = new RegExp("ws://[^:]*:[0-9]*/debugger-proxy\\?role=debugger");
280 assert(webSocketConstructor.calledWithMatch(websocketRegex), "The web socket was not constructed to the correct url: " + webSocketConstructor.args[0][0]);
281
282 const expectedListeners = ["open", "close", "message", "error"];
283 expectedListeners.forEach((event) => {
284 assert((<any>webSocket).on.calledWithMatch(event), `Missing listener for ${event}`);
285 });
286 });
287 });
288
289 test("with packager running should attempt to reconnect after disconnecting", function () {
290 let startWorker = sinon.spy(multipleLifetimesWorker, "start");
291 return multipleLifetimesWorker.start().then(() => {
292 // Forget previous invocations
293 startWorker.reset();
294 packagerIsRunning.returns(Q.resolve(true));
295
296 clock = sinon.useFakeTimers();
297
298 const closeInvocation: Sinon.SinonStub = (<any>webSocket).on.withArgs("close");
299 closeInvocation.callArg(1);
300
301 // Ensure that the retry is 100ms after the disconnection
302 clock.tick(99);
303 assert(startWorker.notCalled, "Attempted to reconnect too quickly");
304
305 clock.tick(1);
306 }).then(() => {
307 assert(startWorker.called);
308 });
309 });
310
311 test("with packager running should respond correctly to prepareJSRuntime messages", function () {
312 return multipleLifetimesWorker.start().then(() => {
313 const messageId = 1;
314 const testMessage = JSON.stringify({ method: "prepareJSRuntime", id: messageId });
315 const expectedReply = JSON.stringify({ replyID: messageId });
316
317 const appWorkerDeferred = Q.defer<void>();
318
319 const appWorkerStart: Sinon.SinonStub = (<any>sandboxedAppWorkerStub).start;
320 const websocketSend: Sinon.SinonStub = (<any>webSocket).send;
321
322 appWorkerStart.returns(appWorkerDeferred.promise);
323
324 sendMessage(testMessage);
325
326 assert(appWorkerStart.called, "SandboxedAppWorker not started in respones to prepareJSRuntime");
327 assert(websocketSend.notCalled, "Response sent prior to configuring sandbox worker");
328
329 appWorkerDeferred.resolve(void 0);
330
331 return Q.delay(1).then(() => {
332 assert(websocketSend.calledWith(expectedReply), "Did not receive the expected response to prepareJSRuntime");
333 });
334 });
335 });
336
337 test("with packager running should pass unknown messages to the sandboxedAppWorker", function () {
338 return multipleLifetimesWorker.start().then(() => {
339 // Start up an app worker
340 const prepareJSMessage = JSON.stringify({ method: "prepareJSRuntime", id: 1 });
341 const appWorkerStart: Sinon.SinonStub = (<any>sandboxedAppWorkerStub).start;
342 appWorkerStart.returns(Q.resolve(void 0));
343
344 sendMessage(prepareJSMessage);
345
346 // Then attempt to message it
347
348 const testMessage = { method: "unknownMethod" };
349 const testMessageString = JSON.stringify(testMessage);
350
351 const postMessageStub: Sinon.SinonStub = (<any>sandboxedAppWorkerStub).postMessage;
352
353 assert(postMessageStub.notCalled, "sandboxedAppWorker.postMessage called prior to any message");
354 sendMessage(testMessageString);
355
356 assert(postMessageStub.calledWith(testMessage), "message was not passed to sandboxedAppWorker");
357 });
358 });
359
360 test("with packager running should close connection if there is another debugger connected to packager", () => {
361 return multipleLifetimesWorker.start().then(() => {
362 // Forget previous invocations
363 webSocketConstructor.reset();
364 clock = sinon.useFakeTimers(new Date().getTime());
365
366 const closeInvocation: Sinon.SinonStub = (<any>webSocket).on.withArgs("close");
367 (<any>webSocket)._closeMessage = "Another debugger is already connected";
368 closeInvocation.callArg(1);
369
370 // Ensure it doesn't try to reconnect
371 clock.tick(100);
372 assert(webSocketConstructor.notCalled, "socket attempted to reconnect");
373 });
374 });
375
376 test("without packager running should not start if there is no packager running", () => {
377 packagerIsRunning.returns(Q.reject(false));
378
379 return multipleLifetimesWorker.start()
380 .done(() => {
381 assert(webSocketConstructor.notCalled, "socket should not be created");
382 }, reason => {
383 assert(reason.message === `Cannot attach to packager. Are you sure there is a packager and it is running in the port ${packagerPort}? If your packager is configured to run in another port make sure to add that to the setting.json.`);
384 });
385 });
386 });
387
388 });
389});