stub-generator-helpers.js (12563B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const { 7 getAdHocFrontOrPrimitiveGrip, 8 } = require("devtools/client/fronts/object"); 9 10 const CHROME_PREFIX = "chrome://mochitests/content/browser/"; 11 const STUBS_FOLDER = "devtools/client/webconsole/test/node/fixtures/stubs/"; 12 const STUBS_UPDATE_ENV = "WEBCONSOLE_STUBS_UPDATE"; 13 14 async function createCommandsForTab(tab) { 15 const { 16 CommandsFactory, 17 } = require("devtools/shared/commands/commands-factory"); 18 const commands = await CommandsFactory.forTab(tab); 19 return commands; 20 } 21 22 async function createCommandsForMainProcess() { 23 const { 24 CommandsFactory, 25 } = require("devtools/shared/commands/commands-factory"); 26 const commands = await CommandsFactory.forMainProcess(); 27 return commands; 28 } 29 30 // eslint-disable-next-line complexity 31 function getCleanedPacket(key, packet) { 32 const { stubPackets } = require(CHROME_PREFIX + STUBS_FOLDER + "index"); 33 34 // Strip escaped characters. 35 const safeKey = key 36 .replace(/\\n/g, "\n") 37 .replace(/\\r/g, "\r") 38 .replace(/\\\"/g, `\"`) 39 .replace(/\\\'/g, `\'`); 40 41 cleanTimeStamp(packet); 42 // Remove the targetFront property that has a cyclical reference and that we don't need 43 // in our node tests. 44 delete packet.targetFront; 45 46 if (!stubPackets.has(safeKey)) { 47 return packet; 48 } 49 50 // If the stub already exist, we want to ignore irrelevant properties (generated id, timer, …) 51 // that might changed and "pollute" the diff resulting from this stub generation. 52 const existingPacket = stubPackets.get(safeKey); 53 const res = Object.assign({}, packet, { 54 from: existingPacket.from, 55 }); 56 57 if (res.innerWindowID) { 58 res.innerWindowID = existingPacket.innerWindowID; 59 } 60 61 if (res.startedDateTime) { 62 res.startedDateTime = existingPacket.startedDateTime; 63 } 64 65 if (res.channelId) { 66 res.channelId = existingPacket.channelId; 67 } 68 69 if (res.resultID) { 70 res.resultID = existingPacket.resultID; 71 } 72 73 if (res.timer) { 74 // Clean timer properties on the message. 75 // Those properties are found on console.time, timeLog and timeEnd calls, 76 // and those time can vary, which is why we need to clean them. 77 if ("duration" in res.timer) { 78 res.timer.duration = existingPacket.timer.duration; 79 } 80 } 81 82 // Clean innerWindowId on the message prop. 83 if (existingPacket.innerWindowID) { 84 res.innerWindowID = existingPacket.innerWindowID; 85 } 86 87 if (Array.isArray(res.arguments)) { 88 res.arguments = res.arguments.map((argument, i) => { 89 if (!argument || typeof argument !== "object") { 90 return argument; 91 } 92 93 const newArgument = Object.assign({}, argument); 94 const existingArgument = existingPacket.arguments[i]; 95 96 if (existingArgument && newArgument._grip) { 97 // `window`'s properties count can vary from OS to OS, so we 98 // clean the `ownPropertyLength` property from the grip. 99 if (newArgument._grip.class === "Window") { 100 newArgument._grip.ownPropertyLength = 101 existingArgument._grip.ownPropertyLength; 102 } 103 } 104 return newArgument; 105 }); 106 } 107 108 if (res.sourceId) { 109 res.sourceId = existingPacket.sourceId; 110 } 111 112 if (Array.isArray(res.stacktrace)) { 113 res.stacktrace = res.stacktrace.map((frame, i) => { 114 const existingFrame = existingPacket.stacktrace[i]; 115 if (frame && existingFrame && frame.sourceId) { 116 frame.sourceId = existingFrame.sourceId; 117 } 118 return frame; 119 }); 120 } 121 122 if (res.eventActor) { 123 // Clean startedDateTime on network messages. 124 res.eventActor.startedDateTime = existingPacket.startedDateTime; 125 } 126 127 if (res.pageError) { 128 // Clean innerWindowID on pageError messages. 129 res.pageError.innerWindowID = existingPacket.pageError.innerWindowID; 130 131 if (res.pageError.sourceId) { 132 res.pageError.sourceId = existingPacket.pageError.sourceId; 133 } 134 135 if ( 136 Array.isArray(res.pageError.stacktrace) && 137 Array.isArray(existingPacket.pageError.stacktrace) 138 ) { 139 res.pageError.stacktrace = res.pageError.stacktrace.map((frame, i) => { 140 const existingFrame = existingPacket.pageError.stacktrace[i]; 141 if (frame && existingFrame && frame.sourceId) { 142 frame.sourceId = existingFrame.sourceId; 143 } 144 return frame; 145 }); 146 } 147 } 148 149 if (Array.isArray(res.exceptionStack)) { 150 res.exceptionStack = res.exceptionStack.map((frame, i) => { 151 const existingFrame = existingPacket.exceptionStack[i]; 152 // We're replacing sourceId here even if the property in frame is null to avoid 153 // a frequent intermittent. The sourceId is retrieved from the Debugger#findSources 154 // API, which is not deterministic (See https://searchfox.org/mozilla-central/rev/b172dd415c475e8b2899560e6005b3a953bead2a/js/src/doc/Debugger/Debugger.md#367-375) 155 // This should be fixed in Bug 1717037. 156 if (frame && existingFrame && "sourceId" in frame) { 157 frame.sourceId = existingFrame.sourceId; 158 } 159 return frame; 160 }); 161 } 162 163 if (res.frame && existingPacket.frame) { 164 res.frame.sourceId = existingPacket.frame.sourceId; 165 } 166 167 if (res.packet) { 168 const override = {}; 169 const keys = ["totalTime", "from", "contentSize", "transferredSize"]; 170 keys.forEach(x => { 171 if (res.packet[x] !== undefined) { 172 override[x] = existingPacket.packet[key]; 173 } 174 }); 175 res.packet = Object.assign({}, res.packet, override); 176 } 177 178 if (res.startedDateTime) { 179 res.startedDateTime = existingPacket.startedDateTime; 180 } 181 182 if (res.totalTime && existingPacket.totalTime) { 183 res.totalTime = existingPacket.totalTime; 184 } 185 186 if (res.securityState && existingPacket.securityState) { 187 res.securityState = existingPacket.securityState; 188 } 189 190 // waitingTime can be very small and rounded to 0. However this is still a 191 // valid waiting time, so check isNaN instead of a simple truthy check. 192 if (!isNaN(res.waitingTime) && existingPacket.waitingTime) { 193 res.waitingTime = existingPacket.waitingTime; 194 } 195 196 return res; 197 } 198 199 // eslint-disable-next-line complexity 200 function cleanTimeStamp(packet) { 201 // We want to have the same timestamp for every stub, so they won't be re-sorted when 202 // adding them to the store. 203 const uniqueTimeStamp = 1572867483805; 204 // lowercased timestamp 205 if (packet.timestamp) { 206 packet.timestamp = uniqueTimeStamp; 207 } 208 209 // camelcased timestamp 210 if (packet.timeStamp) { 211 packet.timeStamp = uniqueTimeStamp; 212 } 213 214 if (packet.startTime) { 215 packet.startTime = uniqueTimeStamp; 216 } 217 218 if (packet?.timeStamp) { 219 packet.timeStamp = uniqueTimeStamp; 220 } 221 222 if (packet?.result?._grip?.preview?.timestamp) { 223 packet.result._grip.preview.timestamp = uniqueTimeStamp; 224 } 225 226 if (packet?.result?._grip?.promiseState?.creationTimestamp) { 227 packet.result._grip.promiseState.creationTimestamp = uniqueTimeStamp; 228 } 229 230 if (packet?.exception?._grip?.preview?.timestamp) { 231 packet.exception._grip.preview.timestamp = uniqueTimeStamp; 232 } 233 234 if (packet?.eventActor?.timeStamp) { 235 packet.eventActor.timeStamp = uniqueTimeStamp; 236 } 237 238 if (packet?.pageError?.timeStamp) { 239 packet.pageError.timeStamp = uniqueTimeStamp; 240 } 241 } 242 243 /** 244 * Write stubs to a given file 245 * 246 * @param {string} fileName: The file to write the stubs in. 247 * @param {Map} packets: A Map of the packets. 248 * @param {boolean} isNetworkMessage: Is the packets are networkMessage packets 249 */ 250 async function writeStubsToFile(fileName, packets, isNetworkMessage) { 251 const mozRepo = Services.env.get("MOZ_DEVELOPER_REPO_DIR"); 252 const filePath = `${mozRepo}/${STUBS_FOLDER + fileName}`; 253 254 const serializedPackets = Array.from(packets.entries()).map( 255 ([key, packet]) => { 256 const stringifiedPacket = getSerializedPacket(packet); 257 return `rawPackets.set(\`${key}\`, ${stringifiedPacket});`; 258 } 259 ); 260 261 const fileContent = `/* Any copyright is dedicated to the Public Domain. 262 http://creativecommons.org/publicdomain/zero/1.0/ */ 263 /* eslint-disable max-len */ 264 265 "use strict"; 266 267 /* 268 * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND. SEE devtools/client/webconsole/test/README.md. 269 */ 270 271 const { 272 parsePacketsWithFronts, 273 } = require("chrome://mochitests/content/browser/devtools/client/webconsole/test/browser/stub-generator-helpers.js"); 274 const { prepareMessage } = require("resource://devtools/client/webconsole/utils/messages.js"); 275 const { 276 ConsoleMessage, 277 NetworkEventMessage, 278 } = require("resource://devtools/client/webconsole/types.js"); 279 280 const rawPackets = new Map(); 281 ${serializedPackets.join("\n\n")} 282 283 284 const stubPackets = parsePacketsWithFronts(rawPackets); 285 286 const stubPreparedMessages = new Map(); 287 for (const [key, packet] of Array.from(stubPackets.entries())) { 288 const transformedPacket = prepareMessage(${"packet"}, { 289 getNextId: () => "1", 290 }); 291 const message = ${ 292 isNetworkMessage 293 ? "NetworkEventMessage(transformedPacket);" 294 : "ConsoleMessage(transformedPacket);" 295 } 296 stubPreparedMessages.set(key, message); 297 } 298 299 module.exports = { 300 rawPackets, 301 stubPreparedMessages, 302 stubPackets, 303 }; 304 `; 305 306 await IOUtils.write(filePath, new TextEncoder().encode(fileContent)); 307 } 308 309 function getStubFile(fileName) { 310 return require(CHROME_PREFIX + STUBS_FOLDER + fileName); 311 } 312 313 function sortObjectKeys(obj) { 314 const isArray = Array.isArray(obj); 315 const isObject = Object.prototype.toString.call(obj) === "[object Object]"; 316 const isFront = obj?._grip; 317 318 if (isObject && !isFront) { 319 // Reorder keys for objects, but skip fronts to avoid infinite recursion. 320 const sortedKeys = Object.keys(obj).sort((k1, k2) => k1.localeCompare(k2)); 321 const withSortedKeys = {}; 322 sortedKeys.forEach(k => { 323 withSortedKeys[k] = k !== "stacktrace" ? sortObjectKeys(obj[k]) : obj[k]; 324 }); 325 return withSortedKeys; 326 } else if (isArray) { 327 return obj.map(item => sortObjectKeys(item)); 328 } 329 return obj; 330 } 331 332 /** 333 * @param {object} packet 334 * The packet to serialize. 335 * @param {object} options 336 * @param {boolean} options.sortKeys 337 * Pass true to sort all keys alphabetically in the packet before serialization. 338 * For instance stub comparison should not fail if the order of properties changed. 339 * @param {boolean} options.replaceActorIds 340 * Pass true to replace actorIDs with a fake one so it's easier to compare stubs 341 * that includes grips. 342 */ 343 function getSerializedPacket( 344 packet, 345 { sortKeys = false, replaceActorIds = false } = {} 346 ) { 347 if (sortKeys) { 348 packet = sortObjectKeys(packet); 349 } 350 351 const actorIdPlaceholder = "XXX"; 352 353 return JSON.stringify( 354 packet, 355 function (key, value) { 356 // The message can have fronts that we need to serialize 357 if (value && value._grip) { 358 return { 359 _grip: value._grip, 360 actorID: replaceActorIds ? actorIdPlaceholder : value.actorID, 361 }; 362 } 363 364 if ( 365 replaceActorIds && 366 (key === "actor" || key === "actorID" || key === "sourceId") && 367 typeof value === "string" 368 ) { 369 return actorIdPlaceholder; 370 } 371 372 if (key === "resourceId") { 373 return undefined; 374 } 375 376 return value; 377 }, 378 2 379 ); 380 } 381 382 /** 383 * 384 * @param {Map} rawPackets 385 */ 386 function parsePacketsWithFronts(rawPackets) { 387 const packets = new Map(); 388 for (const [key, packet] of rawPackets.entries()) { 389 const newPacket = parsePacketAndCreateFronts(packet); 390 packets.set(key, newPacket); 391 } 392 return packets; 393 } 394 395 function parsePacketAndCreateFronts(packet) { 396 if (!packet) { 397 return packet; 398 } 399 if (Array.isArray(packet)) { 400 packet.forEach(parsePacketAndCreateFronts); 401 } 402 if (typeof packet === "object") { 403 for (const [key, value] of Object.entries(packet)) { 404 if (value?._grip) { 405 // The message of an error grip might be a longString. 406 if (value._grip?.preview?.message?._grip) { 407 value._grip.preview.message = value._grip.preview.message._grip; 408 } 409 410 packet[key] = getAdHocFrontOrPrimitiveGrip(value._grip, { 411 conn: { 412 poolFor: () => {}, 413 addActorPool: () => {}, 414 getFrontByID: () => {}, 415 }, 416 manage: () => {}, 417 }); 418 } else { 419 packet[key] = parsePacketAndCreateFronts(value); 420 } 421 } 422 } 423 424 return packet; 425 } 426 427 module.exports = { 428 STUBS_UPDATE_ENV, 429 createCommandsForTab, 430 createCommandsForMainProcess, 431 getStubFile, 432 getCleanedPacket, 433 getSerializedPacket, 434 parsePacketsWithFronts, 435 parsePacketAndCreateFronts, 436 writeStubsToFile, 437 };