forked from dfinity/pic-js
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpocket-ic-server.ts
More file actions
172 lines (150 loc) · 4.12 KB
/
pocket-ic-server.ts
File metadata and controls
172 lines (150 loc) · 4.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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();
}
}