From a26f636c9b422f866b0dbe42566cd90c256483d1 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 11 Nov 2022 12:37:37 +0100 Subject: [PATCH] ci: Automated plugin test with puppeteer --- .github/workflows/reportBrokenPlugins.yml | 37 ++++ src/plugins/settings.tsx | 2 - src/webpack/patchWebpack.ts | 14 +- test/generateReport.ts | 195 ++++++++++++++++++++++ 4 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/reportBrokenPlugins.yml create mode 100644 test/generateReport.ts diff --git a/.github/workflows/reportBrokenPlugins.yml b/.github/workflows/reportBrokenPlugins.yml new file mode 100644 index 00000000..9f694a4e --- /dev/null +++ b/.github/workflows/reportBrokenPlugins.yml @@ -0,0 +1,37 @@ +name: Test Patches +on: + workflow_dispatch: + schedule: + # Every day at midnight + - cron: 0 0 * * * + +jobs: + TestPlugins: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json + + - name: Use Node.js 19 + uses: actions/setup-node@v3 + with: + node-version: 19 + cache: "pnpm" + + - name: Install dependencies + run: | + pnpm install --frozen-lockfile + pnpm add puppeteer + + - name: Build web + run: pnpm buildWeb --standalone + + - name: Create Report + run: | + export PATH="$PWD/node_modules/.bin:$PATH" + esbuild test/generateReport.ts > dist/report.mjs + node dist/report.mjs >> $GITHUB_STEP_SUMMARY + env: + DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} diff --git a/src/plugins/settings.tsx b/src/plugins/settings.tsx index d80b0ffa..b3b49b63 100644 --- a/src/plugins/settings.tsx +++ b/src/plugins/settings.tsx @@ -16,8 +16,6 @@ * along with this program. If not, see . */ -import React from "react"; - import gitHash from "~git-hash"; import { Devs } from "../utils/constants"; diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index 679d481b..f14ab0ba 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -65,7 +65,7 @@ function patchPush() { const originalMod = mod; const patchedBy = new Set(); - modules[id] = function (module, exports, require) { + const factory = modules[id] = function (module, exports, require) { try { mod(module, exports, require); } catch (err) { @@ -118,10 +118,14 @@ function patchPush() { logger.error("Error while firing callback for webpack chunk", err); } } - }; + } as any as { toString: () => string, original: any, (...args: any[]): void; }; - modules[id].toString = () => mod.toString(); - modules[id].original = originalMod; + // for some reason throws some error on which calling .toString() leads to infinite recursion + // when you force load all chunks??? + try { + factory.toString = () => mod.toString(); + factory.original = originalMod; + } catch { } for (let i = 0; i < patches.length; i++) { const patch = patches[i]; @@ -147,7 +151,7 @@ function patchPush() { mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`); } } catch (err) { - logger.error(`Failed to apply patch ${replacement.match} of ${patch.plugin} to ${id}:\n`, err); + logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err); if (IS_DEV) { const changeSize = code.length - lastCode.length; diff --git a/test/generateReport.ts b/test/generateReport.ts new file mode 100644 index 00000000..928808a3 --- /dev/null +++ b/test/generateReport.ts @@ -0,0 +1,195 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +// eslint-disable-next-line spaced-comment +/// +// eslint-disable-next-line spaced-comment +/// + +import { readFileSync } from "fs"; +// puppeteer is not added as dependency because it downloads chromium (~100mb) +// which is not needed for normal development and manually installed by github actions +// Thus, if you want to run this locally, run `pnpm i puppeteer` first +import pup, { JSHandle } from "puppeteer"; + +const browser = await pup.launch({ + headless: true, + args: [ + "--no-sandbox", + "--disable-setuid-sandbox", + '--user-agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36"', + ] +}); + +const page = await browser.newPage(); + +function maybeGetError(handle: JSHandle) { + return (handle as JSHandle)?.getProperty("message") + .then(m => m.jsonValue()); +} + +const report = { + badPatches: [] as { + plugin: string; + type: string; + id: string; + match: string; + error?: string; + }[], + badStarts: [] as { + plugin: string; + error: string; + }[], + otherErrors: [] as string[] +}; + +function toCodeBlock(s: string) { + s = s.replace(/```/g, "`\u200B`\u200B`"); + return "```" + s + " ```"; +} + +function printReport() { + console.log("# Vencord Report"); + console.log(); + + console.log("## Bad Patches"); + report.badPatches.forEach(p => { + console.log(`- ${p.plugin} (${p.type})`); + console.log(` - ID: \`${p.id}\``); + console.log(` - Match: ${toCodeBlock(p.match)}`); + if (p.error) console.log(` - Error: ${toCodeBlock(p.error)}`); + }); + + console.log(); + + console.log("## Bad Starts"); + report.badStarts.forEach(p => { + console.log(`- ${p.plugin}`); + console.log(` - Error: ${toCodeBlock(p.error)}`); + }); +} + +page.on("console", async e => { + const level = e.type(); + const args = e.args(); + + const firstArg = (await args[0]?.jsonValue()); + if (firstArg === "PUPPETEER_TEST_DONE_SIGNAL") { + await browser.close(); + printReport(); + process.exit(); + } + + const isVencord = (await args[0]?.jsonValue()) === "[Vencord]"; + if (isVencord) { + // make ci fail + process.exitCode = 1; + + const jsonArgs = await Promise.all(args.map(a => a.jsonValue())); + const [, tag, message] = jsonArgs; + const cause = await maybeGetError(args[3]); + + switch (tag) { + case "WebpackInterceptor:": + const [, plugin, type, id, regex] = (message as string).match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!; + report.badPatches.push({ + plugin, + type, + id, + match: regex, + error: cause + }); + break; + case "PluginManager:": + const [, name] = (message as string).match(/Failed to start (.+)/)!; + report.badStarts.push({ + plugin: name, + error: cause + }); + break; + } + } else if (level === "error") { + report.otherErrors.push(e.text()); + } +}); + +page.on("error", e => console.error("[Error]", e)); +page.on("pageerror", e => console.error("[Page Error]", e)); + +await page.setBypassCSP(true); + +function runTime(token: string) { + // spoof languages to not be suspicious + Object.defineProperty(navigator, "languages", { + get: function () { + return ["en-US", "en"]; + }, + }); + + + // Monkey patch Logger to not log with custom css + Vencord.Util.Logger.prototype._log = function (level, levelColor, args) { + if (level === "warn" || level === "error") + console[level]("[Vencord]", this.name + ":", ...args); + }; + + // force enable all plugins and patches + Vencord.Plugins.patches.length = 0; + Object.values(Vencord.Plugins.plugins).forEach(p => { + p.required = true; + p.patches?.forEach(patch => { + patch.plugin = p.name; + delete patch.predicate; + if (!Array.isArray(patch.replacement)) + patch.replacement = [patch.replacement]; + Vencord.Plugins.patches.push(patch); + }); + }); + + Vencord.Webpack.waitFor( + "loginToken", + m => m.loginToken(token) + ); + + // force load all chunks + Vencord.Webpack.onceReady.then(() => setTimeout(async () => { + const { wreq } = Vencord.Webpack; + + const ids = Function("return" + wreq.u.toString().match(/\{.+\}/s)![0])(); + for (const id in ids) { + const isWasm = await fetch(wreq.p + wreq.u(id)) + .then(r => r.text()) + .then(t => t.includes(".module.wasm")); + + if (!isWasm) + await wreq.e(id as any); + } + for (const patch of Vencord.Plugins.patches) { + new Vencord.Util.Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`); + } + setTimeout(() => console.log("PUPPETEER_TEST_DONE_SIGNAL"), 1000); + }, 1000)); +} + +await page.evaluateOnNewDocument(` + ${readFileSync("./dist/browser.js", "utf-8")} + + ;(${runTime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)}); +`); + +await page.goto("https://discord.com/login");