Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Reduce e2e tests flakiness
Sebastian Martinez committed 2 years ago
commit d5d665f07053c4626f14d44b710e6bac9a0b0dd4
parent cc70c2468475823672df58272b3b8374f6a58a8a
5 files changed +156 -26
modified 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

modified 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",
modified tests/support/globalSetup.ts
@@ -19,6 +19,10 @@ export default async function globalSetup(_config: FullConfig): Promise<void> {
    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);
  }

modified tests/support/heartwood-version
@@ -1 +1 @@
-
a6a3290833f6da4a7785a426bed342df069ce47f
+
29507b550d6dcea5082baf1f5049ef93b8cc34a8
modified 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<string, string>;
  #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,
      },
    };