test/bd-patcher.test.js (4822 bytes)
1 "use strict"; 2 const test = require("node:test"); 3 const assert = require("node:assert/strict"); 4 const {loadBdPatcher} = require("./helpers/installed.js"); 5 const {Recorder, wrapPatchChild, unwrapPatchChild} = require("../00PluginProfiler.plugin.js").internals; 6 7 function setup() { 8 const errors = []; 9 const Patcher = loadBdPatcher({err: (...args) => errors.push(args)}); 10 const clock = {t: 0}; 11 const recorder = new Recorder(() => clock.t); 12 const msByOwner = () => Object.fromEntries([...recorder.owners].map(([name, entry]) => [name, entry.ms])); 13 const wrapAll = callers => { 14 const children = callers.flatMap(caller => Patcher.getPatchesByCaller(caller)); 15 for (const child of children) assert.equal(wrapPatchChild(recorder, child, child.caller), true); 16 return children; 17 }; 18 return {Patcher, errors, clock, recorder, msByOwner, wrapAll}; 19 } 20 21 test("patch time goes to the plugin, the original function time does not, and results stay the same", () => { 22 const {Patcher, errors, clock, recorder, msByOwner, wrapAll} = setup(); 23 const target = {work(x) { clock.t += 100; return x * 2; }}; 24 Patcher.before("A", target, "work", () => { clock.t += 5; }); 25 Patcher.instead("B", target, "work", (self, args, next) => { clock.t += 7; return next(...args) + 1; }); 26 Patcher.after("C", target, "work", (self, args, value) => { clock.t += 3; return value + 10; }); 27 const expected = target.work(4); 28 assert.equal(expected, 19); 29 30 const children = wrapAll(["A", "B", "C"]); 31 assert.equal(target.work(4), expected); 32 assert.deepEqual(msByOwner(), {A: 5, B: 7, C: 3}); 33 assert.equal(recorder.unattributedMs, 100); 34 35 // A second scan must not wrap a wrapper. 36 for (const child of children) assert.equal(wrapPatchChild(recorder, child, child.caller), false); 37 for (const child of children) unwrapPatchChild(child); 38 assert.equal(target.work(4), expected); 39 assert.deepEqual(errors, []); 40 }); 41 42 test("unwrap puts back the exact callbacks", () => { 43 const {Patcher, wrapAll} = setup(); 44 const target = {work() { return 1; }}; 45 const before = () => {}; 46 const after = (self, args, value) => value; 47 Patcher.before("A", target, "work", before); 48 Patcher.after("A", target, "work", after); 49 const children = wrapAll(["A"]); 50 assert.notEqual(children[0].callback, before); 51 for (const child of children) unwrapPatchChild(child); 52 assert.deepEqual(children.map(child => child.callback), [before, after]); 53 }); 54 55 test("chained instead patches each get only their own time", () => { 56 const {Patcher, clock, recorder, msByOwner, wrapAll} = setup(); 57 const target = {work(x) { clock.t += 40; return x; }}; 58 Patcher.instead("B1", target, "work", (self, args, next) => { clock.t += 2; const value = next(...args); clock.t += 1; return value + 1; }); 59 Patcher.instead("B2", target, "work", (self, args, next) => { clock.t += 4; return next(...args) * 10; }); 60 const expected = target.work(1); 61 wrapAll(["B1", "B2"]); 62 assert.equal(target.work(1), expected); 63 assert.deepEqual(msByOwner(), {B1: 3, B2: 4}); 64 assert.equal(recorder.unattributedMs, 40); 65 }); 66 67 test("a patch that calls a patched function pays for the Discord work but not for the other plugin", () => { 68 const {Patcher, clock, msByOwner, wrapAll} = setup(); 69 const inner = {step() { clock.t += 10; }}; 70 const outer = {work() { clock.t += 1; }}; 71 Patcher.after("C", inner, "step", () => { clock.t += 6; }); 72 Patcher.before("A", outer, "work", () => { clock.t += 2; inner.step(); }); 73 wrapAll(["A", "C"]); 74 outer.work(); 75 // Without plugin A, Discord does not call inner.step here. So A owns those 10 ms. Plugin C owns its 6 ms. 76 assert.deepEqual(msByOwner(), {A: 12, C: 6}); 77 }); 78 79 test("a plugin can unpatch while the profiler wraps its callback", () => { 80 const {Patcher, clock, msByOwner, wrapAll} = setup(); 81 const target = {work() { return "original"; }}; 82 const unpatch = Patcher.after("A", target, "work", () => { clock.t += 1; return "patched"; }); 83 wrapAll(["A"]); 84 assert.equal(target.work(), "patched"); 85 unpatch(); 86 assert.equal(target.work(), "original"); 87 assert.deepEqual(Patcher.getPatchesByCaller("A"), []); 88 assert.deepEqual(msByOwner(), {A: 1}); 89 }); 90 91 test("an error in a wrapped callback reaches the BetterDiscord logger as before", () => { 92 const {Patcher, errors, recorder, wrapAll} = setup(); 93 const target = {work() { return 5; }}; 94 Patcher.before("A", target, "work", () => { throw new Error("plugin bug"); }); 95 wrapAll(["A"]); 96 assert.equal(target.work(), 5); 97 assert.equal(errors.length, 1); 98 assert.equal(errors[0][2].message, "plugin bug"); 99 assert.equal(recorder.stack.length, 0); 100 assert.equal(recorder.owners.get("A").calls, 1); 101 });