UNPKG

@holochain/tryorama

Version:

Toolset to manage Holochain conductors and facilitate running test scenarios

242 lines (241 loc) 8.97 kB
import assert from "node:assert"; import { v4 as uuidv4 } from "uuid"; import { addAllAgentsToAllConductors, enableAndGetAgentApp, runLocalServices, stopLocalServices, } from "./conductor-helpers.js"; import { cleanAllConductors, createConductor, } from "./conductor.js"; /** * An abstraction of a test scenario to write tests against Holochain hApps, * running on a local conductor. * * @public */ export class Scenario { timeout; noDpki; dpkiNetworkSeed; networkSeed; disableLocalServices; serviceProcess; bootstrapServerUrl; signalingServerUrl; conductors; /** * Scenario constructor. * * @param options - Timeout for requests to Admin and App API calls. */ constructor(options) { this.timeout = options?.timeout; this.noDpki = false; this.dpkiNetworkSeed = uuidv4(); this.networkSeed = uuidv4(); this.disableLocalServices = options?.disableLocalServices ?? false; this.serviceProcess = undefined; this.bootstrapServerUrl = undefined; this.signalingServerUrl = undefined; this.conductors = []; } /** * Create and add a conductor to the scenario. * * @returns The newly added conductor instance. */ async addConductor() { await this.ensureLocalServices(); assert(this.serviceProcess); assert(this.signalingServerUrl); const conductor = await createConductor(this.signalingServerUrl, { timeout: this.timeout, bootstrapServerUrl: this.bootstrapServerUrl, }); this.conductors.push(conductor); return conductor; } /** * Create conductors with agents and add them to the scenario. * * The specified number of conductors is created and one agent is * generated on each conductor. * * @param amount - The number of players to be created. * @param networkConfig - Optional {@link NetworkConfig} * @returns An array of {@link Player}s */ async addPlayers(amount, networkConfig) { await this.ensureLocalServices(); return Promise.all(new Array(amount).fill(0).map(async () => { const conductor = await this.addConductor(); if (networkConfig) { conductor.setNetworkConfig(networkConfig); } const agentPubKey = await conductor.adminWs().generateAgentPubKey(); return { conductor, agentPubKey }; })); } /** * Installs the provided apps for the provided players. * * The number of players must be at least as high as the number of apps. * * # Errors * * If any of the app options contains an agent pub key, an error is thrown, * because the agent pub keys of the players will be used for app installation. * * @param appsWithOptions - The apps with options to be installed * @param players - The players the apps are installed for * @returns An array of player apps. */ async installAppsForPlayers(appsWithOptions, players) { if (appsWithOptions.some((appWithOptions) => appWithOptions.options?.agentPubKey)) { throw new Error("Agent pub key in app options must not be set. Agent pub keys are taken from the players."); } await this.ensureLocalServices(); return Promise.all(appsWithOptions.map((appWithOptions, i) => { const player = players[i]; appWithOptions.options = appWithOptions.options ?? {}; appWithOptions.options.agentPubKey = player.agentPubKey; return this.installPlayerApp(player.conductor, appWithOptions); })); } /** * Installs the same provided app for the provided players. * * @param appsWithOptions - The app with options to be installed for all players * @param players - The players the apps are installed for * @returns An array of player apps. */ async installSameAppForPlayers(appWithOptions, players) { if (appWithOptions.options?.agentPubKey) { throw new Error("Agent pub key in app options must not be set. Agent pub keys are taken from the players."); } await this.ensureLocalServices(); return Promise.all(players.map((player) => { appWithOptions.options = appWithOptions.options ?? {}; appWithOptions.options.agentPubKey = player.agentPubKey; return this.installPlayerApp(player.conductor, appWithOptions); })); } /** * Create and add a single player with an app installed to the scenario. * * @param appBundleSource - The bundle or path to the bundle. * @param options - {@link AppOptions}. * @returns A player with the installed app. */ async addPlayerWithApp(appWithOptions) { await this.ensureLocalServices(); const conductor = await this.addConductor(); if (appWithOptions.options?.networkConfig) { conductor.setNetworkConfig(appWithOptions.options.networkConfig); } appWithOptions.options = { ...appWithOptions.options, networkSeed: appWithOptions.options?.networkSeed ?? this.networkSeed, }; return this.installPlayerApp(conductor, appWithOptions); } async installPlayerApp(conductor, appWithOptions) { const appInfo = await conductor.installApp(appWithOptions); const adminWs = conductor.adminWs(); const port = await conductor.attachAppInterface(); const issued = await adminWs.issueAppAuthenticationToken({ installed_app_id: appInfo.installed_app_id, }); const appWs = await conductor.connectAppWs(issued.token, port); const agentApp = await enableAndGetAgentApp(adminWs, appWs, appInfo); return { conductor, appWs, ...agentApp }; } /** * Create and add multiple players to the scenario, with the same app installed * for each player. * * @param appsWithOptions - An app to be installed for each player * @returns All created player apps. */ async addPlayersWithSameApp(appWithOptions, amount) { await this.ensureLocalServices(); return Promise.all(new Array(amount) .fill(0) .map(() => this.addPlayerWithApp(appWithOptions))); } /** * Create and add multiple players to the scenario, with an app installed * for each player. * * @param appsWithOptions - An array with an app for each player. * @returns All created player apps. */ async addPlayersWithApps(appsWithOptions) { await this.ensureLocalServices(); return Promise.all(appsWithOptions.map((appWithOptions) => this.addPlayerWithApp(appWithOptions))); } /** * Register all agents of all passed in conductors to each other. This skips * peer discovery through gossip and thus accelerates test runs. * * @public */ async shareAllAgents() { return addAllAgentsToAllConductors(this.conductors); } /** * Shut down all conductors in the scenario. */ async shutDown() { await Promise.all(this.conductors.map((conductor) => conductor.shutDown())); if (this.serviceProcess) { await stopLocalServices(this.serviceProcess); } } /** * Shut down and delete all conductors in the scenario. */ async cleanUp() { await this.shutDown(); await cleanAllConductors(); this.conductors = []; this.serviceProcess = undefined; this.bootstrapServerUrl = undefined; this.signalingServerUrl = undefined; } async ensureLocalServices() { if (this.disableLocalServices) { this.bootstrapServerUrl = new URL("https://BAD_URL"); this.signalingServerUrl = new URL("wss://BAD_URL"); } if (!this.serviceProcess) { ({ servicesProcess: this.serviceProcess, bootstrapServerUrl: this.bootstrapServerUrl, signalingServerUrl: this.signalingServerUrl, } = await runLocalServices()); } } } /** * A wrapper function to create and run a scenario. A scenario is created * and all involved conductors are shut down and cleaned up after running. * * @param testScenario - The test to be run. * @param cleanUp - Whether to delete conductors after running. @defaultValue true * * @public */ export const runScenario = async (testScenario, cleanUp = true, options) => { const scenario = new Scenario(options); try { await testScenario(scenario); } catch (error) { console.error("error occurred during test run:", error); throw error; } finally { if (cleanUp) { await scenario.cleanUp(); } else { await scenario.shutDown(); } } };