SpecialPowersProcessActor.sys.mjs (6069B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 SpecialPowersSandbox: 9 "resource://testing-common/SpecialPowersSandbox.sys.mjs", 10 }); 11 12 let nextSpfpId = 1; 13 14 /** 15 * SpecialPowersForProcess wraps a content process, and allows the caller to 16 * spawn() tasks like SpecialPowers.spawn() and contentPage.spawn(), including 17 * Assert functionality. Assertion messages are passed back to the test scope, 18 * which must be passed along with the process to the constructor. 19 */ 20 export class SpecialPowersForProcess { 21 static instances = new Map(); 22 23 /** 24 * Create a new SpecialPowersForProcess that enables callers to spawn tasks 25 * in the given content process. 26 * 27 * @param {any} scope 28 * The test scope to receive assertion messages. 29 * In test files this is often equivalent to globalThis. 30 * @param {nsIDOMProcessParent} domProcess 31 * The content process where the spawned code should run. 32 */ 33 constructor(testScope, domProcess) { 34 this.isXpcshellScope = !!testScope.do_get_profile; 35 this.isSimpleTestScope = !this.isXpcshellScope && !!testScope.SimpleTest; 36 if (!this.isXpcshellScope && !this.isSimpleTestScope) { 37 // Must be global of xpcshell test, or global of browser chrome mochitest. 38 throw new Error("testScope cannot receive assertion messages!"); 39 } 40 41 if (!(domProcess instanceof Ci.nsIDOMProcessParent)) { 42 throw new Error("domProcess is not a nsIDOMProcessParent!"); 43 } 44 45 // This actor is registered via SpecialPowersParent.registerActor(). 46 // In mochitests that is part of the SpecialPowers add-on initialization. 47 // Xpcshell tests can initialize it with XPCShellContentUtils.init(), via 48 // XPCShellContentUtils.ensureInitialized(). 49 this.actor = domProcess.getActor("SpecialPowersProcessActor"); 50 51 this.testScope = testScope; 52 53 this.spfpId = nextSpfpId++; 54 SpecialPowersForProcess.instances.set(this.spfpId, this); 55 56 testScope.registerCleanupFunction(() => this.destroy()); 57 } 58 59 destroy() { 60 if (!this.testScope) { 61 // Already destroyed. 62 return; 63 } 64 SpecialPowersForProcess.instances.delete(this.spfpId); 65 this.actor = null; 66 this.testScope = null; 67 } 68 69 /** 70 * Like `SpecialPowers.spawn`, but spawns a task in a child process instead. 71 * The task has access to Assert. 72 * 73 * @param {Array<any>} args 74 * An array of arguments to pass to the task. All arguments 75 * must be structured clone compatible, and will be cloned 76 * before being passed to the task. 77 * @param {function} task 78 * The function to run in the context of the target process. The 79 * function will be stringified and re-evaluated in the context 80 * of the target's content process. It may return any structured 81 * clone compatible value, or a Promise which resolves to the 82 * same, which will be returned to the caller. 83 * @returns {Promise<any>} 84 * A promise which resolves to the return value of the task, or 85 * which rejects if the task raises an exception. As this is 86 * being written, the rejection value will always be undefined 87 * in the cases where the task throws an error, though that may 88 * change in the future. 89 */ 90 spawn(args, task) { 91 return this.actor.sendQuery("Spawn", { 92 args, 93 task: String(task), 94 caller: Cu.getFunctionSourceLocation(task), 95 spfpId: this.spfpId, 96 }); 97 } 98 99 // Called when ProxiedAssert is received; this data is sent from 100 // SpecialPowersSandbox's reportCallback in response to assertion messages. 101 reportCallback(data) { 102 if ("info" in data) { 103 if (this.isXpcshellScope) { 104 this.testScope.info(data.info); 105 } else if (this.isSimpleTestScope) { 106 this.testScope.SimpleTest.info(data.info); 107 } else { 108 // We checked this in the constructor, so this is unexpected. 109 throw new Error(`testScope cannot receive assertion messages!?!`); 110 } 111 return; 112 } 113 114 const { name, diag, passed, stack, expectFail } = data; 115 if (this.isXpcshellScope) { 116 this.testScope.do_report_result(passed, name, stack); 117 } else if (this.isSimpleTestScope) { 118 // browser chrome mochitest 119 let expected = expectFail ? "fail" : "pass"; 120 this.testScope.SimpleTest.record(passed, name, diag, stack, expected); 121 } else { 122 // We checked this in the constructor, so this is unexpected. 123 throw new Error(`testScope cannot receive assertion messages!?!`); 124 } 125 } 126 } 127 128 // A minimal process actor that allows spawn() to run in the given process. 129 export class SpecialPowersProcessActorParent extends JSProcessActorParent { 130 receiveMessage(aMessage) { 131 switch (aMessage.name) { 132 case "ProxiedAssert": { 133 const { spfpId, data } = aMessage.data; 134 const spfp = SpecialPowersForProcess.instances.get(spfpId); 135 if (!spfp) { 136 dump(`Unexpected ProxiedAssert: ${uneval(data)}\n`); 137 throw new Error(`Unexpected message for ${spfpId} `); 138 } 139 spfp.reportCallback(data); 140 return undefined; 141 } 142 default: 143 throw new Error( 144 `Unknown SpecialPowersProcessActorParent action: ${aMessage.name}` 145 ); 146 } 147 } 148 } 149 150 export class SpecialPowersProcessActorChild extends JSProcessActorChild { 151 receiveMessage(aMessage) { 152 switch (aMessage.name) { 153 case "Spawn": { 154 return this._spawnTaskInChild(aMessage.data); 155 } 156 default: 157 throw new Error( 158 `Unknown SpecialPowersProcessActorChild action: ${aMessage.name}` 159 ); 160 } 161 } 162 163 _spawnTaskInChild({ task, args, caller, spfpId }) { 164 let sb = new lazy.SpecialPowersSandbox(null, data => { 165 this.sendAsyncMessage("ProxiedAssert", { spfpId, data }); 166 }); 167 168 return sb.execute(task, args, caller); 169 } 170 }