UNPKG

@holochain/tryorama

Version:

Toolset to manage Holochain conductors and facilitate running test scenarios

249 lines (248 loc) 11.1 kB
import { encodeHashToBase64, } from "@holochain/client"; import isEqual from "lodash/isEqual.js"; import sortBy from "lodash/sortBy.js"; /** * A utility function to wait the given amount of time. * * @param milliseconds - The number of milliseconds to wait. * @returns A promise that is resolved after the given amount of milliseconds. * * @public */ export const pause = (milliseconds) => { return new Promise((resolve) => { setTimeout(resolve, milliseconds); }); }; /** * * A utility function to wait until a given function `isComplete` returns `true` for a given `input`, * or a timeout is reached. * * If the timeout is reached, then throws an error containing the string returned by the given function `onTimeoutMessage`. * * @param isComplete - Function to run on an interval, until the result is `true`. * @param onTimeoutMessage - Function that generates a string message which will be logged and thrown when the timeout is reached. * @param input - The input parameters to pass to `isComplete` and `onTimeoutMessage`. * @param intervalMs - Interval to pause between isCompleted runs (defaults to 500 milliseconds). * @param timeoutMs - A timeout for the delay (defaults to 60000 milliseconds). */ const retryUntilCompleteOrTimeout = async (isComplete, onTimeoutMessage, input, intervalMs = 500, timeoutMs = 40_000) => { // Always run the check at least once, even if the timeoutMs is 0. let completed = await isComplete(input); const startTime = Date.now(); while (!completed) { // Check if timeout has passed const currentTime = Date.now(); if (Math.floor(currentTime - startTime) >= timeoutMs) { const timeoutMessage = await onTimeoutMessage(input); const failureMessage = `Timeout of ${timeoutMs} ms has passed. ${timeoutMessage}`; console.error(failureMessage); throw Error(failureMessage); } completed = await isComplete(input); if (!completed) { await pause(intervalMs); } } }; const playerAppsToConductorCells = (players, dnaHash) => players.map((playerApp) => ({ conductor: playerApp.conductor, cellId: [dnaHash, playerApp.agentPubKey], })); /** * A utility function to compare conductors' integrated DhtOps. * * @param conductors - Array of conductors. * @param cellId - Cell id to compare integrated DhtOps from. * @returns A promise that is resolved after conductors' Integrated DhtOps match. * * @public */ export const areDhtsSynced = async (playerApps, dnaHash) => { const conductorCells = playerAppsToConductorCells(playerApps, dnaHash); return areConductorCellsDhtsSynced(conductorCells); }; /** * A utility function to compare conductors' integrated DhtOps. * * @param conductorCells - Array of ConductorCells * @returns A promise that is resolved after conductors' Integrated DhtOps match. * * @public */ export const areConductorCellsDhtsSynced = async (conductorCells) => { if (!isConductorCellDnaHashEqual(conductorCells)) { throw Error("Cannot compare DHT state of different DNAs"); } // Dump all conductors' states const conductorStates = await Promise.all(conductorCells.map((conductorCell) => conductorCell.conductor.adminWs().dumpFullState({ cell_id: conductorCell.cellId, dht_ops_cursor: undefined, }))); // Determine if all published ops are integrated in every conductor, and none are in limbo const limbosEmpty = conductorStates.every((state) => state.integration_dump.integration_limbo.length === 0 && state.integration_dump.validation_limbo.length === 0); // Compare conductors' integrated DhtOps const conductorDhtOpsIntegrated = conductorStates.map((conductor) => { return sortBy(conductor.integration_dump.integrated, [ // There are chain and warrant ops (op) => { if ("ChainOp" in op) { // Sort chain ops by op type (e. g. StoreEntry). return Object.keys(op.ChainOp)[0]; } else { // Sort warrant ops by signature. return encodeHashToBase64(op.WarrantOp.signature); } }, (op) => { if ("ChainOp" in op) { // Secondly sort by chain op signature. return Buffer.from(Object.values(op.ChainOp)[0][0]).toString("base64"); } else { // Sorting by signatures is sufficient for warrant ops. } }, ]); }); const allDhtOpsSynced = conductorDhtOpsIntegrated.every((ops) => isEqual(ops, conductorDhtOpsIntegrated[0])); return allDhtOpsSynced && limbosEmpty; }; /** * A utility function to wait until all conductors' DhtOps have been integrated, * and are identical for a given DNA. * * @param players - Array of players. * @param dnaHash - DNA hash to compare integrated DhtOps from. * @param intervalMs - Interval to pause between comparisons (defaults to 500 milliseconds). * @param timeoutMs - A timeout for the delay (defaults to 60000 milliseconds). * @returns A promise that is resolved after all agents' DHT states match. * * @public */ export const dhtSync = async (players, dnaHash, intervalMs = 500, timeoutMs = 60_000) => retryUntilCompleteOrTimeout(({ players, dnaHash }) => { const conductorCells = playerAppsToConductorCells(players, dnaHash); return areConductorCellsDhtsSynced(conductorCells); }, async ({ players, dnaHash }) => { const conductorCells = playerAppsToConductorCells(players, dnaHash); const conductorStates = await Promise.all(conductorCells.map((conductorCell) => conductorCell.conductor.adminWs().dumpFullState({ cell_id: conductorCell.cellId, dht_ops_cursor: undefined, }))); console.error(conductorStates.map((dump, idx) => ` Conductor ${idx} ------------------------------ # of integrated ops: ${dump.integration_dump.integrated.length} # of ops in integration limbo: ${dump.integration_dump.integration_limbo.length} # of ops in validation limbo: ${dump.integration_dump.validation_limbo.length}\n`)); return `Players' integrated DhtOps are not syncronized.`; }, { players, dnaHash }, intervalMs, timeoutMs); /** * A utility function to verify if all ConductorCells in an array have CellIds with * the same DnaHash. * * @param conductorCells - Array of ConductorCell. * @returns boolean * * @internal */ const isConductorCellDnaHashEqual = (conductorCells) => { const dnaHashes = conductorCells.map((conductorCell) => conductorCell.cellId[0]); return dnaHashes.every((val) => val === dnaHashes[0]); }; /** * A utility function to wait until a player's storage arc matches a desired * storage arc for a DNA * * @param player - A Player. * @param dnaHash - The DNA to check the storage arc for. * @param storageArc - The desired storage DhtArc to wait for. * @param intervalMs - Interval between comparisons in milliseconds (default 500). * @param timeoutMs - Timeout in milliseconds (default 40_000). * @returns A promise that resolves when the player's storage arc matches; rejects on timeout. * * @public */ export const storageArc = async (player, dnaHash, storageArc, intervalMs = 500, timeoutMs = 60_000) => retryUntilCompleteOrTimeout(({ player, dnaHash, storageArc }) => isEqualPlayerStorageArc(player, dnaHash, storageArc), async ({ player, dnaHash, storageArc }) => { const currentStorageArc = await getPlayerStorageArc(player, dnaHash); return `Player's storage arc did not match the desired storage arc ${storageArc}. Final storage arc: ${currentStorageArc}`; }, { player, dnaHash, storageArc }, intervalMs, timeoutMs); /** * A utility function to get the storage arc for a given player and dna hash. * * @param player - A Player. * @param dnaHash - The DNA to get the storage arc for. * @returns A Promise containing the storage DhtArc * * @public */ export const getPlayerStorageArc = async (player, dnaHash) => { const networkMetrics = await player.appWs.dumpNetworkMetrics({ dna_hash: dnaHash, include_dht_summary: false, }); const dnaHashB64 = encodeHashToBase64(dnaHash); if (networkMetrics[dnaHashB64] === undefined) { throw new Error(`DNA ${dnaHashB64} was not included in NetworkMetrics`); } const networkAgentSummary = networkMetrics[dnaHashB64].local_agents.find((l) => isEqual(l.agent, player.agentPubKey)); if (networkAgentSummary === undefined) { throw new Error(`Agent ${encodeHashToBase64(player.agentPubKey)} was not included in NetworkMetrics local_agents`); } return networkAgentSummary.storage_arc; }; /** * A utility function to get the storage arc for a given player and dna hash, * and then compare it to a desired storage arc. * * @param player - A Player. * @param dnaHash - The DNA to get the storage arc for. * @param storageArc - The desired storage DhtArc to compare to. * @returns boolean * * @internal */ const isEqualPlayerStorageArc = async (player, dnaHash, storageArc) => { const currentStorageArc = await getPlayerStorageArc(player, dnaHash); return isEqual(currentStorageArc, storageArc); }; /** * A utility function to wait until a player's integrated Ops count equals a desired * count for a DNA * * @param player - A Player. * @param cellId - The Cell to check the integrated Ops count for. * @param targetIntegratedOpsCount - The desired integrated Ops count to wait for. * @param intervalMs - Interval between comparisons in milliseconds (default 500). * @param timeoutMs - Timeout in milliseconds (default 40_000). * @returns A promise that resolves when the player's integrated ops count matches; rejects on timeout. * * @public */ export const integratedOpsCount = async (player, cellId, targetIntegratedOpsCount, intervalMs = 500, timeoutMs = 60_000) => retryUntilCompleteOrTimeout(async ({ player, cellId, targetIntegratedOpsCount }) => { const playerFullState = await player.conductor .adminWs() .dumpFullState({ cell_id: cellId, dht_ops_cursor: undefined, }); return (playerFullState.integration_dump.integrated.length === targetIntegratedOpsCount); }, async ({ player, cellId, targetIntegratedOpsCount }) => { const dump = await player.conductor .adminWs() .dumpFullState({ cell_id: cellId, dht_ops_cursor: undefined, }); console.error(` Conductor ------------------------------ # of integrated ops: ${dump.integration_dump.integrated.length} # of ops in integration limbo: ${dump.integration_dump.integration_limbo.length} # of ops in validation limbo: ${dump.integration_dump.validation_limbo.length}\n`); return `Target Integrated Ops Count: ${targetIntegratedOpsCount}, Current Integrated Ops Count: ${dump.integration_dump.integrated.length}`; }, { player, cellId, targetIntegratedOpsCount }, intervalMs, timeoutMs);