@fluidframework/azure-end-to-end-tests 2.70.0-361248 → 2.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/lib/test/multiprocess/childClient.tool.js +226 -104
- package/lib/test/multiprocess/childClient.tool.js.map +1 -1
- package/lib/test/multiprocess/messageTypes.js.map +1 -1
- package/lib/test/multiprocess/orchestratorUtils.js +72 -17
- package/lib/test/multiprocess/orchestratorUtils.js.map +1 -1
- package/lib/test/multiprocess/presenceTest.spec.js +151 -100
- package/lib/test/multiprocess/presenceTest.spec.js.map +1 -1
- package/package.json +24 -23
- package/src/test/multiprocess/childClient.tool.ts +277 -131
- package/src/test/multiprocess/messageTypes.ts +36 -0
- package/src/test/multiprocess/orchestratorUtils.ts +121 -23
- package/src/test/multiprocess/presenceTest.spec.ts +202 -125
|
@@ -13,11 +13,20 @@ export interface UserIdAndName {
|
|
|
13
13
|
name: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export interface EventEntry {
|
|
17
|
+
timestamp: number;
|
|
18
|
+
agentId: string;
|
|
19
|
+
eventCategory: string;
|
|
20
|
+
eventName: string;
|
|
21
|
+
details?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
/**
|
|
17
25
|
* Message types sent from the orchestrator to the child processes
|
|
18
26
|
*/
|
|
19
27
|
export type MessageToChild =
|
|
20
28
|
| ConnectCommand
|
|
29
|
+
| DebugReportCommand
|
|
21
30
|
| DisconnectSelfCommand
|
|
22
31
|
| RegisterWorkspaceCommand
|
|
23
32
|
| GetLatestValueCommand
|
|
@@ -50,6 +59,24 @@ export interface ConnectCommand {
|
|
|
50
59
|
containerId?: string;
|
|
51
60
|
}
|
|
52
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Instructs a child process to report debug information.
|
|
64
|
+
*
|
|
65
|
+
* @privateRemarks
|
|
66
|
+
* This can be expanded over time to include more options.
|
|
67
|
+
*/
|
|
68
|
+
interface DebugReportCommand {
|
|
69
|
+
command: "debugReport";
|
|
70
|
+
/**
|
|
71
|
+
* Send event log entries.
|
|
72
|
+
*/
|
|
73
|
+
sendEventLog?: true;
|
|
74
|
+
/**
|
|
75
|
+
* Send basic attendee statistics (like count of connected).
|
|
76
|
+
*/
|
|
77
|
+
reportAttendees?: true;
|
|
78
|
+
}
|
|
79
|
+
|
|
53
80
|
/**
|
|
54
81
|
* Instructs a child process to disconnect from a Fluid container.
|
|
55
82
|
* A {@link DisconnectedSelfEvent} should be expected in response.
|
|
@@ -127,6 +154,7 @@ export type MessageFromChild =
|
|
|
127
154
|
| AttendeeConnectedEvent
|
|
128
155
|
| AttendeeDisconnectedEvent
|
|
129
156
|
| ConnectedEvent
|
|
157
|
+
| DebugReportCompleteEvent
|
|
130
158
|
| DisconnectedSelfEvent
|
|
131
159
|
| ErrorEvent
|
|
132
160
|
| LatestMapValueGetResponseEvent
|
|
@@ -167,6 +195,14 @@ interface ConnectedEvent {
|
|
|
167
195
|
attendeeId: AttendeeId;
|
|
168
196
|
}
|
|
169
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Sent from the child processes to the orchestrator in response to a {@link DebugReportCommand}.
|
|
200
|
+
*/
|
|
201
|
+
interface DebugReportCompleteEvent {
|
|
202
|
+
event: "debugReportComplete";
|
|
203
|
+
log?: EventEntry[];
|
|
204
|
+
}
|
|
205
|
+
|
|
170
206
|
/**
|
|
171
207
|
* Sent from the child processes to the orchestrator in response to a {@link DisconnectSelfCommand}.
|
|
172
208
|
*/
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { fork
|
|
6
|
+
import { fork } from "node:child_process";
|
|
7
|
+
import type { ChildProcess as AnyChildProcess } from "node:child_process";
|
|
7
8
|
|
|
8
9
|
import { ScopeType } from "@fluidframework/driver-definitions/legacy";
|
|
9
10
|
import type { AttendeeId } from "@fluidframework/presence/beta";
|
|
@@ -16,6 +17,8 @@ import type {
|
|
|
16
17
|
LatestMapValueUpdatedEvent,
|
|
17
18
|
LatestValueGetResponseEvent,
|
|
18
19
|
LatestMapValueGetResponseEvent,
|
|
20
|
+
MessageToChild,
|
|
21
|
+
EventEntry,
|
|
19
22
|
} from "./messageTypes.js";
|
|
20
23
|
|
|
21
24
|
/**
|
|
@@ -39,6 +42,10 @@ export const testConsole = {
|
|
|
39
42
|
error: console.error,
|
|
40
43
|
};
|
|
41
44
|
|
|
45
|
+
interface ChildProcess extends AnyChildProcess {
|
|
46
|
+
send(message: MessageToChild): boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
42
49
|
/**
|
|
43
50
|
* Fork child processes to simulate multiple Fluid clients.
|
|
44
51
|
*
|
|
@@ -85,9 +92,12 @@ export async function forkChildProcesses(
|
|
|
85
92
|
});
|
|
86
93
|
childReadyPromises.push(readyPromise);
|
|
87
94
|
const errorPromise = new Promise<never>((_resolve, reject) => {
|
|
88
|
-
child.
|
|
95
|
+
child.once("error", (error) => {
|
|
89
96
|
reject(new Error(`Child${i} process errored: ${error.message}`));
|
|
90
97
|
});
|
|
98
|
+
child.once("exit", (code, signal) => {
|
|
99
|
+
reject(new Error(`Child${i} process exited: code ${code}, signal ${signal}`));
|
|
100
|
+
});
|
|
91
101
|
});
|
|
92
102
|
childErrorPromises.push(errorPromise);
|
|
93
103
|
child.send({ command: "ping" });
|
|
@@ -98,6 +108,42 @@ export async function forkChildProcesses(
|
|
|
98
108
|
return { children, childErrorPromise };
|
|
99
109
|
}
|
|
100
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Instructs all listed child processes to send debug reports and then the
|
|
113
|
+
* collection is output sorted by timestamp. Report content is up to the child
|
|
114
|
+
* processes, but typically includes messages sent and some telemetry events.
|
|
115
|
+
*/
|
|
116
|
+
export async function executeDebugReports(
|
|
117
|
+
childrenRequestedToReport: ChildProcess[],
|
|
118
|
+
): Promise<void> {
|
|
119
|
+
const debugReportPromises: Promise<EventEntry[]>[] = [];
|
|
120
|
+
for (const child of childrenRequestedToReport) {
|
|
121
|
+
const debugReportPromise = new Promise<EventEntry[]>((resolve) => {
|
|
122
|
+
const handler = (msg: MessageFromChild): void => {
|
|
123
|
+
if (msg.event === "debugReportComplete") {
|
|
124
|
+
resolve(msg.log ?? []);
|
|
125
|
+
child.off("message", handler);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
child.on("message", handler);
|
|
129
|
+
});
|
|
130
|
+
debugReportPromises.push(debugReportPromise);
|
|
131
|
+
child.send({ command: "debugReport", sendEventLog: true, reportAttendees: true });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const logs = await Promise.all(debugReportPromises);
|
|
135
|
+
const combinedLogs = logs.flat().sort((a, b) => a.timestamp - b.timestamp);
|
|
136
|
+
for (const entry of combinedLogs) {
|
|
137
|
+
testConsole.log(
|
|
138
|
+
`[${new Date(entry.timestamp).toISOString()}] [${entry.agentId}] [${entry.eventCategory}] ${entry.eventName}${
|
|
139
|
+
entry.details
|
|
140
|
+
? ` - ${typeof entry.details === "string" ? entry.details : JSON.stringify(entry.details)}`
|
|
141
|
+
: ""
|
|
142
|
+
}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
101
147
|
/**
|
|
102
148
|
* Creates a {@link ConnectCommand} for a test user with a deterministic id and name.
|
|
103
149
|
*
|
|
@@ -118,6 +164,47 @@ function composeConnectMessage(
|
|
|
118
164
|
};
|
|
119
165
|
}
|
|
120
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Listens for a "connected" response from a child process
|
|
169
|
+
* allowing/handling subset of other expected messages.
|
|
170
|
+
*/
|
|
171
|
+
function listenForConnectedResponse({
|
|
172
|
+
child,
|
|
173
|
+
childId,
|
|
174
|
+
onConnected,
|
|
175
|
+
reject,
|
|
176
|
+
}: {
|
|
177
|
+
child: ChildProcess;
|
|
178
|
+
childId: number | string;
|
|
179
|
+
/**
|
|
180
|
+
* Will be called up to once when a "connected" message is received.
|
|
181
|
+
*/
|
|
182
|
+
onConnected: (msg: Extract<MessageFromChild, { event: "connected" }>) => void;
|
|
183
|
+
/**
|
|
184
|
+
* Callback to reject for unexpected messages or child errors.
|
|
185
|
+
*/
|
|
186
|
+
reject: (reason?: unknown) => void;
|
|
187
|
+
}): void {
|
|
188
|
+
const listener = (msg: MessageFromChild): void => {
|
|
189
|
+
if (msg.event === "connected") {
|
|
190
|
+
child.off("message", listener);
|
|
191
|
+
onConnected(msg);
|
|
192
|
+
} else if (msg.event === "error") {
|
|
193
|
+
child.off("message", listener);
|
|
194
|
+
reject(new Error(`Child ${childId} process error: ${msg.error}`));
|
|
195
|
+
} else if (msg.event !== "ack") {
|
|
196
|
+
child.off("message", listener);
|
|
197
|
+
// This is not strictly required, but is current expectation.
|
|
198
|
+
reject(
|
|
199
|
+
new Error(
|
|
200
|
+
`Unexpected message from child ${childId} while connecting: ${JSON.stringify(msg)}`,
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
child.on("message", listener);
|
|
206
|
+
}
|
|
207
|
+
|
|
121
208
|
interface CreatorAttendeeIdAndAttendeePromises {
|
|
122
209
|
containerCreatorAttendeeId: AttendeeId;
|
|
123
210
|
attendeeIdPromises: Promise<AttendeeId>[];
|
|
@@ -140,18 +227,27 @@ export async function connectChildProcesses(
|
|
|
140
227
|
const containerReadyPromise = new Promise<{
|
|
141
228
|
containerCreatorAttendeeId: AttendeeId;
|
|
142
229
|
containerId: string;
|
|
143
|
-
}>((resolve, reject) =>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
230
|
+
}>((resolve, reject) =>
|
|
231
|
+
listenForConnectedResponse({
|
|
232
|
+
child: firstChild,
|
|
233
|
+
childId: 0,
|
|
234
|
+
onConnected: (msg) => {
|
|
235
|
+
if (msg.containerId) {
|
|
236
|
+
resolve({
|
|
237
|
+
containerCreatorAttendeeId: msg.attendeeId,
|
|
238
|
+
containerId: msg.containerId,
|
|
239
|
+
});
|
|
240
|
+
} else {
|
|
241
|
+
reject(
|
|
242
|
+
new Error(
|
|
243
|
+
`Child 0 (creator) connected without containerId: ${JSON.stringify(msg)}`,
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
reject,
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
155
251
|
{
|
|
156
252
|
// Note that DocWrite is used to have this attendee be the "leader".
|
|
157
253
|
// DocRead would also be valid as DocWrite is specified for attach when there
|
|
@@ -182,15 +278,16 @@ export async function connectChildProcesses(
|
|
|
182
278
|
);
|
|
183
279
|
message.containerId = containerId;
|
|
184
280
|
attendeeIdPromises.push(
|
|
185
|
-
new Promise<AttendeeId>((resolve, reject) =>
|
|
186
|
-
|
|
187
|
-
|
|
281
|
+
new Promise<AttendeeId>((resolve, reject) =>
|
|
282
|
+
listenForConnectedResponse({
|
|
283
|
+
child,
|
|
284
|
+
childId: index,
|
|
285
|
+
onConnected: (msg) => {
|
|
188
286
|
resolve(msg.attendeeId);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}),
|
|
287
|
+
},
|
|
288
|
+
reject,
|
|
289
|
+
}),
|
|
290
|
+
),
|
|
194
291
|
);
|
|
195
292
|
child.send(message);
|
|
196
293
|
}
|
|
@@ -236,6 +333,7 @@ export async function connectAndListenForAttendees(
|
|
|
236
333
|
if (msg.event === "attendeeConnected") {
|
|
237
334
|
attendeesJoinedEvents++;
|
|
238
335
|
if (attendeesJoinedEvents >= attendeeCountRequired) {
|
|
336
|
+
child.off("message", listenForAttendees);
|
|
239
337
|
resolve();
|
|
240
338
|
}
|
|
241
339
|
}
|
|
@@ -323,7 +421,7 @@ export async function connectAndWaitForAttendees(
|
|
|
323
421
|
export async function registerWorkspaceOnChildren(
|
|
324
422
|
children: ChildProcess[],
|
|
325
423
|
workspaceId: string,
|
|
326
|
-
options: { latest?:
|
|
424
|
+
options: { latest?: true; latestMap?: true; timeoutMs: number },
|
|
327
425
|
): Promise<void> {
|
|
328
426
|
const { latest, latestMap, timeoutMs } = options;
|
|
329
427
|
const promises = children.map(async (child, index) => {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
connectAndListenForAttendees,
|
|
16
16
|
connectAndWaitForAttendees,
|
|
17
17
|
connectChildProcesses,
|
|
18
|
+
executeDebugReports,
|
|
18
19
|
forkChildProcesses,
|
|
19
20
|
getLatestMapValueResponses,
|
|
20
21
|
getLatestValueResponses,
|
|
@@ -24,6 +25,14 @@ import {
|
|
|
24
25
|
waitForLatestValueUpdates,
|
|
25
26
|
} from "./orchestratorUtils.js";
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* When true, slower (long running time) tests will be run.
|
|
30
|
+
* Otherwise, those test will not appear. Console output is used to show that
|
|
31
|
+
* they exist. (They could be skipped, though skipped test are often an
|
|
32
|
+
* indication of a problem.)
|
|
33
|
+
*/
|
|
34
|
+
const shouldRunScaleTests = process.env.FLUID_TEST_SCALE !== undefined;
|
|
35
|
+
|
|
27
36
|
const useAzure = process.env.FLUID_CLIENT === "azure";
|
|
28
37
|
|
|
29
38
|
/**
|
|
@@ -46,7 +55,7 @@ const timeoutMultiplier = debuggerAttached ? 1000 : useAzure ? 3 : 1;
|
|
|
46
55
|
* @param context - The Mocha test context.
|
|
47
56
|
* @param duration - The duration in milliseconds to set the timeout to. Zero disables the timeout.
|
|
48
57
|
*/
|
|
49
|
-
function
|
|
58
|
+
function setTestTimeout(context: Mocha.Context, duration: number): void {
|
|
50
59
|
const currentTimeout = context.timeout();
|
|
51
60
|
const newTimeout =
|
|
52
61
|
debuggerAttached || currentTimeout === 0 || duration === 0
|
|
@@ -86,11 +95,6 @@ function setTimeout(context: Mocha.Context, duration: number): void {
|
|
|
86
95
|
* - Send response messages including any relevant data back to the orchestrator to verify expected behavior.
|
|
87
96
|
*/
|
|
88
97
|
|
|
89
|
-
/**
|
|
90
|
-
* This particular test suite tests the following E2E functionality for Presence:
|
|
91
|
-
* - Announce 'attendeeConnected' when remote client joins session.
|
|
92
|
-
* - Announce 'attendeeDisconnected' when remote client disconnects.
|
|
93
|
-
*/
|
|
94
98
|
describe(`Presence with AzureClient`, () => {
|
|
95
99
|
const afterCleanUp: (() => void)[] = [];
|
|
96
100
|
|
|
@@ -102,131 +106,212 @@ describe(`Presence with AzureClient`, () => {
|
|
|
102
106
|
afterCleanUp.length = 0;
|
|
103
107
|
});
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
109
|
+
describe("`attendees` support", () => {
|
|
110
|
+
const numClientsForAttendeeTests = [5, 40, 100, 250];
|
|
111
|
+
for (const numClients of numClientsForAttendeeTests) {
|
|
112
|
+
if (numClients > 50 && !shouldRunScaleTests) {
|
|
113
|
+
testConsole.log(
|
|
114
|
+
`skipping Presence attendee scale tests with ${numClients} clients (set FLUID_TEST_SCALE=true to run)`,
|
|
115
|
+
);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
assert(numClients > 1, "Must have at least two clients");
|
|
120
|
+
/**
|
|
121
|
+
* Timeout for child processes to connect to container ({@link ConnectedEvent})
|
|
122
|
+
*/
|
|
123
|
+
const childConnectTimeoutMs = 1000 * numClients * timeoutMultiplier;
|
|
124
|
+
/**
|
|
125
|
+
* Timeout for presence attendees to join per first child perspective {@link AttendeeConnectedEvent}
|
|
126
|
+
*/
|
|
127
|
+
const allAttendeesJoinedTimeoutMs = (1000 + 200 * numClients) * timeoutMultiplier;
|
|
128
|
+
/**
|
|
129
|
+
* Timeout for presence attendees to fully join (everyone knows about everyone) {@link AttendeeConnectedEvent}
|
|
130
|
+
*/
|
|
131
|
+
const allAttendeesFullyJoinedTimeoutMs = (2000 + 300 * numClients) * timeoutMultiplier;
|
|
132
|
+
|
|
133
|
+
for (const writeClients of [numClients, 1]) {
|
|
134
|
+
it(`announces 'attendeeConnected' when remote client joins session [${numClients} clients, ${writeClients} writers]`, async function () {
|
|
135
|
+
setTestTimeout(this, childConnectTimeoutMs + allAttendeesJoinedTimeoutMs + 1000);
|
|
136
|
+
|
|
137
|
+
// Setup
|
|
138
|
+
const { children, childErrorPromise } = await forkChildProcesses(
|
|
139
|
+
numClients,
|
|
140
|
+
afterCleanUp,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Further Setup with Act and Verify
|
|
144
|
+
await connectAndWaitForAttendees(
|
|
145
|
+
children,
|
|
146
|
+
{
|
|
147
|
+
writeClients,
|
|
148
|
+
attendeeCountRequired: numClients - 1,
|
|
149
|
+
childConnectTimeoutMs,
|
|
150
|
+
allAttendeesJoinedTimeoutMs,
|
|
151
|
+
},
|
|
152
|
+
childErrorPromise,
|
|
153
|
+
);
|
|
154
|
+
});
|
|
118
155
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
156
|
+
it(`announces 'attendeeDisconnected' when remote client disconnects [${numClients} clients, ${writeClients} writers]`, async function () {
|
|
157
|
+
if (useAzure && numClients > 50) {
|
|
158
|
+
// Even with increased timeouts, more than 50 clients can be too large for AFR.
|
|
159
|
+
// This may be due to slow responses/inactivity from the clients that are
|
|
160
|
+
// creating pressure on ADO agent.
|
|
161
|
+
this.skip();
|
|
162
|
+
}
|
|
125
163
|
|
|
126
|
-
|
|
164
|
+
const childDisconnectTimeoutMs = 10_000 * timeoutMultiplier;
|
|
127
165
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
166
|
+
setTestTimeout(
|
|
167
|
+
this,
|
|
168
|
+
childConnectTimeoutMs +
|
|
169
|
+
allAttendeesFullyJoinedTimeoutMs +
|
|
170
|
+
childDisconnectTimeoutMs +
|
|
171
|
+
1000,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Setup
|
|
175
|
+
const { children, childErrorPromise } = await forkChildProcesses(
|
|
176
|
+
numClients,
|
|
177
|
+
afterCleanUp,
|
|
178
|
+
);
|
|
133
179
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
children,
|
|
137
|
-
{
|
|
180
|
+
const startConnectAndFullJoin = performance.now();
|
|
181
|
+
const connectResult = await connectAndListenForAttendees(children, {
|
|
138
182
|
writeClients,
|
|
139
183
|
attendeeCountRequired: numClients - 1,
|
|
140
184
|
childConnectTimeoutMs,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// AB#48866: Fix intermittently failing presence tests
|
|
149
|
-
if (useAzure) {
|
|
150
|
-
this.skip();
|
|
151
|
-
}
|
|
152
|
-
// TODO: AB#45620: "Presence: perf: update Join pattern for scale" can handle
|
|
153
|
-
// larger counts of read-only attendees. Without protocol changes tests with
|
|
154
|
-
// 20+ attendees exceed current limits.
|
|
155
|
-
if (numClients >= 20 && writeClients === 1) {
|
|
156
|
-
this.skip();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const childDisconnectTimeoutMs = 10_000 * timeoutMultiplier;
|
|
160
|
-
|
|
161
|
-
setTimeout(
|
|
162
|
-
this,
|
|
163
|
-
childConnectTimeoutMs +
|
|
164
|
-
allAttendeesJoinedTimeoutMs +
|
|
165
|
-
childDisconnectTimeoutMs +
|
|
166
|
-
1000,
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
// Setup
|
|
170
|
-
const { children, childErrorPromise } = await forkChildProcesses(
|
|
171
|
-
numClients,
|
|
172
|
-
afterCleanUp,
|
|
173
|
-
);
|
|
185
|
+
});
|
|
186
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
187
|
+
connectResult.attendeeCountRequiredPromises[0].then(() =>
|
|
188
|
+
testConsole.log(
|
|
189
|
+
`[${new Date().toISOString()}] All attendees joined per child 0 after ${performance.now() - startConnectAndFullJoin}ms`,
|
|
190
|
+
),
|
|
191
|
+
);
|
|
174
192
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
193
|
+
// Wait for all attendees to be fully joined
|
|
194
|
+
// Keep a tally for debuggability
|
|
195
|
+
let childrenFullyJoined = 0;
|
|
196
|
+
const setNotFullyJoined = new Set<number>();
|
|
197
|
+
for (let i = 0; i < children.length; i++) {
|
|
198
|
+
setNotFullyJoined.add(i);
|
|
199
|
+
}
|
|
200
|
+
const allAttendeesFullyJoined = Promise.all(
|
|
201
|
+
connectResult.attendeeCountRequiredPromises.map(
|
|
202
|
+
async (attendeeFullyJoinedPromise, index) => {
|
|
203
|
+
await attendeeFullyJoinedPromise;
|
|
204
|
+
childrenFullyJoined++;
|
|
205
|
+
setNotFullyJoined.delete(index);
|
|
206
|
+
},
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
let timedout = true;
|
|
210
|
+
const allFullyJoinedOrChildError = Promise.race([
|
|
211
|
+
allAttendeesFullyJoined,
|
|
212
|
+
childErrorPromise,
|
|
213
|
+
]).finally(() => (timedout = false));
|
|
214
|
+
await timeoutAwait(allFullyJoinedOrChildError, {
|
|
215
|
+
durationMs: allAttendeesFullyJoinedTimeoutMs,
|
|
216
|
+
errorMsg: "Not all attendees fully joined",
|
|
217
|
+
}).catch(async (error) => {
|
|
218
|
+
// Ideally this information would just be in the timeout error message, but that
|
|
219
|
+
// must be a resolved string (not dynamic). So, just log it separately.
|
|
220
|
+
testConsole.log(
|
|
221
|
+
`[${new Date().toISOString()}] ${childrenFullyJoined} attendees fully joined before error...`,
|
|
222
|
+
);
|
|
223
|
+
if (timedout) {
|
|
224
|
+
// Gather additional timing data if timed out to understand what increased
|
|
225
|
+
// timeout could work. Test will still fail if this secondary wait succeeds.
|
|
226
|
+
const startAdditionalWait = performance.now();
|
|
227
|
+
try {
|
|
228
|
+
await timeoutAwait(allFullyJoinedOrChildError, {
|
|
229
|
+
durationMs: allAttendeesFullyJoinedTimeoutMs,
|
|
230
|
+
});
|
|
231
|
+
testConsole.log(
|
|
232
|
+
`[${new Date().toISOString()}] All attendees fully joined after additional wait (${performance.now() - startAdditionalWait}ms)`,
|
|
233
|
+
);
|
|
234
|
+
} catch (secondaryError) {
|
|
235
|
+
testConsole.log(
|
|
236
|
+
`[${new Date().toISOString()}] Secondary await resulted in`,
|
|
237
|
+
secondaryError,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Gather and report debug info from children
|
|
243
|
+
// If there are less than 10 children, get all reports.
|
|
244
|
+
// Otherwise, just child 0 and those not fully joined.
|
|
245
|
+
setTestTimeout(this, 0); // Disable test timeout. Will throw within 20s below.
|
|
246
|
+
const childrenRequestedToReport =
|
|
247
|
+
children.length <= 10
|
|
248
|
+
? children
|
|
249
|
+
: // Just those not fully joined
|
|
250
|
+
children.filter((_, index) => index === 0 || setNotFullyJoined.has(index));
|
|
251
|
+
await timeoutAwait(
|
|
252
|
+
Promise.race([
|
|
253
|
+
executeDebugReports(childrenRequestedToReport),
|
|
254
|
+
childErrorPromise,
|
|
255
|
+
]),
|
|
256
|
+
{ durationMs: 20_000, errorMsg: "Debug report timeout" },
|
|
257
|
+
).catch((debugAwaitError) => {
|
|
258
|
+
testConsole.error("Debug report await resulted in", debugAwaitError);
|
|
259
|
+
});
|
|
180
260
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
connectResult.attendeeCountRequiredPromises.map((attendeeFullyJoinedPromise) =>
|
|
187
|
-
attendeeFullyJoinedPromise.then(() => childrenFullyJoined++),
|
|
188
|
-
),
|
|
189
|
-
);
|
|
190
|
-
await timeoutAwait(allAttendeesFullyJoined, {
|
|
191
|
-
durationMs: allAttendeesJoinedTimeoutMs,
|
|
192
|
-
errorMsg: "Not all attendees fully joined",
|
|
193
|
-
}).catch((error) => {
|
|
194
|
-
// Ideally this information would just be in the timeout error message, but that
|
|
195
|
-
// must be a resolved string (not dynamic). So, just log it separately.
|
|
196
|
-
testConsole.log(`${childrenFullyJoined} attendees fully joined before error...`);
|
|
197
|
-
throw error;
|
|
198
|
-
});
|
|
261
|
+
throw error;
|
|
262
|
+
});
|
|
263
|
+
testConsole.log(
|
|
264
|
+
`[${new Date().toISOString()}] All attendees fully joined after ${performance.now() - startConnectAndFullJoin}ms`,
|
|
265
|
+
);
|
|
199
266
|
|
|
200
|
-
|
|
201
|
-
index
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
267
|
+
let child0ReportRequested = false;
|
|
268
|
+
const waitForDisconnected = children.map(async (child, index) =>
|
|
269
|
+
index === 0
|
|
270
|
+
? Promise.resolve()
|
|
271
|
+
: timeoutPromise(
|
|
272
|
+
(resolve) => {
|
|
273
|
+
child.on("message", (msg: MessageFromChild) => {
|
|
274
|
+
if (
|
|
275
|
+
msg.event === "attendeeDisconnected" &&
|
|
276
|
+
msg.attendeeId === connectResult.containerCreatorAttendeeId
|
|
277
|
+
) {
|
|
278
|
+
console.log(`Child[${index}] saw creator disconnect`);
|
|
279
|
+
resolve();
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
durationMs: childDisconnectTimeoutMs,
|
|
285
|
+
errorMsg: `Attendee[${index}] Disconnected Timeout`,
|
|
286
|
+
},
|
|
287
|
+
).catch(async (error) => {
|
|
288
|
+
const childrenRequestedToReport = [child];
|
|
289
|
+
if (!child0ReportRequested) {
|
|
290
|
+
childrenRequestedToReport.unshift(children[0]);
|
|
291
|
+
child0ReportRequested = true;
|
|
292
|
+
}
|
|
293
|
+
await timeoutAwait(
|
|
294
|
+
Promise.race([
|
|
295
|
+
executeDebugReports(childrenRequestedToReport),
|
|
296
|
+
childErrorPromise,
|
|
297
|
+
]),
|
|
298
|
+
{ durationMs: 20_000, errorMsg: "Debug report timeout" },
|
|
299
|
+
).catch((debugAwaitError) => {
|
|
300
|
+
testConsole.error("Debug report await resulted in", debugAwaitError);
|
|
213
301
|
});
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
errorMsg: `Attendee[${index}] Disconnected Timeout`,
|
|
218
|
-
},
|
|
219
|
-
),
|
|
220
|
-
);
|
|
302
|
+
throw error;
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
221
305
|
|
|
222
|
-
|
|
223
|
-
|
|
306
|
+
// Act - disconnect first child process
|
|
307
|
+
children[0].send({ command: "disconnectSelf" });
|
|
224
308
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
309
|
+
// Verify - wait for all 'attendeeDisconnected' events
|
|
310
|
+
await Promise.race([Promise.all(waitForDisconnected), childErrorPromise]);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
228
313
|
}
|
|
229
|
-
}
|
|
314
|
+
});
|
|
230
315
|
|
|
231
316
|
{
|
|
232
317
|
/**
|
|
@@ -279,10 +364,6 @@ describe(`Presence with AzureClient`, () => {
|
|
|
279
364
|
});
|
|
280
365
|
|
|
281
366
|
it(`allows clients to read Latest state from other clients [${numClients} clients]`, async function () {
|
|
282
|
-
// AB#48866: Fix intermittently failing presence tests
|
|
283
|
-
if (useAzure) {
|
|
284
|
-
this.skip();
|
|
285
|
-
}
|
|
286
367
|
// Setup
|
|
287
368
|
const updateEventsPromise = waitForLatestValueUpdates(
|
|
288
369
|
remoteClients,
|
|
@@ -421,10 +502,6 @@ describe(`Presence with AzureClient`, () => {
|
|
|
421
502
|
});
|
|
422
503
|
|
|
423
504
|
it(`returns per-key values on read [${numClients} clients]`, async function () {
|
|
424
|
-
// AB#48866: Fix intermittently failing presence tests
|
|
425
|
-
if (useAzure) {
|
|
426
|
-
this.skip();
|
|
427
|
-
}
|
|
428
505
|
// Setup
|
|
429
506
|
const allAttendeeIds = await Promise.all(attendeeIdPromises);
|
|
430
507
|
const attendee0Id = containerCreatorAttendeeId;
|