diff --git a/scripts/install-binaries b/scripts/install-binaries index 7cdfafb2c..3c6f53ade 100755 --- a/scripts/install-binaries +++ b/scripts/install-binaries @@ -48,8 +48,10 @@ do else # To provide deterministic Node and Repo IDs, we need a rad CLI compiled with the --debug flag. if [ "$BINARY_NAME" = "rad" ]; then - DOWNLOAD_NAME=rad-debug - else + DOWNLOAD_NAME=debug/rad + elif [ "$BINARY_NAME" = "git-remote-rad" ]; then + DOWNLOAD_NAME=debug/git-remote-rad + else DOWNLOAD_NAME=$BINARY_NAME fi diff --git a/tests/support/fixtures.ts b/tests/support/fixtures.ts index a2be3062d..12ba3abbd 100644 --- a/tests/support/fixtures.ts +++ b/tests/support/fixtures.ts @@ -11,7 +11,6 @@ import { test as base, expect } from "@playwright/test"; import * as Process from "./process.js"; import * as logLabel from "@tests/support/logLabel.js"; import { createPeerManager } from "./peerManager.js"; -import { sleep } from "@app/lib/sleep"; export { expect }; @@ -199,9 +198,14 @@ export async function createSeedFixture() { "-C", sourceBrowsingDir, ]); + // Workaround for fixing MaxListenersExceededWarning. + // Since every prefixOutput stream adds stream listeners that don't autoClose. + // TODO: We still seem to have some descriptors left open when running vitest, which we should handle. const peerManager = await createPeerManager({ dataDir: Path.resolve(Path.join(tmpDir, "peers")), - outputLog: FsSync.createWriteStream(Path.join(tmpDir, "log")), + outputLog: FsSync.createWriteStream( + Path.join(tmpDir, "log"), + ).setMaxListeners(20), }); const palm = await peerManager.startPeer({ name: "palm" }); await palm.startHttpd(); @@ -214,10 +218,14 @@ export async function createSeedFixture() { gitOptions: gitOptions["bob"], }); await palm.startNode({ trackingPolicy: "track", trackingScope: "all" }); - await alice.startNode({ connect: palm }); - await bob.startNode({ connect: palm }); - await sleep(1000); + await alice.startNode(); + await bob.startNode(); + await alice.connect(palm); + await bob.connect(palm); + await alice.git(["clone", sourceBrowsingDir], { cwd: alice.checkoutPath }); + await alice.git(["checkout", "feature/branch"]); + await alice.git(["checkout", "orphaned-branch"]); await alice.git(["checkout", "main"]); await alice.rad([ "init", @@ -229,17 +237,20 @@ export async function createSeedFixture() { "Git repository for source browsing tests", "--announce", ]); - await alice.git(["checkout", "feature/branch"]); - await alice.git(["push", "rad"]); - await sleep(2000); - await alice.git(["checkout", "orphaned-branch"]); - await alice.git(["push", "rad"]); - await sleep(2000); - const { stdout: rid } = await alice.rad(["inspect"]); + // Needed due to rad init not pushing all branches. + await alice.git(["push", "rad", "--all"]); await alice.rad(["track", bob.nodeId]); - await sleep(2000); + + await alice.waitForRoutes(rid, alice.nodeId, palm.nodeId); + await bob.waitForRoutes(rid, alice.nodeId, palm.nodeId); + await palm.waitForRoutes(rid, alice.nodeId, palm.nodeId); + await bob.rad(["clone", rid], { cwd: bob.checkoutPath }); - await sleep(2000); + + await alice.waitForRoutes(rid, alice.nodeId, palm.nodeId, bob.nodeId); + await bob.waitForRoutes(rid, alice.nodeId, palm.nodeId, bob.nodeId); + await palm.waitForRoutes(rid, alice.nodeId, palm.nodeId, bob.nodeId); + await Fs.writeFile( Path.join(bob.checkoutPath, "source-browsing", "README.md"), "Updated readme", @@ -269,12 +280,15 @@ export const gitOptions = { alice: { GIT_AUTHOR_NAME: "Alice Liddell", GIT_AUTHOR_EMAIL: "alice@radicle.xyz", + GIT_AUTHOR_DATE: "1671125284", GIT_COMMITTER_NAME: "Alice Liddell", GIT_COMMITTER_EMAIL: "alice@radicle.xyz", + GIT_COMMITTER_DATE: "1671125284", }, bob: { GIT_AUTHOR_NAME: "Bob Belcher", GIT_AUTHOR_EMAIL: "bob@radicle.xyz", + GIT_AUTHOR_DATE: "1671125284", GIT_COMMITTER_NAME: "Bob Belcher", GIT_COMMITTER_EMAIL: "bob@radicle.xyz", GIT_COMMITTER_DATE: "Mon Dec 21 14:00 2022 +0100", diff --git a/tests/support/globalSetup.ts b/tests/support/globalSetup.ts index f37fade5e..3090e881b 100644 --- a/tests/support/globalSetup.ts +++ b/tests/support/globalSetup.ts @@ -19,6 +19,10 @@ export default async function globalSetup(_config: FullConfig): Promise { await assertRadInstalled(); } catch (error) { console.error(error); + console.log(""); + console.log("To download the required test binaries, run:"); + console.log(" 👉 ./scripts/install-binaries"); + console.log(""); process.exit(1); } diff --git a/tests/support/heartwood-version b/tests/support/heartwood-version index 34eec893d..e0c3cc8b6 100644 --- a/tests/support/heartwood-version +++ b/tests/support/heartwood-version @@ -1 +1 @@ -a6a3290833f6da4a7785a426bed342df069ce47f +29507b550d6dcea5082baf1f5049ef93b8cc34a8 diff --git a/tests/support/peerManager.ts b/tests/support/peerManager.ts index e03539e0c..5e29e3481 100644 --- a/tests/support/peerManager.ts +++ b/tests/support/peerManager.ts @@ -5,10 +5,50 @@ import * as Fs from "node:fs/promises"; import * as Path from "node:path"; import * as Stream from "node:stream"; import getPort from "get-port"; +import lodash from "lodash"; import waitOn from "wait-on"; import { execa } from "execa"; import * as Process from "./process.js"; +import { sleep } from "@app/lib/sleep.js"; + +export type RefsUpdate = + | { updated: { name: string; old: string; new: string } } + | { created: { name: string; oid: string } } + | { deleted: { name: string; oid: string } } + | { skipped: { name: string; oid: string } }; + +export type NodeEvent = + | { + type: "refs-fetched"; + remote: string; + rid: string; + updated: RefsUpdate[]; + } + | { + type: "refs-synced"; + remote: string; + rid: string; + } + | { + type: "seed-discovered"; + rid: string; + nid: string; + } + | { + type: "seed-dropped"; + nid: string; + rid: string; + } + | { + type: "peer-connected"; + nid: string; + }; + +export interface RoutingEntry { + nid: string; + rid: string; +} interface PeerManagerParams { dataPath: string; @@ -69,6 +109,7 @@ export class RadiclePeer { #seed: string; #radHome: string; + #eventRecords: NodeEvent[] = []; #outputLog: Stream.Writable; #gitOptions?: Record; #listenSocketAddr?: string; @@ -89,6 +130,15 @@ export class RadiclePeer { this.#outputLog = props.logFile; } + public async waitForEvent(searchEvent: NodeEvent, timeoutInMs: number) { + const start = new Date().getTime(); + + while (new Date().getTime() - start > timeoutInMs) { + this.#eventRecords.find(event => lodash.isEqual(searchEvent, event)); + await sleep(100); + } + } + public static async create({ dataPath, name, @@ -132,7 +182,6 @@ export class RadiclePeer { } public async startNode(params?: { - connect?: RadiclePeer; trackingScope?: "trusted" | "all"; trackingPolicy?: "track" | "block"; }) { @@ -147,12 +196,6 @@ export class RadiclePeer { "--listen", this.#listenSocketAddr, ]; - if (params?.connect) { - args.push( - "--connect", - `${params.connect.nodeId}@${params.connect.#listenSocketAddr}`, - ); - } if (params?.trackingScope) { args.push("--tracking-scope", params.trackingScope); } @@ -164,9 +207,75 @@ export class RadiclePeer { this.spawn("radicle-node", args); await waitOn({ - resources: [`tcp:${this.#listenSocketAddr}`], - timeout: 7000, + resources: [`socket:${Path.join(this.#radHome, "node", "control.sock")}`], }); + + this.rad(["node", "events"], { cwd: this.#radHome }).stdout?.on( + "data", + (data: any) => { + data + .toString() + .split("\n") + .forEach((data: unknown) => { + if (data && typeof data === "string" && data.trim() !== "") { + try { + const result: NodeEvent = JSON.parse(data); + this.#eventRecords.push(result); + } catch (e) { + console.log("Error parsing event", data); + } + } + }); + }, + ); + } + + public async waitForRoutes(rid: string, ...nodes: string[]) { + let remaining = nodes; + + while (remaining.length > 0) { + const { stdout: entries } = await this.rad( + ["node", "routing", "--rid", rid, "--json"], + { + cwd: this.#radHome, + }, + ); + + entries.split("\n").forEach(entry => { + if (entry && entry.trim() !== "") { + try { + const result: RoutingEntry = JSON.parse(entry); + remaining = remaining.filter(nid => result.nid !== nid); + } catch (e) { + console.log("Error parsing entry", entry); + } + } + }); + + await this.waitForEvent( + { type: "seed-discovered", rid, nid: this.nodeId }, + 6000, + ); + } + } + + public async connect(remote: RadiclePeer) { + if (!remote.#listenSocketAddr) { + throw new Error("Remote node has no listen addr yet"); + } + await this.rad( + ["node", "connect", remote.nodeId, remote.#listenSocketAddr], + { cwd: this.#radHome }, + ); + + await this.waitForEvent( + { type: "peer-connected", nid: remote.nodeId }, + 1000, + ); + await remote.waitForEvent( + { type: "peer-connected", nid: this.nodeId }, + 1000, + ); } public uiUrl(): string { @@ -204,6 +313,7 @@ export class RadiclePeer { GIT_CONFIG_NOSYSTEM: "1", RAD_HOME: this.#radHome, RAD_PASSPHRASE: "asdf", + RAD_COMMIT_TIME: "1671125284", RAD_SEED: this.#seed, }, };