UNPKG

pic-js-mops

Version:

An Internet Computer Protocol canister testing library for TypeScript and JavaScript.

168 lines (145 loc) 4.19 kB
import { ChildProcess, spawn } from 'node:child_process'; import { resolve } from 'node:path'; import { chmodSync, rmSync } from 'node:fs'; import { BinNotFoundError, BinStartError, BinStartMacOSArmError, BinTimeoutError, } from './error.js'; import { exists, readFileAsString, tmpFile, isArm, isDarwin, poll, } from './util/index.js'; import { StartServerOptions } from './pocket-ic-server-types.js'; import { Writable } from 'node:stream'; /** * This class represents the main PocketIC server. * It is responsible for maintaining the lifecycle of the server process. * See {@link PocketIc} for details on the client to use with this server. * * @category API * * @example * ```ts * import { PocketIc, PocketIcServer } from '@dfinity/pic'; * import { _SERVICE, idlFactory } from '../declarations'; * * const wasmPath = resolve('..', '..', 'canister.wasm'); * * const picServer = await PocketIcServer.start(); * const pic = await PocketIc.create(picServer.getUrl()); * * const fixture = await pic.setupCanister<_SERVICE>({ idlFactory, wasmPath }); * const { actor } = fixture; * * // perform tests... * * await pic.tearDown(); * await picServer.stop(); * ``` */ export class PocketIcServer { private readonly url: string; private constructor( readonly serverProcess: ChildProcess, portNumber: number, ) { this.url = `http://127.0.0.1:${portNumber}`; } /** * Start a new PocketIC server. * * @param options Options for starting the server. * @returns An instance of the PocketIC server. */ public static async start( options: StartServerOptions = {}, ): Promise<PocketIcServer> { const binPath = options.binPath || this.getBinPath(); await this.assertBinExists(binPath); const pid = process.ppid; const picFilePrefix = `pocket_ic_${pid}`; const portFilePath = tmpFile(`${picFilePrefix}.port`); const serverProcess = spawn(binPath, ['--port-file', portFilePath, '--ttl', options.ttl ? options.ttl.toString() : '60']); if (options.showRuntimeLogs) { serverProcess.stdout.pipe(process.stdout); } else { serverProcess.stdout.pipe(new NullStream()); } if (options.showCanisterLogs) { serverProcess.stderr.pipe(process.stderr); } else { serverProcess.stderr.pipe(new NullStream()); } serverProcess.on('error', error => { if (isArm() && isDarwin()) { throw new BinStartMacOSArmError(error); } throw new BinStartError(error); }); return await poll( async () => { const portString = await readFileAsString(portFilePath); const port = parseInt(portString); if (isNaN(port)) { throw new BinTimeoutError(); } return new PocketIcServer(serverProcess, port); }, { intervalMs: POLL_INTERVAL_MS, timeoutMs: POLL_TIMEOUT_MS }, ); } /** * Get the URL of the server. * * @returns The URL of the server. */ public getUrl(): string { return this.url; } /** * Stop the server. * * @returns A promise that resolves when the server has stopped. */ public async stop(): Promise<void> { return new Promise((resolve, reject) => { this.serverProcess.on('exit', () => { resolve(); }); this.serverProcess.on('error', error => { reject(error); }); this.serverProcess.kill(); const picFilePrefix = `pocket_ic_${process.ppid}`; rmSync(tmpFile(`${picFilePrefix}.port`), {force: true}); rmSync(tmpFile(`${picFilePrefix}.ready`), {force: true}); }); } private static getBinPath(): string { return resolve(__dirname, '..', 'pocket-ic'); } private static async assertBinExists(binPath: string): Promise<void> { const binExists = await exists(binPath); if (!binExists) { throw new BinNotFoundError(binPath); } chmodSync(binPath, 0o700); } } const POLL_INTERVAL_MS = 20; const POLL_TIMEOUT_MS = 30_000; class NullStream extends Writable { override _write( _chunk: any, _encoding: BufferEncoding, callback: (error?: Error | null) => void, ): void { callback(); } }