From c0927ec09d799f7d47ce13f26bb03cebd78dee21 Mon Sep 17 00:00:00 2001 From: Simon Sawicki Date: Sun, 31 Aug 2025 11:52:53 +0200 Subject: [PATCH] Make tests and solving runtime agnostic --- package.json | 10 ++ src/main.ts | 91 +++++++-------- src/setup.ts | 121 +------------------- src/{solvers_test.ts => solvers.test.ts} | 16 +-- tests/download.ts | 12 +- tests/io.ts | 134 +++++++++++++++++++++++ 6 files changed, 207 insertions(+), 177 deletions(-) create mode 100644 package.json rename src/{solvers_test.ts => solvers.test.ts} (63%) create mode 100644 tests/io.ts diff --git a/package.json b/package.json new file mode 100644 index 0000000..dc4e437 --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "type": "module", + "dependencies": { + "astring": "1.9.0", + "meriyah": "6.1.4" + }, + "devDependencies": { + "rollup": "4.49.0" + } +} diff --git a/src/main.ts b/src/main.ts index dd1eb13..a6b1ef4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,15 +1,13 @@ -import { writeAll } from "@std/io"; +import { read, write } from "./io.ts"; + import { getFromPrepared, preprocessPlayer } from "./solvers.ts"; import { isOneOf } from "./utils.ts"; -async function main(): Promise { - if (Deno.stdin.isTerminal()) { - throw "Expected input on stdin"; - } - const input: Input = await new Response(Deno.stdin.readable).json(); - const preprocessedPlayer = input.type === "player" - ? preprocessPlayer(input.player) - : input.preprocessed_player; +function main(input: Input): Output { + const preprocessedPlayer = + input.type === "player" + ? preprocessPlayer(input.player) + : input.preprocessed_player; const solvers = getFromPrepared(preprocessedPlayer); const responses = input.requests.map( @@ -18,7 +16,7 @@ async function main(): Promise { return { type: "error", request, - error: `Failed to extract ${request.type} function`, + error: `Unknown request type: ${request.type}`, }; } const solver = solvers[request.type]; @@ -39,10 +37,13 @@ async function main(): Promise { return { type: "error", request, - error: `${error}`, + error: + error instanceof Error + ? `${error.message}\n${error.stack}` + : `${error}`, }; } - }, + } ); const output: Output = { @@ -55,33 +56,33 @@ async function main(): Promise { return output; } -async function safeMain(): Promise { +async function safeMain(): Promise { try { - return await main(); + const input = await read(); + const output = main(input); + await write(output); } catch (error) { - return { + await write({ type: "error", error: `${error}`, - }; + }); } } -const output = await safeMain(); -const bytes = new TextEncoder().encode(JSON.stringify(output)); -await writeAll(Deno.stdout, bytes); +safeMain(); -type Input = +export type Input = | { - type: "player"; - player: string; - requests: JsChallengeRequest[]; - output_preprocessed: boolean; - } + type: "player"; + player: string; + requests: JsChallengeRequest[]; + output_preprocessed: boolean; + } | { - type: "preprocessed"; - preprocessed_player: string; - requests: JsChallengeRequest[]; - }; + type: "preprocessed"; + preprocessed_player: string; + requests: JsChallengeRequest[]; + }; type JsChallengeRequest = { type: string; @@ -92,23 +93,23 @@ type JsChallengeRequest = { type JsChallengeProviderResponse = | { - type: "result"; - request: JsChallengeRequest; - response: string; - } + type: "result"; + request: JsChallengeRequest; + response: string; + } | { - type: "error"; - request: JsChallengeRequest; - error: string; - }; + type: "error"; + request: JsChallengeRequest; + error: string; + }; -type Output = +export type Output = | { - type: "result"; - preprocessed_player?: string; - responses: JsChallengeProviderResponse[]; - } + type: "result"; + preprocessed_player?: string; + responses: JsChallengeProviderResponse[]; + } | { - type: "error"; - error: string; - }; + type: "error"; + error: string; + }; diff --git a/src/setup.ts b/src/setup.ts index ae59908..0266bb3 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -3,122 +3,7 @@ import { parse } from "meriyah"; export const setupNodes = parse(` globalThis.XMLHttpRequest = { prototype: {} }; const window = Object.assign(Object.create(null), globalThis); +window.location = new URL("https://www.youtube.com/watch?v=yt-dlp-wins"); const document = {}; -`).body || [ - { - type: "ExpressionStatement", - expression: { - type: "AssignmentExpression", - left: { - type: "MemberExpression", - object: { - type: "Identifier", - name: "globalThis", - }, - computed: false, - property: { - type: "Identifier", - name: "XMLHttpRequest", - }, - optional: false, - }, - operator: "=", - right: { - type: "ObjectExpression", - properties: [ - { - type: "Property", - key: { - type: "Identifier", - name: "prototype", - }, - value: { - type: "ObjectExpression", - properties: [], - }, - kind: "init", - computed: false, - method: false, - shorthand: false, - }, - ], - }, - }, - }, - { - type: "VariableDeclaration", - kind: "const", - declarations: [ - { - type: "VariableDeclarator", - id: { - type: "Identifier", - name: "window", - }, - init: { - type: "CallExpression", - callee: { - type: "MemberExpression", - object: { - type: "Identifier", - name: "Object", - }, - computed: false, - property: { - type: "Identifier", - name: "assign", - }, - optional: false, - }, - arguments: [ - { - type: "CallExpression", - callee: { - type: "MemberExpression", - object: { - type: "Identifier", - name: "Object", - }, - computed: false, - property: { - type: "Identifier", - name: "create", - }, - optional: false, - }, - arguments: [ - { - type: "Literal", - value: null, - }, - ], - optional: false, - }, - { - type: "Identifier", - name: "globalThis", - }, - ], - optional: false, - }, - }, - ], - }, - { - type: "VariableDeclaration", - kind: "const", - declarations: [ - { - type: "VariableDeclarator", - id: { - type: "Identifier", - name: "document", - }, - init: { - type: "ObjectExpression", - properties: [], - }, - }, - ], - }, -]; +let self = globalThis; +`).body; diff --git a/src/solvers_test.ts b/src/solvers.test.ts similarity index 63% rename from src/solvers_test.ts rename to src/solvers.test.ts index a5465d8..75f6e8d 100644 --- a/src/solvers_test.ts +++ b/src/solvers.test.ts @@ -1,22 +1,24 @@ -import { assertStrictEquals } from "@std/assert"; import { getFromPrepared, preprocessPlayer } from "./solvers.ts"; import { players, tests } from "../tests/tests.ts"; import { getCachePath } from "../tests/utils.ts"; +import { getIO } from "../tests/io.ts"; + +const io = await getIO(); for (const test of tests) { for (const variant of test.variants ?? players.keys()) { const path = getCachePath(test.player, variant); - Deno.test(`${test.player} ${variant}`, async (t) => { - const content = await Deno.readTextFile(path); + await io.test(`${test.player} ${variant}`, async (assert, subtest) => { + const content = await io.read(path); const solvers = getFromPrepared(preprocessPlayer(content)); for (const mode of ["nsig", "sig"] as const) { for (const step of test[mode] || []) { - await t.step(`${step.input} (${mode})`, () => { + await subtest(`${step.input} (${mode})`, () => { const got = solvers[mode]?.(step.input); - assertStrictEquals(got, step.expected); - }); + assert.equal(got, step.expected); + }) } } - }); + }) } } diff --git a/tests/download.ts b/tests/download.ts index 56d7fbd..6fe58c5 100644 --- a/tests/download.ts +++ b/tests/download.ts @@ -1,12 +1,14 @@ -import { exists } from "@std/fs/exists"; import { players, tests } from "./tests.ts"; import { getCachePath } from "./utils.ts"; +import { getIO } from "./io.ts"; + +const io = await getIO(); for (const test of tests) { const variants = test.variants ?? players.keys(); for (const variant of variants) { const path = getCachePath(test.player, variant); - if (await exists(path)) { + if (await io.exists(path)) { continue; } const playerPath = players.get(variant); @@ -17,10 +19,6 @@ for (const test of tests) { console.error(`Failed to request ${variant} player for ${test.player}`); continue; } - const file = await Deno.open(path, { - createNew: true, - write: true, - }); - response.body!.pipeTo(file.writable); + await io.write(path, response); } } diff --git a/tests/io.ts b/tests/io.ts new file mode 100644 index 0000000..383d2a4 --- /dev/null +++ b/tests/io.ts @@ -0,0 +1,134 @@ +type IO = { + exists(path: string): Promise; + read(path: string): Promise; + write(path: string, response: Response): Promise; + test: Test; +}; + +type Assert = { + equal(actual: T, expected: T, message?: string): void; +}; +type Test = (name: string, func: TestFunc) => Promise; +type TestFunc = (assert: Assert, subtest: Subtest) => Promise | void; +type Subtest = (name: string, func: SubtestFunc) => Promise; +type SubtestFunc = (assert: Assert) => Promise | void; + +let io: IO | null = null; + +export async function getIO(): Promise { + if (io === null) { + io = await _getIO(); + } + return io; +} + +async function _getIO(): Promise { + if (typeof Deno !== "undefined") { + const { exists } = await import("@std/fs/exists"); + const { assertStrictEquals } = await import("@std/assert"); + const assert = { + equal(actual: T, expected: T, message?: string) { + return assertStrictEquals(actual, expected, message); + }, + }; + return { + exists, + read(path: string): Promise { + return Deno.readTextFile(path); + }, + async write(path: string, response: Response): Promise { + const file = await Deno.open(path, { + createNew: true, + write: true, + }); + response.body!.pipeTo(file.writable); + }, + test(name: string, func: TestFunc) { + Deno.test(name, (t) => { + return func(assert, async (name, func): Promise => { + await t.step(name, () => { + return func(assert); + }); + }); + }); + return Promise.resolve(); + }, + }; + } else if (typeof Bun !== "undefined") { + const { expect, test } = await import("bun:test"); + const { access } = await import("node:fs/promises"); + const assert = { + equal(actual: T, expected: T, message?: string) { + return expect(actual).toBe(expected, message); + }, + }; + return { + async exists(path: string): Promise { + try { + await access(path); + return true; + } catch { + return false; + } + }, + read(path: string): Promise { + return Bun.file(path).text(); + }, + write(path: string, response: Response): Promise { + return Bun.write(path, response); + }, + test(name: string, func: TestFunc) { + test(name, () => { + // XXX: how to do subtests + return func(assert, async (name, func): Promise => { + await func(assert); + }); + }); + return Promise.resolve(); + }, + }; + } else if ( + typeof navigator === "object" && + navigator.userAgent.startsWith("Node.js") + ) { + const { suite, test } = await import("node:test"); + const { readFile, writeFile, access } = await import("node:fs/promises"); + const { deepStrictEqual } = await import("node:assert"); + const assert: Assert = { + equal(actual: T, expected: T, message?: string): void { + deepStrictEqual(actual, expected, message); + }, + }; + return { + async exists(path: string): Promise { + try { + await access(path); + return true; + } catch { + return false; + } + }, + read(path: string): Promise { + return readFile(path, { encoding: "utf-8" }); + }, + write(path: string, response: Response): Promise { + return writeFile(path, response.body!); + }, + test(name: string, func: TestFunc): Promise { + suite(name, () => { + return func(assert, async (name, func): Promise => { + await test(name, async () => { + await func(assert); + }); + }); + }); + return Promise.resolve(); + }, + }; + } + throw new Error( + `unsupported runtime for testing${ + navigator.userAgent ? `: ${navigator.userAgent}` : "" + }` + ); +}