Repository navigation
Expand file tree
/
Copy pathinProcessLockWait.test.ts
More file actions
231 lines (206 loc) · 8.72 KB
/
Copy pathinProcessLockWait.test.ts
File metadata and controls
231 lines (206 loc) · 8.72 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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
jest.mock('@microsoft/rush/lib/start', () => ({}));
jest.mock('@rushstack/rush-client-core', () => ({
...jest.requireActual('@rushstack/rush-client-core'),
connectOrAwaitDaemonStartupAsync: jest.fn(),
executeWithDaemonRestartAsync: jest.fn()
}));
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import {
DaemonClientError,
connectOrAwaitDaemonStartupAsync,
executeWithDaemonRestartAsync,
type DaemonClient
} from '@rushstack/rush-client-core';
import * as connectionOptions from '../daemonConnectionOptions';
import { getInProcessLockWaitDeadlineMs, setInProcessLockWait } from '../inProcessLockWait';
import { getBundledRushVersion } from '../lazyRushModules';
import { launchClientAsync } from '../launchClient';
// Rush reads these names from EnvironmentVariableNames in @microsoft/rush-lib.
const DEADLINE: string = '_RUSH_LOCK_WAIT_DEADLINE';
const DAEMON_PID: string = '_RUSH_LOCK_WAIT_DAEMON_PID';
// Rushx keeps a script that has a terminal in-process, so the rushx test hides the test runner's terminal.
function hideTerminal(): () => void {
const streams: object[] = [process.stdin, process.stdout, process.stderr];
const descriptors: (PropertyDescriptor | undefined)[] = streams.map((stream) =>
Object.getOwnPropertyDescriptor(stream, 'isTTY')
);
for (const stream of streams) {
Object.defineProperty(stream, 'isTTY', { value: undefined, configurable: true, writable: true });
}
return () => {
streams.forEach((stream, index) => {
const descriptor: PropertyDescriptor | undefined = descriptors[index];
if (descriptor) Object.defineProperty(stream, 'isTTY', descriptor);
else delete (stream as { isTTY?: boolean }).isTTY;
});
};
}
describe(getInProcessLockWaitDeadlineMs.name, () => {
it('counts the wait timeout from when the wait began', () => {
expect(
getInProcessLockWaitDeadlineMs({
startedAtMs: 1000,
admission: { waitTimeoutMs: 30000, waitTimeoutIsDefault: true },
daemonPid: 42
})
).toBe(31000);
expect(
getInProcessLockWaitDeadlineMs({
startedAtMs: 1000,
admission: { waitTimeoutMs: 60500 },
daemonPid: 42
})
).toBe(61500);
});
it('does not wait with --no-wait, a zero timeout, or no admission options', () => {
for (const admission of [{ noWait: true }, { waitTimeoutMs: 0 }, undefined]) {
expect(getInProcessLockWaitDeadlineMs({ startedAtMs: 1000, admission, daemonPid: 42 })).toBe(1000);
}
});
});
describe(setInProcessLockWait.name, () => {
it('sets the deadline and the daemon PID', () => {
const environment: NodeJS.ProcessEnv = { PATH: '/bin' };
setInProcessLockWait(environment, {
startedAtMs: 1000,
admission: { waitTimeoutMs: 5000 },
daemonPid: 42
});
expect(environment).toEqual({ PATH: '/bin', [DEADLINE]: '6000', [DAEMON_PID]: '42' });
});
it('removes a daemon PID that no daemon handed over', () => {
const environment: NodeJS.ProcessEnv = { [DAEMON_PID]: '7' };
setInProcessLockWait(environment, {
startedAtMs: 1000,
admission: { noWait: true },
daemonPid: undefined
});
expect(environment).toEqual({ [DEADLINE]: '1000' });
});
});
describe('rush-client asks in-process Rush to wait for the repository lock after it tried the daemon', () => {
const SENT_AT_MS: number = 1_000_000;
let folder: string;
let originalArgv: string[];
let originalEnvironment: NodeJS.ProcessEnv;
let now: jest.SpyInstance<number, []>;
function writeRushJson(rushVersion: string): void {
fs.writeFileSync(
path.join(folder, 'rush.json'),
JSON.stringify({ rushVersion, pnpmVersion: '10.27.0', projects: [] })
);
}
function mockDaemon(pid: number): void {
jest.mocked(connectOrAwaitDaemonStartupAsync).mockResolvedValue({
status: Promise.resolve({ pid }),
closeAsync: async () => undefined
} as unknown as DaemonClient);
}
/** The daemon takes 20 s to hand the request back. */
function mockHandBack(successorPid?: number): void {
jest.mocked(executeWithDaemonRestartAsync).mockImplementation(async (client, connection, options) => {
now.mockReturnValue(SENT_AT_MS + 20000);
if (successorPid !== undefined) {
await options.onRestartAsync?.({ restart: 1, reason: undefined, successorPid });
}
return { kind: 'fallback', reason: 'unsupported', message: 'The daemon cannot run this command.' };
});
}
function getLockWait(): { deadline?: string; daemonPid?: string } {
return { deadline: process.env[DEADLINE], daemonPid: process.env[DAEMON_PID] };
}
beforeEach(() => {
folder = fs.mkdtempSync(path.join(os.tmpdir(), 'rush-client-lock-wait-'));
originalArgv = process.argv;
originalEnvironment = process.env;
writeRushJson(getBundledRushVersion());
jest.spyOn(connectionOptions, 'getDaemonConnectionOptionsAsync').mockResolvedValue({
paths: {
runtimeDir: folder,
socketPath: path.join(folder, 'd.sock'),
lockfilePath: path.join(folder, 'daemon.pid.json')
}
});
jest.spyOn(process.stderr, 'write').mockReturnValue(true);
jest.spyOn(process, 'cwd').mockReturnValue(folder);
now = jest.spyOn(Date, 'now').mockReturnValue(SENT_AT_MS);
process.argv = [process.execPath, 'rush-client', 'build', '--to', 'project'];
process.env = { ...originalEnvironment, CI: 'false', RUSH_DAEMON: '1', RUSH_REPORTER: 'legacy' };
for (const name of ['RUSH_LOG_LEVEL', 'RUSH_PREVIEW_VERSION', DEADLINE, DAEMON_PID])
delete process.env[name];
});
afterEach(() => {
process.argv = originalArgv;
process.env = originalEnvironment;
jest.restoreAllMocks();
jest.mocked(connectOrAwaitDaemonStartupAsync).mockReset();
jest.mocked(executeWithDaemonRestartAsync).mockReset();
fs.rmSync(folder, { recursive: true });
});
it('for what is left of the wait timeout when the daemon hands the request back', async () => {
mockDaemon(42);
mockHandBack();
await launchClientAsync(false);
expect(executeWithDaemonRestartAsync).toHaveBeenCalledTimes(1);
expect(getLockWait()).toEqual({ deadline: `${SENT_AT_MS + 30000}`, daemonPid: '42' });
});
it('naming the daemon that the request followed a restart to', async () => {
mockDaemon(42);
mockHandBack(43);
await launchClientAsync(false);
expect(getLockWait()).toEqual({ deadline: `${SENT_AT_MS + 30000}`, daemonPid: '43' });
});
it('for the explicit wait timeout, and not at all with --no-wait', async () => {
mockDaemon(42);
mockHandBack();
process.argv = [process.execPath, 'rush-client', 'build', '--wait-timeout', '90', '--to', 'project'];
await launchClientAsync(false);
expect(getLockWait()).toEqual({ deadline: `${SENT_AT_MS + 90000}`, daemonPid: '42' });
now.mockReturnValue(SENT_AT_MS);
process.argv = [process.execPath, 'rush-client', 'build', '--no-wait', '--to', 'project'];
await launchClientAsync(false);
expect(getLockWait()).toEqual({ deadline: `${SENT_AT_MS}`, daemonPid: '42' });
});
it('for the whole wait timeout when it cannot reach a daemon', async () => {
jest
.mocked(connectOrAwaitDaemonStartupAsync)
.mockRejectedValue(new DaemonClientError('startupFailed', 'No ready daemon; auto-start is disabled.'));
await launchClientAsync(false);
expect(getLockWait()).toEqual({ deadline: `${SENT_AT_MS + 30000}`, daemonPid: undefined });
});
it('not when the command runs in-process without the daemon', async () => {
process.env.RUSH_DAEMON = '0';
await launchClientAsync(false);
expect(connectOrAwaitDaemonStartupAsync).not.toHaveBeenCalled();
expect(getLockWait()).toEqual({});
});
it('not for rushx, whose script would inherit the variables', async () => {
const restoreTerminal: () => void = hideTerminal();
try {
mockDaemon(42);
mockHandBack();
process.argv = [process.execPath, 'rushx-client', 'sample'];
delete process.env.RUSH_REPORTER;
await launchClientAsync(true);
expect(executeWithDaemonRestartAsync).toHaveBeenCalledTimes(1);
expect(process.argv[1]).toBe(
path.join(path.dirname(require.resolve('@microsoft/rush/package.json')), 'bin/rushx')
);
expect(getLockWait()).toEqual({});
} finally {
restoreTerminal();
}
});
it('not when the workspace selects another Rush release, which would not remove the variables', async () => {
writeRushJson('5.178.1');
mockDaemon(42);
mockHandBack();
await launchClientAsync(false);
expect(executeWithDaemonRestartAsync).toHaveBeenCalledTimes(1);
expect(getLockWait()).toEqual({});
});
});