microsoft/vscode-react-native

Public

mirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
8b3830512db36fdbdab4085c9fa4da04242acbe5

Branches

Tags

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

Clone

HTTPS

Download ZIP

test/debugger/appWorker.test.ts

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