Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

End-to-end tests: push the real histories side by side, each to a server of its own (#240)

* End-to-end tests: push the real histories side by side, each to a server of its own
test-bot authored 1 parent 1c63ad9commit 920d91b8132629a155b28c9eb283e6c4cc12e3e7Browse files
1 file changed+123−63
modifiedapps/diffinery/test/e2e/e2e.test.mjs+123−63View file
@@ -16,7 +16,7 @@
1616 import { fileURLToPath } from "node:url";
1717 import { createServer } from "node:net";
1818 import { crc32, deflateSync } from "node:zlib";
19-import { test, before, after } from "node:test";
19+import { test, describe, before, after } from "node:test";
2020 import assert from "node:assert/strict";
2121 import { fakeGitHub, lfsObject } from "./fake-github.mjs";
2222 import { migratedState } from "./d1-state.mjs";
@@ -25,7 +25,6 @@
2525 const AGENT = resolve(APP, "../../containers/runner/dist/agent.mjs");
2626 const work = mkdtempSync(join(tmpdir(), "diffinery-e2e-"));
2727 let base;
28-let dev;
2928 // The dev server writes to a file, not a pipe: a long synchronous git command keeps this process
3029 // from reading a pipe, and a full one (retried webhooks log a stack each) stalls the server mid-push.
3130 const devLogFile = join(work, "wrangler.log");
@@ -174,36 +173,51 @@
174173 return { claim, out };
175174 }
176175
177-before(async () => {
178- assert.ok(existsSync(AGENT), `build the runner first: pnpm --filter @diffinery/runner build (missing ${AGENT})`);
176+/**
177+ * A `wrangler dev` server of its own with an admin token. Most tests share the one before() starts;
178+ * the real histories each push to their own, so they run side by side on separate processes.
179+ */
180+const servers = [];
181+async function startServer(name = "") {
179182 const port = await freePort();
180- base = `http://127.0.0.1:${port}`;
181- const persist = join(work, "state");
183+ const url = `http://127.0.0.1:${port}`;
184+ const persist = join(work, name ? `state-${name}` : "state");
185+ const logFile = name ? join(work, `wrangler-${name}.log`) : devLogFile;
186+ const log = () => (existsSync(logFile) ? readFileSync(logFile, "utf8") : "");
182187 migratedState(APP, persist);
183188 // Until the web UI exists, run the same app through the API-only entry.
184189 const entry = existsSync(join(APP, "src/web/routes.tsx")) ? [] : ["test/e2e/entry.ts"];
185- const out = openSync(devLogFile, "a");
186- dev = spawn("pnpm", ["exec", "wrangler", "dev", ...entry, "-e", "dev", "--ip", "127.0.0.1", "--port", String(port), "--inspector-port", String(await freePort()), "--persist-to", persist, "--var", `PUBLIC_URL:${base}`, "--var", `MASTER_KEY:${randomBytes(32).toString("base64")}`, "--var", "SCHEDULE_TICK_MINUTES:1", "--show-interactive-dev-session=false"], {
190+ const out = openSync(logFile, "a");
191+ const child = spawn("pnpm", ["exec", "wrangler", "dev", ...entry, "-e", "dev", "--ip", "127.0.0.1", "--port", String(port), "--inspector-port", String(await freePort()), "--persist-to", persist, "--var", `PUBLIC_URL:${url}`, "--var", `MASTER_KEY:${randomBytes(32).toString("base64")}`, "--var", "SCHEDULE_TICK_MINUTES:1", "--show-interactive-dev-session=false"], {
187192 cwd: APP,
188193 env: { ...process.env, CI: "1", WRANGLER_SEND_METRICS: "false" },
189194 stdio: ["ignore", out, out],
190195 detached: true,
191196 });
192- await until("wrangler dev", async () => {
193- try {
194- return (await fetch(`${base}/healthz`)).ok;
195- } catch {
196- return false;
197- }
198- }, 120_000);
197+ servers.push(child);
198+ const end = Date.now() + 120_000;
199+ while (!(await fetch(`${url}/healthz`).then((r) => r.ok, () => false))) {
200+ if (Date.now() > end) assert.fail(`timed out waiting for wrangler dev${name ? ` (${name})` : ""}\n--- wrangler log tail ---\n${log().slice(-4000)}`);
201+ await sleep(500);
202+ }
199203 // DEV_AUTH signs requests without credentials in as dev@diffinery.local; mint a real token for everything else.
200- const res = await fetch(`${base}/api/v3/diffinery/tokens`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ name: "e2e", scopes: ["admin"] }) });
204+ const res = await fetch(`${url}/api/v3/diffinery/tokens`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ name: "e2e", scopes: ["admin"] }) });
201205 assert.equal(res.status, 201, await res.clone().text());
202- token = (await res.json()).token;
206+ const stop = () => {
207+ try {
208+ process.kill(-child.pid, "SIGTERM");
209+ } catch {}
210+ };
211+ return { base: url, token: (await res.json()).token, log, stop };
212+}
213+
214+before(async () => {
215+ assert.ok(existsSync(AGENT), `build the runner first: pnpm --filter @diffinery/runner build (missing ${AGENT})`);
216+ ({ base, token } = await startServer());
203217 });
204218
205219 after(() => {
206- if (dev?.pid) {
220+ for (const dev of servers) {
207221 try {
208222 process.kill(-dev.pid, "SIGTERM");
209223 } catch {}
@@ -2793,58 +2807,104 @@
27932807 });
27942808
27952809 /**
2796- * A real project's history, pushed whole to a new repository and cloned back. Pinned commits, so
2797- * the packs are the same each run; cloned from GitHub without the Diffinery token.
2810+ * `git` without blocking this process, so the histories below run side by side; it rejects with
2811+ * git's output and, on a server error, the tail of `log`. A `tok` of null sends no credentials (for GitHub).
27982812 */
2799-async function pushHistory(name, upstream) {
2800- const me = await ok("GET", "/user");
2801- await ok("POST", "/user/repos", { name });
2802- const remote = `${base}/${me.login}/${name}.git`;
2813+function gitAsync(cwd, args, tok = token, log = devLog) {
2814+ const auth = tok === null ? [] : ["-c", `http.extraHeader=Authorization: Basic ${Buffer.from(`x-token:${tok}`).toString("base64")}`];
2815+ return new Promise((done, fail) => {
2816+ const child = spawn("git", [...auth, "-c", "protocol.version=2", ...args], {
2817+ cwd,
2818+ stdio: ["ignore", "pipe", "pipe"],
2819+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_AUTHOR_NAME: "E2E", GIT_AUTHOR_EMAIL: "e2e@example.com", GIT_COMMITTER_NAME: "E2E", GIT_COMMITTER_EMAIL: "e2e@example.com" },
2820+ });
2821+ let stdout = "";
2822+ let stderr = "";
2823+ child.stdout.on("data", (d) => (stdout += d));
2824+ child.stderr.on("data", (d) => (stderr += d));
2825+ child.on("error", fail);
2826+ child.on("close", (status) => {
2827+ if (status === 0) return done(stdout);
2828+ let message = `git ${args.join(" ")} exited ${status}\n${stderr}`;
2829+ if (/HTTP 5\d\d/.test(stderr)) message += `\n--- wrangler log tail ---\n${log().slice(-4000)}`;
2830+ fail(Object.assign(new Error(message), { stdout, stderr }));
2831+ });
2832+ });
2833+}
2834+
2835+/**
2836+ * A real project's history, pushed whole to a new repository on `server` and cloned back. Pinned
2837+ * commits, so the packs are the same each run; cloned from GitHub without the Diffinery token.
2838+ */
2839+async function pushHistory(server, name, upstream) {
2840+ const call = async (method, path, body) => {
2841+ const send = () => fetch(`${server.base}/api/v3${path}`, { method, headers: { authorization: `Bearer ${server.token}`, "content-type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body) });
2842+ // A GET goes again on a kept-alive connection the server closed meanwhile, as in api().
2843+ const r = await send().catch((e) => (method === "GET" ? sleep(1000).then(send) : Promise.reject(e)));
2844+ const text = await r.text();
2845+ assert.ok(r.ok, `${method} ${path} -> ${r.status} ${text}`);
2846+ return JSON.parse(text);
2847+ };
2848+ const g = (cwd, args) => gitAsync(cwd, args, server.token, server.log);
2849+ const me = await call("GET", "/user");
2850+ await call("POST", "/user/repos", { name });
2851+ const remote = `${server.base}/${me.login}/${name}.git`;
28032852 // Test fixtures in these histories look like credentials (tornado has a test private key): pushed as an admin's bypass.
2804- git(upstream, ["-c", "http.postBuffer=1048576", "push", "-q", "-o", "allow-secrets=e2e of a public history", remote, "main"], token, { maxBuffer: 16 * 1024 * 1024 });
2853+ await g(upstream, ["-c", "http.postBuffer=1048576", "push", "-q", "-o", "allow-secrets=e2e of a public history", remote, "main"]);
28052854 const out = join(work, `${name}-clone`);
2806- git(work, ["clone", "-q", "--bare", remote, out]);
2807- assert.equal(git(out, ["rev-parse", "main"]).trim(), git(upstream, ["rev-parse", "main"]).trim());
2808- assert.equal(git(out, ["rev-list", "--count", "main"]).trim(), git(upstream, ["rev-list", "--count", "main"]).trim());
2809- git(out, ["fsck", "--full"]);
2855+ await g(work, ["clone", "-q", "--bare", remote, out]);
2856+ assert.equal((await g(out, ["rev-parse", "main"])).trim(), (await g(upstream, ["rev-parse", "main"])).trim());
2857+ assert.equal((await g(out, ["rev-list", "--count", "main"])).trim(), (await g(upstream, ["rev-list", "--count", "main"])).trim());
2858+ await g(out, ["fsck", "--full"]);
28102859 }
28112860
2812-for (const [name, url, sha] of [
2813- // 37 MB: staged in R2 and unpacked in pieces.
2814- ["jquery", "https://github.com/jquery/jquery", "c2f27864e4ffc2d1409c289fcb9a42b6165dfeaf"],
2815- // About 12 MB each: too many objects for parsing whole in memory, so unpacked in pieces too.
2816- ["tornado", "https://github.com/tornadoweb/tornado", "056cacaefc7de31bda28363378c4ffc86b97d7e4"],
2817- ["flask", "https://github.com/pallets/flask", "d086db856be187255b8ec61ef409357393020f32"],
2818-]) {
2819- test(`real histories: ${name}'s whole history goes in one push and clones back intact`, async () => {
2820- const upstream = join(work, `${name}-upstream.git`);
2821- execFileSync("git", ["clone", "-q", "--bare", "--no-tags", url, upstream], { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
2822- execFileSync("git", ["update-ref", "refs/heads/main", sha], { cwd: upstream });
2823- await pushHistory(name, upstream);
2824- });
2825-}
2826-
2827-test("real histories: a 2.6 MB lockfile's 60 versions, a chain of deltas, go in one small push", async () => {
2828- const upstream = join(work, "lockfile-upstream");
2829- mkdirSync(upstream, { recursive: true });
2830- git(upstream, ["init", "-q", "-b", "main"]);
2831- const versions = new Map();
2832- const pkgs = Array.from({ length: 11_000 }, (_, i) => `pkg-${String(i).padStart(5, "0")}`);
2833- pkgs.forEach((p, i) => versions.set(p, `1.${i % 51}.${i % 97}`));
2834- for (let c = 0; c < 60; c++) {
2835- for (let k = 0; k < 40; k++) versions.set(pkgs[(c * 7919 + k * 104729) % pkgs.length], `2.${c}.${k}`);
2836- const body = pkgs.map((p) => {
2837- const v = versions.get(p);
2838- return ` "node_modules/${p}": {\n "version": "${v}",\n "resolved": "https://registry.npmjs.org/${p}/-/${p}-${v}.tgz",\n "integrity": "sha512-${createHash("sha512").update(p + v).digest("hex").slice(0, 64)}"\n },\n`;
2861+// Side by side, each on a server of its own: one big push keeps one server process busy, so on
2862+// separate processes they use the runner's other cores and take about as long as jquery's alone
2863+// instead of the sum. The lockfile's push uses the shared server.
2864+describe("real histories: whole projects, side by side", { concurrency: true }, () => {
2865+ for (const [name, url, sha] of [
2866+ // 37 MB: staged in R2 and unpacked in pieces.
2867+ ["jquery", "https://github.com/jquery/jquery", "c2f27864e4ffc2d1409c289fcb9a42b6165dfeaf"],
2868+ // About 12 MB each: too many objects for parsing whole in memory, so unpacked in pieces too.
2869+ ["tornado", "https://github.com/tornadoweb/tornado", "056cacaefc7de31bda28363378c4ffc86b97d7e4"],
2870+ ["flask", "https://github.com/pallets/flask", "d086db856be187255b8ec61ef409357393020f32"],
2871+ ]) {
2872+ test(`real histories: ${name}'s whole history goes in one push and clones back intact`, async () => {
2873+ const upstream = join(work, `${name}-upstream.git`);
2874+ const [server] = await Promise.all([
2875+ startServer(name),
2876+ gitAsync(work, ["clone", "-q", "--bare", "--no-tags", url, upstream], null).then(() => gitAsync(upstream, ["update-ref", "refs/heads/main", sha], null)),
2877+ ]);
2878+ try {
2879+ await pushHistory(server, name, upstream);
2880+ } finally {
2881+ server.stop();
2882+ }
28392883 });
2840- writeFileSync(join(upstream, "package-lock.json"), `{\n "lockfileVersion": 3,\n "packages": {\n${body.join("")} }\n}\n`);
2841- git(upstream, ["add", "-A"]);
2842- git(upstream, ["commit", "-q", "-m", `lockfile ${c}`]);
28432884 }
2844- assert.ok(readFileSync(join(upstream, "package-lock.json")).length > 2_500_000);
2845- const packBytes = Number(execFileSync("bash", ["-c", "git pack-objects --revs --stdout -q <<<main | wc -c"], { cwd: upstream, encoding: "utf8" }).trim());
2846- assert.ok(packBytes < 16 * 1024 * 1024, `the pack is ${packBytes} bytes`);
2847- await pushHistory("lockfile", upstream);
2885+
2886+ test("real histories: a 2.6 MB lockfile's 60 versions, a chain of deltas, go in one small push", async () => {
2887+ const upstream = join(work, "lockfile-upstream");
2888+ mkdirSync(upstream, { recursive: true });
2889+ git(upstream, ["init", "-q", "-b", "main"]);
2890+ const versions = new Map();
2891+ const pkgs = Array.from({ length: 11_000 }, (_, i) => `pkg-${String(i).padStart(5, "0")}`);
2892+ pkgs.forEach((p, i) => versions.set(p, `1.${i % 51}.${i % 97}`));
2893+ for (let c = 0; c < 60; c++) {
2894+ for (let k = 0; k < 40; k++) versions.set(pkgs[(c * 7919 + k * 104729) % pkgs.length], `2.${c}.${k}`);
2895+ const body = pkgs.map((p) => {
2896+ const v = versions.get(p);
2897+ return ` "node_modules/${p}": {\n "version": "${v}",\n "resolved": "https://registry.npmjs.org/${p}/-/${p}-${v}.tgz",\n "integrity": "sha512-${createHash("sha512").update(p + v).digest("hex").slice(0, 64)}"\n },\n`;
2898+ });
2899+ writeFileSync(join(upstream, "package-lock.json"), `{\n "lockfileVersion": 3,\n "packages": {\n${body.join("")} }\n}\n`);
2900+ git(upstream, ["add", "-A"]);
2901+ git(upstream, ["commit", "-q", "-m", `lockfile ${c}`]);
2902+ }
2903+ assert.ok(readFileSync(join(upstream, "package-lock.json")).length > 2_500_000);
2904+ const packBytes = Number(execFileSync("bash", ["-c", "git pack-objects --revs --stdout -q <<<main | wc -c"], { cwd: upstream, encoding: "utf8" }).trim());
2905+ assert.ok(packBytes < 16 * 1024 * 1024, `the pack is ${packBytes} bytes`);
2906+ await pushHistory({ base, token, log: devLog }, "lockfile", upstream);
2907+ });
28482908 });
28492909
28502910 test("large files: a 40 MB file is pushed and cloned intact, and push protection still reads big text files", async () => {