microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
5759f79cea6d220d75c4589a028aec52100953d6

Branches

Tags

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

Clone

HTTPS

Download ZIP

test/debugger/appWorker.test.ts

535lines · 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 sinon from "sinon";
8import * as child_process from "child_process";
9import { MultipleLifetimesAppWorker } from "../../src/debugger/appWorker";
10import { ForkedAppWorker } from "../../src/debugger/forkedAppWorker";
11import * as ForkedAppWorkerModule from "../../src/debugger/forkedAppWorker";
12import * as packagerStatus from "../../src/common/packagerStatus";
13import { ScriptImporter, DownloadedScript } from "../../src/debugger/scriptImporter";
14import { PromiseUtil } from "../../src/common/node/promise";
15
16suite("appWorker", function () {
17 suite("debuggerContext", function () {
18 const packagerPort = 8081;
19
20 suite("SandboxedAppWorker", function () {
21 const originalSpawn = child_process.spawn;
22 const sourcesStoragePath = path.resolve(__dirname, "assets");
23
24 // Inject 5 sec delay before shutting down to worker to give tests some time to execute
25 const WORKER_DELAY_SHUTDOWN = `setTimeout(() => {console.log("Shutting down")}, 5000)`;
26
27 let testWorker: ForkedAppWorker;
28 let spawnStub: Sinon.SinonStub;
29 let postReplyFunction = sinon.stub();
30
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 PromiseUtil.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 PromiseUtil.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, "postReplyFunction called before message sent");
88 worker.postMessage(testMessage);
89 return PromiseUtil.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 = { promiseString: Promise.toString() };
97 const startScriptContents = `var testResponse = { promiseString: Promise.toString() };
98 postMessage(testResponse);`;
99
100 return workerWithScript(startScriptContents).start()
101 .then(() => PromiseUtil.delay(5000))
102 .then(() =>
103 assert(postReplyFunction.calledWithExactly(expectedMessageResult)));
104 }).timeout(5500);
105
106 test("should download script from remote packager", async () => {
107 class MockAppWorker extends ForkedAppWorker {
108 public workerLoaded: Promise<void>;
109 public scriptImporter: ScriptImporter;
110 public debuggeeProcess: any = {
111 send: () => void 0,
112 };
113 }
114 const remotePackagerAddress = "1.2.3.4";
115 const remotePackagerPort = 1337;
116 const worker = new MockAppWorker(remotePackagerAddress, remotePackagerPort, sourcesStoragePath, "", postReplyFunction);
117 const downloadAppScriptStub = sinon.stub(worker.scriptImporter, "downloadAppScript");
118 const fakeDownloadedScript = <DownloadedScript>{ filepath: "/home/test/file" };
119 downloadAppScriptStub.returns(Promise.resolve(fakeDownloadedScript));
120 const debuggeeProcessSendStub = sinon.stub(worker.debuggeeProcess, "send");
121 worker.workerLoaded = Promise.resolve();
122 const fakeMessage = {
123 method: "executeApplicationScript",
124 url: "http://localhost:8081/test-url",
125 };
126
127 await worker.postMessage(fakeMessage);
128
129 assert.strictEqual(downloadAppScriptStub.calledOnce, true);
130 assert.strictEqual(downloadAppScriptStub.firstCall.args[0], `http://${remotePackagerAddress}:${remotePackagerPort}/test-url`);
131 assert.strictEqual(debuggeeProcessSendStub.calledOnce, true);
132 assert.deepStrictEqual(debuggeeProcessSendStub.firstCall.args[0], {
133 data: {
134 ...fakeMessage,
135 url: worker.pathToFileUrl(fakeDownloadedScript.filepath),
136 },
137 });
138 });
139
140 test("debuggee process should pass its output to appWorker", () => {
141 class MockAppWorker extends ForkedAppWorker {
142 public getDebuggeeProcess() {
143 return this.debuggeeProcess;
144 }
145 }
146
147 const sourcesStoragePath = path.resolve(__dirname, "assets", "consoleLog");
148 const testWorker: MockAppWorker = new MockAppWorker("localhost", packagerPort, sourcesStoragePath, "", () => {});
149
150 let ws: WebSocket;
151 let waitForContinue: Promise<void>;
152 let waitForCheckingOutput: Promise<void>;
153 let debuggeeProcess: child_process.ChildProcess;
154
155 teardown((done) => {
156 if (ws) ws.close();
157 done();
158 });
159
160 const sendContinueToDebuggee = (wsDebuggerUrl: string,
161 resolve: (value?: void | PromiseLike<void> | undefined) => void,
162 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 PromiseUtil.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 waitForContinue = new Promise((resolve, reject) => {
186 return testWorker.start().then((port: number) => {
187 let output: string = "";
188 let debugOutput: string = "";
189 let isAlreadySending = false;
190 debuggeeProcess = testWorker.getDebuggeeProcess() as child_process.ChildProcess;
191 debuggeeProcess.stderr.on("data", (data: string) => {
192 // Two notices:
193 // 1. More correct way would be getting websocket debugger url by requesting GET http://localhost:debugPort/json/list
194 // but for some reason sometimes it returns ECONNRESET, so we have to find it in debug logs produced by debuggee
195 // 2. Debuggee process writes debug logs in stderr for some reasons
196 data = data.toString();
197 debugOutput += data;
198 console.log(data);
199 // Looking for websocket url
200
201 // 1. Node v8+: ws://127.0.0.1:31732/7dd4c075-3222-4f31-8fb5-50cc5705dd21
202 const guidPattern = "[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}";
203 const finalPattern = new RegExp(`(ws:\/\/127.0.0.1:[0-9]+\/${guidPattern}$)`, "gm");
204 let found = debugOutput.match(finalPattern);
205 if (found && !isAlreadySending) {
206 isAlreadySending = true;
207 // Debuggee process which has been ran with --debug-brk will be stopped at 0 line,
208 // so we have to send it a command to continue execution of the script via websocket.
209 sendContinueToDebuggee(found[0], resolve, reject);
210 return;
211 }
212
213 // 2. Node v6: ws=127.0.0.1:31732/7dd4c075-3222-4f31-8fb5-50cc5705dd21
214 found = debugOutput.match(/(ws=.+$)/gm);
215 if (found) {
216 sendContinueToDebuggee(found[0].replace("ws=", "ws:\\\\"), resolve, reject);
217 return;
218 }
219 });
220 debuggeeProcess.stdout.on("data", (data: string) => {
221 output += data;
222 });
223 debuggeeProcess.on("exit", () => {
224 assert.notEqual(output, "");
225 assert.strictEqual(output.trim(), "test output from debuggee process");
226 waitForCheckingOutput = Promise.resolve();
227 });
228 return waitForContinue;
229 }).then(() => {
230 debuggeeProcess.kill();
231 return waitForCheckingOutput;
232 });
233 });
234 }).timeout(5000);
235 });
236
237 suite("ScriptImporter", function () {
238 let scriptImporter: ScriptImporter;
239 setup(function () {
240 scriptImporter = new ScriptImporter("localhost", 8081, "sources");
241 });
242 test("prepareDebuggerWorkerURL should return correct URL for RN < 0.50.0", () => {
243 assert.strictEqual(scriptImporter.prepareDebuggerWorkerURL("0.49.0"), "http://localhost:8081/debuggerWorker.js");
244 });
245 test("prepareDebuggerWorkerURL should return correct URL for RN >= 0.50.0", () => {
246 assert.strictEqual(scriptImporter.prepareDebuggerWorkerURL("0.50.0"), "http://localhost:8081/debugger-ui/debuggerWorker.js");
247 });
248 test("prepareDebuggerWorkerURL should return correct URL when debuggerWorkerUrlPath is used", () => {
249 assert.strictEqual(scriptImporter.prepareDebuggerWorkerURL("0.55.4", ""), "http://localhost:8081/debuggerWorker.js");
250 assert.strictEqual(scriptImporter.prepareDebuggerWorkerURL("0.55.4", "new-debugger/"), "http://localhost:8081/new-debugger/debuggerWorker.js");
251 assert.strictEqual(scriptImporter.prepareDebuggerWorkerURL("0.55.4", undefined), "http://localhost:8081/debugger-ui/debuggerWorker.js");
252 assert.strictEqual(scriptImporter.prepareDebuggerWorkerURL("0.49.0", "debugger-ui/"), "http://localhost:8081/debugger-ui/debuggerWorker.js");
253 });
254 });
255
256 suite("MultipleLifetimesAppWorker", function () {
257 const sourcesStoragePath = path.resolve(__dirname, "assets");
258
259 let multipleLifetimesWorker: MultipleLifetimesAppWorker;
260 let sandboxedAppWorkerStub: Sinon.SinonStub;
261 let appWorkerModuleStub: Sinon.SinonStub;
262 let webSocket: Sinon.SinonStub;
263 let webSocketConstructor: Sinon.SinonStub;
264 let packagerIsRunning: Sinon.SinonStub;
265
266 let sendMessage: (message: string) => void;
267
268 let clock: Sinon.SinonFakeTimers;
269
270 setup(function () {
271 webSocket = sinon.createStubInstance(WebSocket);
272
273 sandboxedAppWorkerStub = sinon.createStubInstance(ForkedAppWorker);
274 appWorkerModuleStub = sinon.stub(ForkedAppWorkerModule, "ForkedAppWorker").returns(sandboxedAppWorkerStub);
275
276 const messageInvocation: Sinon.SinonStub = (<any>webSocket).on.withArgs("message");
277 sendMessage = (message: string) => messageInvocation.callArgWith(1, message);
278
279 webSocketConstructor = sinon.stub();
280 webSocketConstructor.returns(webSocket);
281 packagerIsRunning = sinon.stub(packagerStatus, "ensurePackagerRunning");
282 packagerIsRunning.returns(Promise.resolve());
283 const attachRequestArguments = {
284 address: "localhost",
285 port: packagerPort,
286 };
287
288 multipleLifetimesWorker = new MultipleLifetimesAppWorker(attachRequestArguments, sourcesStoragePath, "", {
289 webSocketConstructor: webSocketConstructor,
290 });
291
292 sinon.stub(multipleLifetimesWorker, "downloadAndPatchDebuggerWorker").returns(Promise.resolve());
293 });
294
295 teardown(function () {
296 // Reset everything
297 multipleLifetimesWorker.stop();
298 appWorkerModuleStub.restore();
299 packagerIsRunning.restore();
300
301 if (clock) {
302 clock.restore();
303 }
304 });
305
306 test("with packager running should construct a websocket connection to the correct endpoint and listen for events", function () {
307 return multipleLifetimesWorker.start().then(() => {
308 const websocketRegex = new RegExp("ws://[^:]*:[0-9]*/debugger-proxy\\?role=debugger");
309 assert(webSocketConstructor.calledWithMatch(websocketRegex), "The web socket was not constructed to the correct url: " + webSocketConstructor.args[0][0]);
310
311 const expectedListeners = ["open", "close", "message", "error"];
312 expectedListeners.forEach((event) => {
313 assert((<any>webSocket).on.calledWithMatch(event), `Missing listener for ${event}`);
314 });
315 });
316 });
317
318 test("with packager running should attempt to reconnect after disconnecting", function () {
319 let startWorker = sinon.spy(multipleLifetimesWorker, "start");
320 return multipleLifetimesWorker.start().then(() => {
321 // Forget previous invocations
322 startWorker.reset();
323 packagerIsRunning.returns(Promise.resolve());
324
325 clock = sinon.useFakeTimers();
326
327 const closeInvocation: Sinon.SinonStub = (<any>webSocket).on.withArgs("close");
328 closeInvocation.callArg(1);
329
330 // Ensure that the retry is 100ms after the disconnection
331 clock.tick(99);
332 assert(startWorker.notCalled, "Attempted to reconnect too quickly");
333
334 clock.tick(1);
335 }).then(() => {
336 assert(startWorker.called);
337 });
338 });
339
340 test("with packager running should respond correctly to prepareJSRuntime messages", function () {
341 return multipleLifetimesWorker.start().then(() => {
342 const messageId = 1;
343 const testMessage = JSON.stringify({ method: "prepareJSRuntime", id: messageId });
344 const expectedReply = JSON.stringify({ replyID: messageId });
345
346
347
348 const appWorkerStart: Sinon.SinonStub = (<any>sandboxedAppWorkerStub).start;
349 const websocketSend: Sinon.SinonStub = (<any>webSocket).send;
350
351 const appWorkerDeferred = new Promise((resolve) => {
352 appWorkerStart.returns(appWorkerDeferred);
353
354 sendMessage(testMessage);
355
356 assert(appWorkerStart.called, "SandboxedAppWorker not started in respones to prepareJSRuntime");
357 assert(websocketSend.notCalled, "Response sent prior to configuring sandbox worker");
358
359 resolve();
360 });
361
362
363 return PromiseUtil.delay(1).then(() => {
364 assert(websocketSend.calledWith(expectedReply), "Did not receive the expected response to prepareJSRuntime");
365 });
366 });
367 });
368
369 test("with packager running should pass unknown messages to the sandboxedAppWorker", function () {
370 return multipleLifetimesWorker.start().then(() => {
371 // Start up an app worker
372 const prepareJSMessage = JSON.stringify({ method: "prepareJSRuntime", id: 1 });
373 const appWorkerStart: Sinon.SinonStub = (<any>sandboxedAppWorkerStub).start;
374 appWorkerStart.returns(Promise.resolve());
375
376 sendMessage(prepareJSMessage);
377
378 // Then attempt to message it
379
380 const testMessage = { method: "unknownMethod" };
381 const testMessageString = JSON.stringify(testMessage);
382
383 const postMessageStub: Sinon.SinonStub = (<any>sandboxedAppWorkerStub).postMessage;
384
385 assert(postMessageStub.notCalled, "sandboxedAppWorker.postMessage called prior to any message");
386 sendMessage(testMessageString);
387
388 assert(postMessageStub.calledWith(testMessage), "message was not passed to sandboxedAppWorker");
389 });
390 });
391
392 test("with packager running should close connection if there is another debugger connected to packager", () => {
393 return multipleLifetimesWorker.start().then(() => {
394 // Forget previous invocations
395 webSocketConstructor.reset();
396 clock = sinon.useFakeTimers(new Date().getTime());
397
398 const closeInvocation: Sinon.SinonStub = (<any>webSocket).on.withArgs("close");
399 (<any>webSocket)._closeMessage = "Another debugger is already connected";
400 closeInvocation.callArg(1);
401
402 // Ensure it doesn't try to reconnect
403 clock.tick(100);
404 assert(webSocketConstructor.notCalled, "socket attempted to reconnect");
405 });
406 });
407
408 test("without packager running should not start if there is no packager running", () => {
409 packagerIsRunning.returns(Promise.reject(new Error()));
410
411 return multipleLifetimesWorker.start()
412 .then(() => {}, () => {
413 assert(webSocketConstructor.notCalled, "socket should not be created");
414 });
415 });
416 });
417
418 suite("console.trace()", function () {
419 function runScriptAndCheckOutput(expectedTraceMessage: string, consoleTraceCall: string, done: Mocha.Done): void {
420 const script = [MultipleLifetimesAppWorker.CONSOLE_TRACE_PATCH, consoleTraceCall].join("\n");
421 const testProcess = child_process.spawn("node", ["-e", script]);
422 let procData: string = "";
423 let procErrData: string = "";
424 testProcess.stdout.on("data", (data: Buffer) => {
425 procData += data.toString();
426 });
427 testProcess.stderr.on("data", (data: Buffer) => {
428 procErrData += data.toString();
429 });
430 testProcess.on("error", (err: Error) => {
431 console.error(err);
432 });
433 testProcess.on("close", (code: number) => {
434 assert.strictEqual(code, 0);
435 if (procErrData !== "") {
436 assert.fail(procErrData);
437 }
438 const traceContent = procData.trim().split("\n");
439 assert.strictEqual(traceContent[0], expectedTraceMessage);
440 traceContent.shift();
441 traceContent.forEach(element => {
442 assert.strictEqual(element.trim().startsWith("at"), true, `Stack frame ${element} isn't started with 'at'`);
443 });
444 done();
445 });
446 }
447
448 test("console.trace() patch should produce a correct output if called without args", (done: Mocha.Done) => {
449 const consoleTraceCall = `console.trace();`;
450 const expectedTraceMessage = "Trace";
451 runScriptAndCheckOutput(expectedTraceMessage, consoleTraceCall, done);
452 });
453
454 test("console.trace() patch should produce a correct output if called with simple args", (done: Mocha.Done) => {
455 const consoleTraceCall = `console.trace(\"Simple string\", 1337);`;
456 const expectedTraceMessage = "Trace: Simple string 1337";
457 runScriptAndCheckOutput(expectedTraceMessage, consoleTraceCall, done);
458 });
459
460 test("console.trace() patch should produce a correct output if called with formatted string", (done: Mocha.Done) => {
461 const consoleTraceCall = `console.trace("%s: %d", "Format string prints", 42);`;
462 const expectedTraceMessage = "Trace: Format string prints: 42";
463 runScriptAndCheckOutput(expectedTraceMessage, consoleTraceCall, done);
464 });
465 });
466
467 suite("process.toString()", function() {
468 test("process.toString() should return empty string to avoid errors in native app", (done: Mocha.Done) => {
469 const nodeProcessCheckToStringReturnCommand = "console.log(process.toString());";
470 const nodeProcessCheckCommand = "console.log(Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]');";
471 const script = [MultipleLifetimesAppWorker.PROCESS_TO_STRING_PATCH, nodeProcessCheckToStringReturnCommand, nodeProcessCheckCommand].join("\n");
472
473 const testProcess = child_process.spawn("node", ["-e", script]);
474 let procData: string = "";
475 let procErrData: string = "";
476 testProcess.stdout.on("data", (data: Buffer) => {
477 procData += data.toString();
478 });
479 testProcess.stderr.on("data", (data: Buffer) => {
480 procErrData += data.toString();
481 });
482 testProcess.on("error", (err: Error) => {
483 console.error(err);
484 });
485 testProcess.on("close", (code: number) => {
486 assert.strictEqual(code, 0);
487 if (procErrData !== "") {
488 assert.fail(procErrData);
489 }
490 const output = procData.split("\n");
491 assert.strictEqual(output[0].trim(), "");
492 assert.strictEqual(output[1].trim(), "false");
493 done();
494 });
495 });
496 });
497
498 suite("node --no-deprecation tests", function() {
499 test("node process should work with --no-deprecation flag and produce no deprecation warnings for Buffer function and GLOBAL variable usage", (done: Mocha.Done) => {
500 const globalVariableCheck = "GLOBAL.toString();";
501 const bufferCommandCheck = "Buffer('TestString');";
502 const script = [globalVariableCheck, bufferCommandCheck].join("\n");
503
504 const testProcess = child_process.spawn("node", ["--no-deprecation", "-e", script]);
505 let procData: string = "";
506 let procErrData: string = "";
507 testProcess.stdout.on("data", (data: Buffer) => {
508 procData += data.toString();
509 });
510 testProcess.stderr.on("data", (data: Buffer) => {
511 procErrData += data.toString();
512 });
513 testProcess.on("error", (err: Error) => {
514 console.error(err);
515 });
516 testProcess.on("close", (code: number) => {
517 assert.strictEqual(code, 0);
518 if (procErrData !== "") {
519 if (procErrData.indexOf("DeprecationWarning") !== -1 || procErrData.indexOf("DEP") !== -1) {
520 assert.fail(`Deprecation messages found in stderr:\n ${procErrData}`);
521 } else {
522 assert.fail(procErrData);
523 }
524 }
525 if (procData !== "") {
526 if (procData.indexOf("DeprecationWarning") !== -1 || procData.indexOf("DEP") !== -1) {
527 assert.fail(`Deprecation messages found in stdout:\n ${procData}`);
528 }
529 }
530 done();
531 });
532 });
533 });
534 });
535});