microsoft/vscode-react-native

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
b2b5d016a126b337e01405c6006a069da028e0bf

Branches

Tags

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

Clone

HTTPS

Download ZIP

test/debugger/appWorker.test.ts

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