head_dbg.js (30359B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 /* eslint-disable no-shadow */ 4 5 "use strict"; 6 var CC = Components.Constructor; 7 8 // Populate AppInfo before anything (like the shared loader) accesses 9 // System.appinfo, which is a lazy getter. 10 const appInfo = ChromeUtils.importESModule( 11 "resource://testing-common/AppInfo.sys.mjs" 12 ); 13 appInfo.updateAppInfo({ 14 ID: "devtools@tests.mozilla.org", 15 name: "devtools-tests", 16 version: "1", 17 platformVersion: "42", 18 crashReporter: true, 19 }); 20 21 const { require, loader } = ChromeUtils.importESModule( 22 "resource://devtools/shared/loader/Loader.sys.mjs" 23 ); 24 const { worker } = ChromeUtils.importESModule( 25 "resource://devtools/shared/loader/worker-loader.sys.mjs" 26 ); 27 28 const { NetUtil } = ChromeUtils.importESModule( 29 "resource://gre/modules/NetUtil.sys.mjs" 30 ); 31 32 // Always log packets when running tests. runxpcshelltests.py will throw 33 // the output away anyway, unless you give it the --verbose flag. 34 Services.prefs.setBoolPref("devtools.debugger.log", false); 35 // Enable remote debugging for the relevant tests. 36 Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true); 37 38 const makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js"); 39 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); 40 const { 41 ActorRegistry, 42 } = require("resource://devtools/server/actors/utils/actor-registry.js"); 43 const { 44 DevToolsServer, 45 } = require("resource://devtools/server/devtools-server.js"); 46 const { DevToolsServer: WorkerDevToolsServer } = worker.require( 47 "resource://devtools/server/devtools-server.js" 48 ); 49 const { 50 DevToolsClient, 51 } = require("resource://devtools/client/devtools-client.js"); 52 const { ObjectFront } = require("resource://devtools/client/fronts/object.js"); 53 const { 54 LongStringFront, 55 } = require("resource://devtools/client/fronts/string.js"); 56 const { 57 createCommandsDictionary, 58 } = require("resource://devtools/shared/commands/index.js"); 59 const { 60 CommandsFactory, 61 } = require("resource://devtools/shared/commands/commands-factory.js"); 62 63 const { addDebuggerToGlobal } = ChromeUtils.importESModule( 64 "resource://gre/modules/jsdebugger.sys.mjs" 65 ); 66 67 const { AddonTestUtils } = ChromeUtils.importESModule( 68 "resource://testing-common/AddonTestUtils.sys.mjs" 69 ); 70 const { getAppInfo } = ChromeUtils.importESModule( 71 "resource://testing-common/AppInfo.sys.mjs" 72 ); 73 74 const systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].createInstance( 75 Ci.nsIPrincipal 76 ); 77 78 var { loadSubScript, loadSubScriptWithOptions } = Services.scriptloader; 79 80 /** 81 * The logic here must resemble the logic of --start-debugger-server as closely 82 * as possible. DevToolsStartup.sys.mjs uses a distinct loader that results in 83 * the existence of two isolated module namespaces. In practice, this can cause 84 * bugs such as bug 1837185. 85 */ 86 function getDistinctDevToolsServer() { 87 const { 88 useDistinctSystemPrincipalLoader, 89 releaseDistinctSystemPrincipalLoader, 90 } = ChromeUtils.importESModule( 91 "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs", 92 { global: "shared" } 93 ); 94 const requester = {}; 95 const distinctLoader = useDistinctSystemPrincipalLoader(requester); 96 registerCleanupFunction(() => { 97 releaseDistinctSystemPrincipalLoader(requester); 98 }); 99 100 const { DevToolsServer: DistinctDevToolsServer } = distinctLoader.require( 101 "resource://devtools/server/devtools-server.js" 102 ); 103 return DistinctDevToolsServer; 104 } 105 106 /** 107 * Initializes any test that needs to work with add-ons. 108 * 109 * Should be called once per test script that needs to use AddonTestUtils (and 110 * not once per test task!). 111 */ 112 async function startupAddonsManager() { 113 // Create a directory for extensions. 114 const profileDir = do_get_profile().clone(); 115 profileDir.append("extensions"); 116 117 AddonTestUtils.init(globalThis); 118 AddonTestUtils.overrideCertDB(); 119 AddonTestUtils.appInfo = getAppInfo(); 120 121 await AddonTestUtils.promiseStartupManager(); 122 } 123 124 async function createTargetForFakeTab(title) { 125 const client = await startTestDevToolsServer(title); 126 127 const tabs = await listTabs(client); 128 const tabDescriptor = findTab(tabs, title); 129 130 // These xpcshell tests use mocked actors (xpcshell-test/testactors) 131 // which still don't support watcher actor. 132 // Because of that we still can't enable server side targets and target swiching. 133 tabDescriptor.disableTargetSwitching(); 134 135 return tabDescriptor.getTarget(); 136 } 137 138 async function createTargetForMainProcess() { 139 const commands = await CommandsFactory.forMainProcess(); 140 return commands.descriptorFront.getTarget(); 141 } 142 143 /** 144 * Create a MemoryFront for a fake test tab. 145 */ 146 async function createTabMemoryFront() { 147 const target = await createTargetForFakeTab("test_memory"); 148 149 // MemoryFront requires the HeadSnapshotActor actor to be available 150 // as a global actor. This isn't registered by startTestDevToolsServer which 151 // only register the target actors and not the browser ones. 152 DevToolsServer.registerActors({ browser: true }); 153 154 const memoryFront = await target.getFront("memory"); 155 await memoryFront.attach(); 156 157 registerCleanupFunction(async () => { 158 await memoryFront.detach(); 159 160 // On XPCShell, the target isn't for a local tab and so target.destroy 161 // won't close the client. So do it so here. It will automatically destroy the target. 162 await target.client.close(); 163 }); 164 165 return { target, memoryFront }; 166 } 167 168 /** 169 * Same as createTabMemoryFront but attaches the MemoryFront to the MemoryActor 170 * scoped to the full runtime rather than to a tab. 171 */ 172 async function createMainProcessMemoryFront() { 173 const target = await createTargetForMainProcess(); 174 175 const memoryFront = await target.getFront("memory"); 176 await memoryFront.attach(); 177 178 registerCleanupFunction(async () => { 179 await memoryFront.detach(); 180 // For XPCShell, the main process target actor is ContentProcessTargetActor 181 // which doesn't expose any `detach` method. So that the target actor isn't 182 // destroyed when calling target.destroy. 183 // Close the client to cleanup everything. 184 await target.client.close(); 185 }); 186 187 return { client: target.client, memoryFront }; 188 } 189 190 function createLongStringFront(conn, form) { 191 // CAUTION -- do not replicate in the codebase. Instead, use marshalling 192 // This code is simulating how the LongStringFront would be created by protocol.js 193 // We should not use it like this in the codebase, this is done only for testing 194 // purposes until we can return a proper LongStringFront from the server. 195 const front = new LongStringFront(conn, form); 196 front.actorID = form.actor; 197 front.manage(front); 198 return front; 199 } 200 201 function createTestGlobal(name, options) { 202 // By default, use a content principal to better simulate running as a web page 203 const origin = "http://example.com/" + name; 204 const principal = options?.chrome 205 ? Services.scriptSecurityManager.getSystemPrincipal() 206 : Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); 207 208 const chromeWebNav = Services.appShell.createWindowlessBrowser(false); 209 const { docShell } = chromeWebNav; 210 docShell.createAboutBlankDocumentViewer(principal, principal); 211 const window = docShell.docViewer.DOMDocument.defaultView; 212 window.document.title = name; 213 214 // When using a chrome document, the window object won't be an xray wrapper. 215 return window.wrappedJSObject || window; 216 } 217 218 function connect(client) { 219 dump("Connecting client.\n"); 220 return client.connect(); 221 } 222 223 function close(client) { 224 dump("Closing client.\n"); 225 return client.close(); 226 } 227 228 function listTabs(client) { 229 dump("Listing tabs.\n"); 230 return client.mainRoot.listTabs(); 231 } 232 233 function findTab(tabs, title) { 234 dump("Finding tab with title '" + title + "'.\n"); 235 for (const tab of tabs) { 236 if (tab.title === title) { 237 return tab; 238 } 239 } 240 return null; 241 } 242 243 function waitForNewSource(threadFront, url) { 244 dump("Waiting for new source with url '" + url + "'.\n"); 245 return waitForEvent(threadFront, "newSource", function (packet) { 246 return packet.source.url === url; 247 }); 248 } 249 250 function resume(threadFront) { 251 dump("Resuming thread.\n"); 252 return threadFront.resume(); 253 } 254 255 async function addWatchpoint(threadFront, frame, variable, property, type) { 256 const path = `${variable}.${property}`; 257 info(`Add an ${path} ${type} watchpoint`); 258 const environment = await frame.getEnvironment(); 259 const obj = environment.bindings.variables[variable]; 260 const objFront = threadFront.pauseGrip(obj.value); 261 return objFront.addWatchpoint(property, path, type); 262 } 263 264 function getSources(threadFront) { 265 dump("Getting sources.\n"); 266 return threadFront.getSources(); 267 } 268 269 function findSource(sources, url) { 270 dump("Finding source with url '" + url + "'.\n"); 271 for (const source of sources) { 272 if (source.url === url) { 273 return source; 274 } 275 } 276 return null; 277 } 278 279 function waitForPause(threadFront) { 280 dump("Waiting for pause.\n"); 281 return waitForEvent(threadFront, "paused"); 282 } 283 284 function waitForProperty(dbg, property) { 285 return new Promise(resolve => { 286 Object.defineProperty(dbg, property, { 287 set(newValue) { 288 resolve(newValue); 289 }, 290 }); 291 }); 292 } 293 294 function setBreakpoint(threadFront, location) { 295 dump("Setting breakpoint.\n"); 296 return threadFront.setBreakpoint(location, {}); 297 } 298 299 function getPrototypeAndProperties(objClient) { 300 dump("getting prototype and properties.\n"); 301 302 return objClient.getPrototypeAndProperties(); 303 } 304 305 function dumpn(msg) { 306 dump("DBG-TEST: " + msg + "\n"); 307 } 308 309 function testExceptionHook(ex) { 310 try { 311 do_report_unexpected_exception(ex); 312 } catch (e) { 313 return { throw: e }; 314 } 315 return undefined; 316 } 317 318 // Convert an nsIScriptError 'logLevel' value into an appropriate string. 319 function scriptErrorLogLevel(message) { 320 switch (message.logLevel) { 321 case Ci.nsIConsoleMessage.info: 322 return "info"; 323 case Ci.nsIConsoleMessage.warn: 324 return "warning"; 325 default: 326 Assert.equal(message.logLevel, Ci.nsIConsoleMessage.error); 327 return "error"; 328 } 329 } 330 331 // Register a console listener, so console messages don't just disappear 332 // into the ether. 333 var errorCount = 0; 334 var listener = { 335 observe(message) { 336 try { 337 let string; 338 errorCount++; 339 try { 340 // If we've been given an nsIScriptError, then we can print out 341 // something nicely formatted, for tools like Emacs to pick up. 342 message.QueryInterface(Ci.nsIScriptError); 343 dumpn( 344 message.sourceName + 345 ":" + 346 message.lineNumber + 347 ": " + 348 scriptErrorLogLevel(message) + 349 ": " + 350 message.errorMessage 351 ); 352 string = message.errorMessage; 353 } catch (e1) { 354 // Be a little paranoid with message, as the whole goal here is to lose 355 // no information. 356 try { 357 string = "" + message.message; 358 } catch (e2) { 359 string = "<error converting error message to string>"; 360 } 361 } 362 363 // Make sure we exit all nested event loops so that the test can finish. 364 while ( 365 DevToolsServer && 366 DevToolsServer.xpcInspector && 367 DevToolsServer.xpcInspector.eventLoopNestLevel > 0 368 ) { 369 DevToolsServer.xpcInspector.exitNestedEventLoop(); 370 } 371 372 // In the world before bug 997440, exceptions were getting lost because of 373 // the arbitrary JSContext being used in nsXPCWrappedJS::CallMethod. 374 // In the new world, the wanderers have returned. However, because of the, 375 // currently very-broken, exception reporting machinery in 376 // nsXPCWrappedJS these get reported as errors to the console, even if 377 // there's actually JS on the stack above that will catch them. If we 378 // throw an error here because of them our tests start failing. So, we'll 379 // just dump the message to the logs instead, to make sure the information 380 // isn't lost. 381 dumpn("head_dbg.js observed a console message: " + string); 382 } catch (_) { 383 // Swallow everything to avoid console reentrancy errors. We did our best 384 // to log above, but apparently that didn't cut it. 385 } 386 }, 387 }; 388 389 Services.console.registerListener(listener); 390 391 function addTestGlobal(name, server = DevToolsServer) { 392 const global = createTestGlobal(name); 393 server.addTestGlobal(global); 394 return global; 395 } 396 397 // List the DevToolsClient |client|'s tabs, look for one whose title is 398 // |title|. 399 async function getTestTab(client, title) { 400 const tabs = await client.mainRoot.listTabs(); 401 for (const tab of tabs) { 402 if (tab.title === title) { 403 return tab; 404 } 405 } 406 return null; 407 } 408 /** 409 * Attach to the client's tab whose title is specified 410 * 411 * @param {object} client 412 * @param {object} title 413 * @returns commands 414 */ 415 async function attachTestTab(client, title) { 416 const descriptorFront = await getTestTab(client, title); 417 418 // These xpcshell tests use mocked actors (xpcshell-test/testactors) 419 // which still don't support watcher actor. 420 // Because of that we still can't enable server side targets and target swiching. 421 descriptorFront.disableTargetSwitching(); 422 423 const commands = await createCommandsDictionary(descriptorFront); 424 await commands.targetCommand.startListening(); 425 return commands; 426 } 427 428 /** 429 * Attach to the client's tab whose title is specified, and then attach to 430 * that tab's thread. 431 * 432 * @param {object} client 433 * @param {object} title 434 * @returns {object} 435 * targetFront 436 * threadFront 437 * commands 438 */ 439 async function attachTestThread(client, title) { 440 const commands = await attachTestTab(client, title); 441 const targetFront = commands.targetCommand.targetFront; 442 443 // Pass any configuration, in order to ensure starting all the thread actors 444 // and have them to handle debugger statements. 445 await commands.threadConfigurationCommand.updateConfiguration({ 446 skipBreakpoints: false, 447 // Disable pause overlay as we don't have a true tab and it would throw trying to display them 448 pauseOverlay: false, 449 }); 450 451 const threadFront = await targetFront.getFront("thread"); 452 Assert.equal(threadFront.state, "attached", "Thread front is attached"); 453 return { targetFront, threadFront, commands }; 454 } 455 456 /** 457 * Initialize the testing devtools server. 458 */ 459 function initTestDevToolsServer(server = DevToolsServer) { 460 if (server === WorkerDevToolsServer) { 461 const { createRootActor } = worker.require("xpcshell-test/testactors"); 462 server.setRootActor(createRootActor); 463 } else { 464 const { createRootActor } = require("xpcshell-test/testactors"); 465 server.setRootActor(createRootActor); 466 } 467 468 // Allow incoming connections. 469 server.init(function () { 470 return true; 471 }); 472 } 473 474 /** 475 * Initialize the testing devtools server with a tab whose title is |title|. 476 */ 477 async function startTestDevToolsServer(title, server = DevToolsServer) { 478 initTestDevToolsServer(server); 479 addTestGlobal(title); 480 DevToolsServer.registerActors({ target: true }); 481 482 const transport = DevToolsServer.connectPipe(); 483 const client = new DevToolsClient(transport); 484 485 await connect(client); 486 return client; 487 } 488 489 async function finishClient(client) { 490 await client.close(); 491 DevToolsServer.destroy(); 492 do_test_finished(); 493 } 494 495 /** 496 * Takes a relative file path and returns the absolute file url for it. 497 */ 498 function getFileUrl(name, allowMissing = false) { 499 const file = do_get_file(name, allowMissing); 500 return Services.io.newFileURI(file).spec; 501 } 502 503 /** 504 * Returns the full path of the file with the specified name in a 505 * platform-independent and URL-like form. 506 */ 507 function getFilePath( 508 name, 509 allowMissing = false, 510 usePlatformPathSeparator = false 511 ) { 512 const file = do_get_file(name, allowMissing); 513 let path = Services.io.newFileURI(file).spec; 514 let filePrePath = "file://"; 515 if ("nsILocalFileWin" in Ci && file instanceof Ci.nsILocalFileWin) { 516 filePrePath += "/"; 517 } 518 519 path = path.slice(filePrePath.length); 520 521 if (usePlatformPathSeparator && path.match(/^\w:/)) { 522 path = path.replace(/\//g, "\\"); 523 } 524 525 return path; 526 } 527 528 /** 529 * Returns the full text contents of the given file. 530 */ 531 function readFile(fileName) { 532 const f = do_get_file(fileName); 533 const s = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( 534 Ci.nsIFileInputStream 535 ); 536 s.init(f, -1, -1, false); 537 try { 538 return NetUtil.readInputStreamToString(s, s.available()); 539 } finally { 540 s.close(); 541 } 542 } 543 544 function writeFile(fileName, content) { 545 const file = do_get_file(fileName, true); 546 const stream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( 547 Ci.nsIFileOutputStream 548 ); 549 stream.init(file, -1, -1, 0); 550 try { 551 do { 552 const numWritten = stream.write(content, content.length); 553 content = content.slice(numWritten); 554 } while (content.length); 555 } finally { 556 stream.close(); 557 } 558 } 559 560 function StubTransport() {} 561 StubTransport.prototype.ready = function () {}; 562 StubTransport.prototype.send = function () {}; 563 StubTransport.prototype.close = function () {}; 564 565 // Create async version of the object where calling each method 566 // is equivalent of calling it with asyncall. Mainly useful for 567 // destructuring objects with methods that take callbacks. 568 const Async = target => new Proxy(target, Async); 569 Async.get = (target, name) => 570 typeof target[name] === "function" 571 ? asyncall.bind(null, target[name], target) 572 : target[name]; 573 574 // Calls async function that takes callback and errorback and returns 575 // returns promise representing result. 576 const asyncall = (fn, self, ...args) => 577 new Promise((...etc) => fn.call(self, ...args, ...etc)); 578 579 const Test = task => () => { 580 add_task(task); 581 run_next_test(); 582 }; 583 584 const assert = Assert.ok.bind(Assert); 585 586 /** 587 * Create a promise that is resolved on the next occurence of the given event. 588 * 589 * @param ThreadFront threadFront 590 * @param String event 591 * @param Function predicate 592 * @returns Promise 593 */ 594 function waitForEvent(front, type, predicate) { 595 if (!predicate) { 596 return front.once(type); 597 } 598 599 return new Promise(function (resolve) { 600 function listener(packet) { 601 if (!predicate(packet)) { 602 return; 603 } 604 front.off(type, listener); 605 resolve(packet); 606 } 607 front.on(type, listener); 608 }); 609 } 610 611 /** 612 * Execute the action on the next tick and return a promise that is resolved on 613 * the next pause. 614 * 615 * When using promises and Task.jsm, we often want to do an action that causes a 616 * pause and continue the task once the pause has ocurred. Unfortunately, if we 617 * do the action that causes the pause within the task's current tick we will 618 * pause before we have a chance to yield the promise that waits for the pause 619 * and we enter a dead lock. The solution is to create the promise that waits 620 * for the pause, schedule the action to run on the next tick of the event loop, 621 * and finally yield the promise. 622 * 623 * @param Function action 624 * @param ThreadFront threadFront 625 * @returns Promise 626 */ 627 function executeOnNextTickAndWaitForPause(action, threadFront) { 628 const paused = waitForPause(threadFront); 629 executeSoon(action); 630 return paused; 631 } 632 633 function evalCallback(debuggeeGlobal, func) { 634 Cu.evalInSandbox("(" + func + ")()", debuggeeGlobal, "1.8", "test.js", 1); 635 } 636 637 /** 638 * Interrupt JS execution for the specified thread. 639 * 640 * @param ThreadFront threadFront 641 * @returns Promise 642 */ 643 function interrupt(threadFront) { 644 dumpn("Interrupting."); 645 return threadFront.interrupt(); 646 } 647 648 /** 649 * Resume JS execution for the specified thread and then wait for the next pause 650 * event. 651 * 652 * @param DevToolsClient client 653 * @param ThreadFront threadFront 654 * @returns Promise 655 */ 656 async function resumeAndWaitForPause(threadFront) { 657 const paused = waitForPause(threadFront); 658 await resume(threadFront); 659 return paused; 660 } 661 662 /** 663 * Resume JS execution for a single step and wait for the pause after the step 664 * has been taken. 665 * 666 * @param ThreadFront threadFront 667 * @returns Promise 668 */ 669 function stepIn(threadFront) { 670 dumpn("Stepping in."); 671 const paused = waitForPause(threadFront); 672 return threadFront.stepIn().then(() => paused); 673 } 674 675 /** 676 * Resume JS execution for a step over and wait for the pause after the step 677 * has been taken. 678 * 679 * @param ThreadFront threadFront 680 * @returns Promise 681 */ 682 async function stepOver(threadFront, frameActor) { 683 dumpn("Stepping over."); 684 await threadFront.stepOver(frameActor); 685 return waitForPause(threadFront); 686 } 687 688 /** 689 * Resume JS execution for a step out and wait for the pause after the step 690 * has been taken. 691 * 692 * @param DevToolsClient client 693 * @param ThreadFront threadFront 694 * @returns Promise 695 */ 696 async function stepOut(threadFront, frameActor) { 697 dumpn("Stepping out."); 698 await threadFront.stepOut(frameActor); 699 return waitForPause(threadFront); 700 } 701 702 /** 703 * Restart specific frame and wait for the pause after the restart 704 * has been taken. 705 * 706 * @param DevToolsClient client 707 * @param ThreadFront threadFront 708 * @returns Promise 709 */ 710 async function restartFrame(threadFront, frameActor) { 711 dumpn("Restarting frame."); 712 await threadFront.restart(frameActor); 713 return waitForPause(threadFront); 714 } 715 716 /** 717 * Get the list of `count` frames currently on stack, starting at the index 718 * `first` for the specified thread. 719 * 720 * @param ThreadFront threadFront 721 * @param Number first 722 * @param Number count 723 * @returns Promise 724 */ 725 function getFrames(threadFront, first, count) { 726 dumpn("Getting frames."); 727 return threadFront.getFrames(first, count); 728 } 729 730 /** 731 * Black box the specified source. 732 * 733 * @param SourceFront sourceFront 734 * @returns Promise 735 */ 736 async function blackBox(sourceFront, range = null) { 737 dumpn("Black boxing source: " + sourceFront.actor); 738 const pausedInSource = await sourceFront.blackBox(range); 739 ok(true, "blackBox didn't throw"); 740 return pausedInSource; 741 } 742 743 /** 744 * Stop black boxing the specified source. 745 * 746 * @param SourceFront sourceFront 747 * @returns Promise 748 */ 749 async function unBlackBox(sourceFront, range = null) { 750 dumpn("Un-black boxing source: " + sourceFront.actor); 751 await sourceFront.unblackBox(range); 752 ok(true, "unblackBox didn't throw"); 753 } 754 755 /** 756 * Get a source at the specified url. 757 * 758 * @param ThreadFront threadFront 759 * @param string url 760 * @returns Promise<SourceFront> 761 */ 762 async function getSource(threadFront, url) { 763 const source = await getSourceForm(threadFront, url); 764 if (source) { 765 return threadFront.source(source); 766 } 767 768 throw new Error("source not found"); 769 } 770 771 async function getSourceById(threadFront, id) { 772 const form = await getSourceFormById(threadFront, id); 773 return threadFront.source(form); 774 } 775 776 async function getSourceForm(threadFront, url) { 777 const { sources } = await threadFront.getSources(); 778 return sources.find(s => s.url === url); 779 } 780 781 async function getSourceFormById(threadFront, id) { 782 const { sources } = await threadFront.getSources(); 783 return sources.find(source => source.actor == id); 784 } 785 786 async function checkFramesLength(threadFront, expectedFrames) { 787 const frameResponse = await threadFront.getFrames(0, null); 788 Assert.equal( 789 frameResponse.frames.length, 790 expectedFrames, 791 "Thread front has the expected number of frames" 792 ); 793 } 794 795 /** 796 * Do a reload which clears the thread debugger 797 * 798 * @param TabFront tabFront 799 * @returns Promise<response> 800 */ 801 function reload(tabFront) { 802 return tabFront.reload({}); 803 } 804 805 /** 806 * Returns an array of stack location strings given a thread and a sample. 807 * 808 * @param object thread 809 * @param object sample 810 * @returns object 811 */ 812 function getInflatedStackLocations(thread, sample) { 813 const stackTable = thread.stackTable; 814 const frameTable = thread.frameTable; 815 const stringTable = thread.stringTable; 816 const SAMPLE_STACK_SLOT = thread.samples.schema.stack; 817 const STACK_PREFIX_SLOT = stackTable.schema.prefix; 818 const STACK_FRAME_SLOT = stackTable.schema.frame; 819 const FRAME_LOCATION_SLOT = frameTable.schema.location; 820 821 // Build the stack from the raw data and accumulate the locations in 822 // an array. 823 let stackIndex = sample[SAMPLE_STACK_SLOT]; 824 const locations = []; 825 while (stackIndex !== null) { 826 const stackEntry = stackTable.data[stackIndex]; 827 const frame = frameTable.data[stackEntry[STACK_FRAME_SLOT]]; 828 locations.push(stringTable[frame[FRAME_LOCATION_SLOT]]); 829 stackIndex = stackEntry[STACK_PREFIX_SLOT]; 830 } 831 832 // The profiler tree is inverted, so reverse the array. 833 return locations.reverse(); 834 } 835 836 async function setupTestFromUrl(url) { 837 do_test_pending(); 838 839 const { createRootActor } = require("xpcshell-test/testactors"); 840 DevToolsServer.setRootActor(createRootActor); 841 DevToolsServer.init(() => true); 842 843 const global = createTestGlobal("test"); 844 DevToolsServer.addTestGlobal(global); 845 846 const devToolsClient = new DevToolsClient(DevToolsServer.connectPipe()); 847 await connect(devToolsClient); 848 849 const tabs = await listTabs(devToolsClient); 850 const descriptorFront = findTab(tabs, "test"); 851 852 // These xpcshell tests use mocked actors (xpcshell-test/testactors) 853 // which still don't support watcher actor. 854 // Because of that we still can't enable server side targets and target swiching. 855 descriptorFront.disableTargetSwitching(); 856 857 const targetFront = await descriptorFront.getTarget(); 858 859 const commands = await createCommandsDictionary(descriptorFront); 860 861 // Pass any configuration, in order to ensure starting all the thread actor 862 // and have it to notify about all sources 863 await commands.threadConfigurationCommand.updateConfiguration({ 864 skipBreakpoints: false, 865 // Disable pause overlay as we don't have a true tab and it would throw trying to display them 866 pauseOverlay: false, 867 }); 868 869 const threadFront = await targetFront.getFront("thread"); 870 871 const sourceUrl = getFileUrl(url); 872 const promise = waitForNewSource(threadFront, sourceUrl); 873 loadSubScript(sourceUrl, global); 874 const { source } = await promise; 875 876 const sourceFront = threadFront.source(source); 877 return { global, devToolsClient, threadFront, sourceFront }; 878 } 879 880 /** 881 * Run the given test function twice, one with a regular DevToolsServer, 882 * testing against a fake tab. And another one against a WorkerDevToolsServer, 883 * testing the worker codepath. 884 * 885 * @param Function test 886 * Test function to run twice. 887 * This test function is called with a dictionary: 888 * - Sandbox debuggee 889 * The custom JS debuggee created for this test. This is a Sandbox using system 890 * principals by default. 891 * - ThreadFront threadFront 892 * A reference to a ThreadFront instance that is attached to the debuggee. 893 * - DevToolsClient client 894 * A reference to the DevToolsClient used to communicated with the RDP server. 895 * @param Object options 896 * Optional arguments to tweak test environment 897 * - JSPrincipal principal 898 * Principal to use for the debuggee. Defaults to systemPrincipal. 899 * - boolean doNotRunWorker 900 * If true, do not run this tests in worker debugger context. Defaults to false. 901 * - bool wantXrays 902 * Whether the debuggee wants Xray vision with respect to same-origin objects 903 * outside the sandbox. Defaults to true. 904 * - bool waitForFinish 905 * Whether to wait for a call to threadFrontTestFinished after the test 906 * function finishes. 907 */ 908 function threadFrontTest(test, options = {}) { 909 const { 910 principal = "http://example.com", 911 doNotRunWorker = false, 912 wantXrays = true, 913 waitForFinish = false, 914 } = options; 915 916 async function runThreadFrontTestWithServer(server, test) { 917 // Setup a server and connect a client to it. 918 initTestDevToolsServer(server); 919 920 // Create a custom debuggee and register it to the server. 921 // We are using a custom Sandbox as debuggee. Create a new zone because 922 // debugger and debuggee must be in different compartments. 923 const debuggee = Cu.Sandbox(principal, { freshZone: true, wantXrays }); 924 const scriptName = "debuggee.js"; 925 debuggee.document = { title: scriptName }; // Reproduce a window object, as createTestGlobal returns a real window object 926 server.addTestGlobal(debuggee); 927 928 const client = new DevToolsClient(server.connectPipe()); 929 await client.connect(); 930 931 // Attach to the fake tab target and retrieve the ThreadFront instance. 932 // Automatically resume as the thread is paused by default after attach. 933 const { targetFront, threadFront, commands } = await attachTestThread( 934 client, 935 scriptName 936 ); 937 938 // Cross the client/server boundary to retrieve the target actor & thread 939 // actor instances, used by some tests. 940 const rootActor = client.transport._serverConnection.rootActor; 941 const targetActor = 942 rootActor._parameters.tabList.getTargetActorForTab(scriptName); 943 const { threadActor } = targetActor; 944 945 // Run the test function 946 const args = { 947 threadActor, 948 threadFront, 949 debuggee, 950 client, 951 server, 952 targetFront, 953 commands, 954 isWorkerServer: server === WorkerDevToolsServer, 955 }; 956 if (waitForFinish) { 957 // Use dispatchToMainThread so that the test function does not have to 958 // finish executing before the test itself finishes. 959 const promise = new Promise( 960 resolve => (threadFrontTestFinished = resolve) 961 ); 962 Services.tm.dispatchToMainThread(() => test(args)); 963 await promise; 964 } else { 965 await test(args); 966 } 967 968 // Cleanup the client after the test ran 969 await client.close(); 970 971 server.removeTestGlobal(debuggee); 972 973 // Also cleanup the created server 974 server.destroy(); 975 } 976 977 return async () => { 978 dump(">>> Run thread front test against a regular DevToolsServer\n"); 979 await runThreadFrontTestWithServer(DevToolsServer, test); 980 981 // Skip tests that fail in the worker context 982 if (!doNotRunWorker) { 983 dump(">>> Run thread front test against a worker DevToolsServer\n"); 984 await runThreadFrontTestWithServer(WorkerDevToolsServer, test); 985 } 986 }; 987 } 988 989 // This callback is used in tandem with the waitForFinish option of 990 // threadFrontTest to support thread front tests that use promises to 991 // asynchronously finish the tests, instead of using async/await. 992 // Newly written tests should avoid using this. See bug 1596114 for migrating 993 // existing tests to async/await and removing this functionality. 994 let threadFrontTestFinished;