@fluidframework/azure-end-to-end-tests 2.70.0-361788 → 2.71.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.
@@ -25,6 +25,14 @@ import {
25
25
  waitForLatestValueUpdates,
26
26
  } from "./orchestratorUtils.js";
27
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
+
28
36
  const useAzure = process.env.FLUID_CLIENT === "azure";
29
37
 
30
38
  /**
@@ -35,7 +43,7 @@ const debuggerAttached = inspector.url() !== undefined;
35
43
  /**
36
44
  * Set this to a high number when debugging to avoid timeouts from debugging time.
37
45
  */
38
- const timeoutMultiplier = debuggerAttached ? 1000 : useAzure ? 3 : 1;
46
+ const timeoutMultiplier = debuggerAttached ? 1000 : useAzure ? 5 : 1;
39
47
 
40
48
  /**
41
49
  * Sets the timeout for the given test context.
@@ -98,215 +106,214 @@ describe(`Presence with AzureClient`, () => {
98
106
  afterCleanUp.length = 0;
99
107
  });
100
108
 
101
- // Note that on slower systems 50+ clients may take too long to join.
102
- const numClientsForAttendeeTests = [5, 20, 50, 100];
103
- // TODO: AB#45620: "Presence: perf: update Join pattern for scale" may help, then remove .slice.
104
- for (const numClients of numClientsForAttendeeTests.slice(0, 2)) {
105
- assert(numClients > 1, "Must have at least two clients");
106
- /**
107
- * Timeout for child processes to connect to container ({@link ConnectedEvent})
108
- */
109
- const childConnectTimeoutMs = 1000 * numClients * timeoutMultiplier;
110
- /**
111
- * Timeout for presence attendees to join per first child perspective {@link AttendeeConnectedEvent}
112
- */
113
- const allAttendeesJoinedTimeoutMs = (1000 + 200 * numClients) * timeoutMultiplier;
114
- /**
115
- * Timeout for presence attendees to fully join (everyone knows about everyone) {@link AttendeeConnectedEvent}
116
- */
117
- const allAttendeesFullyJoinedTimeoutMs = (2000 + 300 * numClients) * timeoutMultiplier;
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
118
 
119
- for (const writeClients of [numClients, 1]) {
120
- it(`announces 'attendeeConnected' when remote client joins session [${numClients} clients, ${writeClients} writers]`, async function () {
121
- // AB#48866: Fix intermittently failing presence tests
122
- if (useAzure) {
123
- this.skip();
124
- }
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 testAnnouncesAttendeeConnected() {
135
+ setTestTimeout(this, childConnectTimeoutMs + allAttendeesJoinedTimeoutMs + 1000);
125
136
 
126
- setTestTimeout(this, childConnectTimeoutMs + allAttendeesJoinedTimeoutMs + 1000);
137
+ // Setup
138
+ const { children, childErrorPromise } = await forkChildProcesses(
139
+ this.test?.title ?? "",
140
+ numClients,
141
+ afterCleanUp,
142
+ );
127
143
 
128
- // Setup
129
- const { children, childErrorPromise } = await forkChildProcesses(
130
- numClients,
131
- afterCleanUp,
132
- );
144
+ // Further Setup with Act and Verify
145
+ await connectAndWaitForAttendees(
146
+ children,
147
+ {
148
+ writeClients,
149
+ attendeeCountRequired: numClients - 1,
150
+ childConnectTimeoutMs,
151
+ allAttendeesJoinedTimeoutMs,
152
+ },
153
+ childErrorPromise,
154
+ );
155
+ });
156
+
157
+ it(`announces 'attendeeDisconnected' when remote client disconnects [${numClients} clients, ${writeClients} writers]`, async function testAnnouncesAttendeeDisconnected() {
158
+ if (useAzure && numClients > 50) {
159
+ // Even with increased timeouts, more than 50 clients can be too large for AFR.
160
+ // This may be due to slow responses/inactivity from the clients that are
161
+ // creating pressure on ADO agent.
162
+ this.skip();
163
+ }
133
164
 
134
- // Further Setup with Act and Verify
135
- await connectAndWaitForAttendees(
136
- children,
137
- {
165
+ const childDisconnectTimeoutMs = 10_000 * timeoutMultiplier;
166
+
167
+ setTestTimeout(
168
+ this,
169
+ childConnectTimeoutMs +
170
+ allAttendeesFullyJoinedTimeoutMs +
171
+ childDisconnectTimeoutMs +
172
+ 1000,
173
+ );
174
+
175
+ // Setup
176
+ const { children, childErrorPromise } = await forkChildProcesses(
177
+ this.test?.title ?? "",
178
+ numClients,
179
+ afterCleanUp,
180
+ );
181
+
182
+ const startConnectAndFullJoin = performance.now();
183
+ const connectResult = await connectAndListenForAttendees(children, {
138
184
  writeClients,
139
185
  attendeeCountRequired: numClients - 1,
140
186
  childConnectTimeoutMs,
141
- allAttendeesJoinedTimeoutMs,
142
- },
143
- childErrorPromise,
144
- );
145
- });
146
-
147
- // Even at 5 clients reaching fully connected state is unreliable with current implementation.
148
- it.skip(`announces 'attendeeDisconnected' when remote client disconnects [${numClients} clients, ${writeClients} writers]`, async function () {
149
- // TODO: AB#45620: "Presence: perf: update Join pattern for scale" can handle
150
- // larger counts of read-only attendees. Without protocol changes tests with
151
- // 20+ attendees exceed current limits.
152
- if (numClients >= 20 && writeClients === 1) {
153
- this.skip();
154
- }
155
-
156
- if (useAzure && numClients > 50) {
157
- // Even with increased timeouts, more than 50 clients can be too large for AFR.
158
- // This may be due to slow responses/inactivity from the clients that are
159
- // creating pressure on ADO agent.
160
- this.skip();
161
- }
162
-
163
- const childDisconnectTimeoutMs = 10_000 * timeoutMultiplier;
164
-
165
- setTestTimeout(
166
- this,
167
- childConnectTimeoutMs +
168
- allAttendeesFullyJoinedTimeoutMs +
169
- childDisconnectTimeoutMs +
170
- 1000,
171
- );
172
-
173
- // Setup
174
- const { children, childErrorPromise } = await forkChildProcesses(
175
- numClients,
176
- afterCleanUp,
177
- );
178
-
179
- const startConnectAndFullJoin = performance.now();
180
- const connectResult = await connectAndListenForAttendees(children, {
181
- writeClients,
182
- attendeeCountRequired: numClients - 1,
183
- childConnectTimeoutMs,
184
- });
185
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
186
- connectResult.attendeeCountRequiredPromises[0].then(() =>
187
- testConsole.log(
188
- `[${new Date().toISOString()}] All attendees joined per child 0 after ${performance.now() - startConnectAndFullJoin}ms`,
189
- ),
190
- );
187
+ });
188
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
189
+ connectResult.attendeeCountRequiredPromises[0].then(() =>
190
+ testConsole.log(
191
+ `[${new Date().toISOString()}] All attendees joined per child 0 after ${performance.now() - startConnectAndFullJoin}ms`,
192
+ ),
193
+ );
191
194
 
192
- // Wait for all attendees to be fully joined
193
- // Keep a tally for debuggability
194
- let childrenFullyJoined = 0;
195
- const setNotFullyJoined = new Set<number>();
196
- for (let i = 0; i < children.length; i++) {
197
- setNotFullyJoined.add(i);
198
- }
199
- const allAttendeesFullyJoined = Promise.all(
200
- connectResult.attendeeCountRequiredPromises.map(
201
- async (attendeeFullyJoinedPromise, index) => {
202
- await attendeeFullyJoinedPromise;
203
- childrenFullyJoined++;
204
- setNotFullyJoined.delete(index);
205
- },
206
- ),
207
- );
208
- let timedout = true;
209
- const allFullyJoinedOrChildError = Promise.race([
210
- allAttendeesFullyJoined,
211
- childErrorPromise,
212
- ]).finally(() => (timedout = false));
213
- await timeoutAwait(allFullyJoinedOrChildError, {
214
- durationMs: allAttendeesFullyJoinedTimeoutMs,
215
- errorMsg: "Not all attendees fully joined",
216
- }).catch(async (error) => {
217
- // Ideally this information would just be in the timeout error message, but that
218
- // must be a resolved string (not dynamic). So, just log it separately.
219
- testConsole.log(
220
- `[${new Date().toISOString()}] ${childrenFullyJoined} attendees fully joined before error...`,
195
+ // Wait for all attendees to be fully joined
196
+ // Keep a tally for debuggability
197
+ let childrenFullyJoined = 0;
198
+ const setNotFullyJoined = new Set<number>();
199
+ for (let i = 0; i < children.length; i++) {
200
+ setNotFullyJoined.add(i);
201
+ }
202
+ const allAttendeesFullyJoined = Promise.all(
203
+ connectResult.attendeeCountRequiredPromises.map(
204
+ async (attendeeFullyJoinedPromise, index) => {
205
+ await attendeeFullyJoinedPromise;
206
+ childrenFullyJoined++;
207
+ setNotFullyJoined.delete(index);
208
+ },
209
+ ),
221
210
  );
222
- if (timedout) {
223
- // Gather additional timing data if timed out to understand what increased
224
- // timeout could work. Test will still fail if this secondary wait succeeds.
225
- const startAdditionalWait = performance.now();
226
- try {
227
- await timeoutAwait(allFullyJoinedOrChildError, {
228
- durationMs: allAttendeesFullyJoinedTimeoutMs,
229
- });
230
- testConsole.log(
231
- `[${new Date().toISOString()}] All attendees fully joined after additional wait (${performance.now() - startAdditionalWait}ms)`,
232
- );
233
- } catch (secondaryError) {
234
- testConsole.log(
235
- `[${new Date().toISOString()}] Secondary await resulted in`,
236
- secondaryError,
237
- );
211
+ let timedout = true;
212
+ const allFullyJoinedOrChildError = Promise.race([
213
+ allAttendeesFullyJoined,
214
+ childErrorPromise,
215
+ ]).finally(() => (timedout = false));
216
+ await timeoutAwait(allFullyJoinedOrChildError, {
217
+ durationMs: allAttendeesFullyJoinedTimeoutMs,
218
+ errorMsg: "Not all attendees fully joined",
219
+ }).catch(async (error) => {
220
+ // Ideally this information would just be in the timeout error message, but that
221
+ // must be a resolved string (not dynamic). So, just log it separately.
222
+ testConsole.log(
223
+ `[${new Date().toISOString()}] ${childrenFullyJoined} attendees fully joined before error...`,
224
+ );
225
+ if (timedout) {
226
+ // Gather additional timing data if timed out to understand what increased
227
+ // timeout could work. Test will still fail if this secondary wait succeeds.
228
+ const startAdditionalWait = performance.now();
229
+ try {
230
+ await timeoutAwait(allFullyJoinedOrChildError, {
231
+ durationMs: allAttendeesFullyJoinedTimeoutMs,
232
+ });
233
+ testConsole.log(
234
+ `[${new Date().toISOString()}] All attendees fully joined after additional wait (${performance.now() - startAdditionalWait}ms)`,
235
+ );
236
+ } catch (secondaryError) {
237
+ testConsole.log(
238
+ `[${new Date().toISOString()}] Secondary await resulted in`,
239
+ secondaryError,
240
+ );
241
+ }
238
242
  }
239
- }
240
243
 
241
- // Gather and report debug info from children
242
- // If there are less than 10 children, get all reports.
243
- // Otherwise, just child 0 and those not fully joined.
244
- setTestTimeout(this, 0); // Disable test timeout. Will throw within 20s below.
245
- const childrenRequestedToReport =
246
- children.length <= 10
247
- ? children
248
- : // Just those not fully joined
249
- children.filter((_, index) => index === 0 || setNotFullyJoined.has(index));
250
- await timeoutAwait(
251
- Promise.race([executeDebugReports(childrenRequestedToReport), childErrorPromise]),
252
- { durationMs: 20_000, errorMsg: "Debug report timeout" },
253
- ).catch((debugAwaitError) => {
254
- testConsole.error("Debug report await resulted in", debugAwaitError);
255
- });
244
+ // Gather and report debug info from children
245
+ // If there are less than 10 children, get all reports.
246
+ // Otherwise, just child 0 and those not fully joined.
247
+ setTestTimeout(this, 0); // Disable test timeout. Will throw within 20s below.
248
+ const childrenRequestedToReport =
249
+ children.length <= 10
250
+ ? children
251
+ : // Just those not fully joined
252
+ children.filter((_, index) => index === 0 || setNotFullyJoined.has(index));
253
+ await timeoutAwait(
254
+ Promise.race([
255
+ executeDebugReports(childrenRequestedToReport),
256
+ childErrorPromise,
257
+ ]),
258
+ { durationMs: 20_000, errorMsg: "Debug report timeout" },
259
+ ).catch((debugAwaitError) => {
260
+ testConsole.error("Debug report await resulted in", debugAwaitError);
261
+ });
256
262
 
257
- throw error;
258
- });
259
- testConsole.log(
260
- `[${new Date().toISOString()}] All attendees fully joined after ${performance.now() - startConnectAndFullJoin}ms`,
261
- );
263
+ throw error;
264
+ });
265
+ testConsole.log(
266
+ `[${new Date().toISOString()}] All attendees fully joined after ${performance.now() - startConnectAndFullJoin}ms`,
267
+ );
262
268
 
263
- let child0ReportRequested = false;
264
- const waitForDisconnected = children.map(async (child, index) =>
265
- index === 0
266
- ? Promise.resolve()
267
- : timeoutPromise(
268
- (resolve) => {
269
- child.on("message", (msg: MessageFromChild) => {
270
- if (
271
- msg.event === "attendeeDisconnected" &&
272
- msg.attendeeId === connectResult.containerCreatorAttendeeId
273
- ) {
274
- console.log(`Child[${index}] saw creator disconnect`);
275
- resolve();
276
- }
269
+ let child0ReportRequested = false;
270
+ const waitForDisconnected = children.map(async (child, index) =>
271
+ index === 0
272
+ ? Promise.resolve()
273
+ : timeoutPromise(
274
+ (resolve) => {
275
+ child.on("message", (msg: MessageFromChild) => {
276
+ if (
277
+ msg.event === "attendeeDisconnected" &&
278
+ msg.attendeeId === connectResult.containerCreatorAttendeeId
279
+ ) {
280
+ console.log(`Child[${index}] saw creator disconnect`);
281
+ resolve();
282
+ }
283
+ });
284
+ },
285
+ {
286
+ durationMs: childDisconnectTimeoutMs,
287
+ errorMsg: `Attendee[${index}] Disconnected Timeout`,
288
+ },
289
+ ).catch(async (error) => {
290
+ const childrenRequestedToReport = [child];
291
+ if (!child0ReportRequested) {
292
+ childrenRequestedToReport.unshift(children[0]);
293
+ child0ReportRequested = true;
294
+ }
295
+ await timeoutAwait(
296
+ Promise.race([
297
+ executeDebugReports(childrenRequestedToReport),
298
+ childErrorPromise,
299
+ ]),
300
+ { durationMs: 20_000, errorMsg: "Debug report timeout" },
301
+ ).catch((debugAwaitError) => {
302
+ testConsole.error("Debug report await resulted in", debugAwaitError);
277
303
  });
278
- },
279
- {
280
- durationMs: childDisconnectTimeoutMs,
281
- errorMsg: `Attendee[${index}] Disconnected Timeout`,
282
- },
283
- ).catch(async (error) => {
284
- const childrenRequestedToReport = [child];
285
- if (!child0ReportRequested) {
286
- childrenRequestedToReport.unshift(children[0]);
287
- child0ReportRequested = true;
288
- }
289
- await timeoutAwait(
290
- Promise.race([
291
- executeDebugReports(childrenRequestedToReport),
292
- childErrorPromise,
293
- ]),
294
- { durationMs: 20_000, errorMsg: "Debug report timeout" },
295
- ).catch((debugAwaitError) => {
296
- testConsole.error("Debug report await resulted in", debugAwaitError);
297
- });
298
- throw error;
299
- }),
300
- );
304
+ throw error;
305
+ }),
306
+ );
301
307
 
302
- // Act - disconnect first child process
303
- children[0].send({ command: "disconnectSelf" });
308
+ // Act - disconnect first child process
309
+ children[0].send({ command: "disconnectSelf" });
304
310
 
305
- // Verify - wait for all 'attendeeDisconnected' events
306
- await Promise.race([Promise.all(waitForDisconnected), childErrorPromise]);
307
- });
311
+ // Verify - wait for all 'attendeeDisconnected' events
312
+ await Promise.race([Promise.all(waitForDisconnected), childErrorPromise]);
313
+ });
314
+ }
308
315
  }
309
- }
316
+ });
310
317
 
311
318
  {
312
319
  /**
@@ -330,82 +337,95 @@ describe(`Presence with AzureClient`, () => {
330
337
  /**
331
338
  * Timeout for child processes to connect to container ({@link ConnectedEvent})
332
339
  */
333
- const childConnectTimeoutMs = 1000 * numClients * timeoutMultiplier;
334
-
335
- let children: ChildProcess[];
336
- let childErrorPromise: Promise<never>;
337
- let containerCreatorAttendeeId: AttendeeId;
338
- let attendeeIdPromises: Promise<AttendeeId>[];
339
- let remoteClients: ChildProcess[];
340
- const testValue = "testValue";
341
- const workspaceId = "presenceTestWorkspace";
342
-
343
- beforeEach(async () => {
344
- ({ children, childErrorPromise } = await forkChildProcesses(
345
- numClients,
346
- afterCleanUp,
347
- ));
348
- ({ containerCreatorAttendeeId, attendeeIdPromises } = await connectChildProcesses(
349
- children,
350
- { writeClients: numClients, readyTimeoutMs: childConnectTimeoutMs },
351
- ));
352
- await Promise.all(attendeeIdPromises);
353
- remoteClients = children.filter((_, index) => index !== 0);
354
- // NOTE: For testing purposes child clients will expect a Latest value of type string (StateFactory.latest<{ value: string }>).
355
- await registerWorkspaceOnChildren(children, workspaceId, {
356
- latest: true,
357
- timeoutMs: workspaceRegisterTimeoutMs,
358
- });
359
- });
360
-
361
- it(`allows clients to read Latest state from other clients [${numClients} clients]`, async function () {
362
- // AB#48866: Fix intermittently failing presence tests
363
- if (useAzure) {
364
- this.skip();
365
- }
366
- // Setup
367
- const updateEventsPromise = waitForLatestValueUpdates(
368
- remoteClients,
369
- workspaceId,
370
- childErrorPromise,
371
- stateUpdateTimeoutMs,
372
- { fromAttendeeId: containerCreatorAttendeeId, expectedValue: testValue },
373
- );
340
+ const childConnectTimeoutMs = (4000 + 1000 * numClients) * timeoutMultiplier;
341
+ const testCaseTimeoutMs = 1000;
342
+ const testSetupAndActTimeoutMs = childConnectTimeoutMs + testCaseTimeoutMs;
343
+
344
+ // These tests use beforeEach to setup complex state that takes a lot of time
345
+ // and is dependent on number of clients. Keeping the work in beforeEach
346
+ // allows time reporting to report the tested scenario apart from the setup time.
347
+ // So this describe block isolates those beforeEach setups from each distinct
348
+ // client count. Test cases descriptions also have the client count for clarity.
349
+ describe(`with ${numClients} clients`, () => {
350
+ let children: ChildProcess[];
351
+ let childErrorPromise: Promise<never>;
352
+ let containerCreatorAttendeeId: AttendeeId;
353
+ let attendeeIdPromises: Promise<AttendeeId>[];
354
+ let remoteClients: ChildProcess[];
355
+ const testValue = "testValue";
356
+ const workspaceId = "presenceTestWorkspace";
357
+
358
+ beforeEach(async function usingLatestStateObject_beforeEach(): Promise<void> {
359
+ const startTime = performance.now();
360
+ setTestTimeout(this, testSetupAndActTimeoutMs);
361
+
362
+ ({ children, childErrorPromise } = await forkChildProcesses(
363
+ this.currentTest?.title ?? "",
364
+ numClients,
365
+ afterCleanUp,
366
+ ));
367
+ ({ containerCreatorAttendeeId, attendeeIdPromises } = await connectChildProcesses(
368
+ children,
369
+ { writeClients: numClients, readyTimeoutMs: childConnectTimeoutMs },
370
+ ));
371
+ await Promise.all(attendeeIdPromises);
372
+ remoteClients = children.filter((_, index) => index !== 0);
373
+ // NOTE: For testing purposes child clients will expect a Latest value of type string (StateFactory.latest<{ value: string }>).
374
+ await registerWorkspaceOnChildren(children, workspaceId, {
375
+ latest: true,
376
+ timeoutMs: workspaceRegisterTimeoutMs,
377
+ });
374
378
 
375
- // Act - Trigger the update
376
- children[0].send({
377
- command: "setLatestValue",
378
- workspaceId,
379
- value: testValue,
379
+ testConsole.log(
380
+ ` Setup for "${this.currentTest?.title}" completed in ${performance.now() - startTime}ms`,
381
+ );
380
382
  });
381
- const updateEvents = await updateEventsPromise;
382
383
 
383
- // Verify all events are from the expected attendee
384
- for (const updateEvent of updateEvents) {
385
- assert.strictEqual(updateEvent.attendeeId, containerCreatorAttendeeId);
386
- assert.deepStrictEqual(updateEvent.value, testValue);
387
- }
388
-
389
- // Act - Request each remote client to read latest state from container creator
390
- for (const child of remoteClients) {
391
- child.send({
392
- command: "getLatestValue",
384
+ it(`allows clients to read Latest state from other clients [${numClients} clients]`, async function () {
385
+ // Setup
386
+ const updateEventsPromise = waitForLatestValueUpdates(
387
+ remoteClients,
388
+ workspaceId,
389
+ childErrorPromise,
390
+ stateUpdateTimeoutMs,
391
+ { fromAttendeeId: containerCreatorAttendeeId, expectedValue: testValue },
392
+ );
393
+
394
+ // Act - Trigger the update
395
+ children[0].send({
396
+ command: "setLatestValue",
393
397
  workspaceId,
394
- attendeeId: containerCreatorAttendeeId,
398
+ value: testValue,
395
399
  });
396
- }
400
+ const updateEvents = await updateEventsPromise;
397
401
 
398
- const getResponses = await getLatestValueResponses(
399
- remoteClients,
400
- workspaceId,
401
- childErrorPromise,
402
- getStateTimeoutMs,
403
- );
402
+ // Verify all events are from the expected attendee
403
+ for (const updateEvent of updateEvents) {
404
+ assert.strictEqual(updateEvent.attendeeId, containerCreatorAttendeeId);
405
+ assert.deepStrictEqual(updateEvent.value, testValue);
406
+ }
404
407
 
405
- // Verify - all responses should contain the expected value
406
- for (const getResponse of getResponses) {
407
- assert.deepStrictEqual(getResponse.value, testValue);
408
- }
408
+ // Act - Request each remote client to read latest state from container creator
409
+ for (const child of remoteClients) {
410
+ child.send({
411
+ command: "getLatestValue",
412
+ workspaceId,
413
+ attendeeId: containerCreatorAttendeeId,
414
+ });
415
+ }
416
+
417
+ const getResponses = await getLatestValueResponses(
418
+ remoteClients,
419
+ workspaceId,
420
+ childErrorPromise,
421
+ getStateTimeoutMs,
422
+ );
423
+
424
+ // Verify - all responses should contain the expected value
425
+ for (const getResponse of getResponses) {
426
+ assert.deepStrictEqual(getResponse.value, testValue);
427
+ }
428
+ });
409
429
  });
410
430
  }
411
431
  });
@@ -418,197 +438,211 @@ describe(`Presence with AzureClient`, () => {
418
438
  /**
419
439
  * Timeout for child processes to connect to container ({@link ConnectedEvent})
420
440
  */
421
- const childConnectTimeoutMs = 1000 * numClients * timeoutMultiplier;
422
-
423
- let children: ChildProcess[];
424
- let childErrorPromise: Promise<never>;
425
- let containerCreatorAttendeeId: AttendeeId;
426
- let attendeeIdPromises: Promise<AttendeeId>[];
427
- let remoteClients: ChildProcess[];
428
- const workspaceId = "presenceTestWorkspace";
429
- const key1 = "player1";
430
- const key2 = "player2";
431
- const value1 = { name: "Alice", score: 100 };
432
- const value2 = { name: "Bob", score: 200 };
433
-
434
- beforeEach(async () => {
435
- ({ children, childErrorPromise } = await forkChildProcesses(
436
- numClients,
437
- afterCleanUp,
438
- ));
439
- ({ containerCreatorAttendeeId, attendeeIdPromises } = await connectChildProcesses(
440
- children,
441
- { writeClients: numClients, readyTimeoutMs: childConnectTimeoutMs },
442
- ));
443
- await Promise.all(attendeeIdPromises);
444
- remoteClients = children.filter((_, index) => index !== 0);
445
- // NOTE: For testing purposes child clients will expect a LatestMap value of type Record<string, string | number> (StateFactory.latestMap<{ value: Record<string, string | number> }, string>).
446
- await registerWorkspaceOnChildren(children, workspaceId, {
447
- latestMap: true,
448
- timeoutMs: workspaceRegisterTimeoutMs,
449
- });
450
- });
451
-
452
- it(`allows clients to read LatestMap values from other clients [${numClients} clients]`, async () => {
453
- // Setup
454
- const testKey = "cursor";
455
- const testValue = { x: 150, y: 300 };
456
- const updateEventsPromise = waitForLatestMapValueUpdates(
457
- remoteClients,
458
- workspaceId,
459
- testKey,
460
- childErrorPromise,
461
- stateUpdateTimeoutMs,
462
- { fromAttendeeId: containerCreatorAttendeeId, expectedValue: testValue },
463
- );
441
+ const childConnectTimeoutMs = (4000 + 1000 * numClients) * timeoutMultiplier;
442
+ const testCaseTimeoutMs = 1000;
443
+ const testSetupAndActTimeoutMs = childConnectTimeoutMs + testCaseTimeoutMs;
444
+
445
+ // These tests use beforeEach to setup complex state that takes a lot of time
446
+ // and is dependent on number of clients. Keeping the work in beforeEach
447
+ // allows time reporting to report the tested scenario apart from the setup time.
448
+ // So this describe block isolates those beforeEach setups from each distinct
449
+ // client count. Test cases descriptions also have the client count for clarity.
450
+ describe(`with ${numClients} clients`, () => {
451
+ let children: ChildProcess[];
452
+ let childErrorPromise: Promise<never>;
453
+ let containerCreatorAttendeeId: AttendeeId;
454
+ let attendeeIdPromises: Promise<AttendeeId>[];
455
+ let remoteClients: ChildProcess[];
456
+ const workspaceId = "presenceTestWorkspace";
457
+ const key1 = "player1";
458
+ const key2 = "player2";
459
+ const value1 = { name: "Alice", score: 100 };
460
+ const value2 = { name: "Bob", score: 200 };
461
+
462
+ beforeEach(async function usingLatestMapStateObject_beforeEach(): Promise<void> {
463
+ const startTime = performance.now();
464
+
465
+ setTestTimeout(this, testSetupAndActTimeoutMs);
466
+
467
+ ({ children, childErrorPromise } = await forkChildProcesses(
468
+ this.currentTest?.title ?? "",
469
+ numClients,
470
+ afterCleanUp,
471
+ ));
472
+ ({ containerCreatorAttendeeId, attendeeIdPromises } = await connectChildProcesses(
473
+ children,
474
+ { writeClients: numClients, readyTimeoutMs: childConnectTimeoutMs },
475
+ ));
476
+ await Promise.all(attendeeIdPromises);
477
+ remoteClients = children.filter((_, index) => index !== 0);
478
+ // NOTE: For testing purposes child clients will expect a LatestMap value of type Record<string, string | number> (StateFactory.latestMap<{ value: Record<string, string | number> }, string>).
479
+ await registerWorkspaceOnChildren(children, workspaceId, {
480
+ latestMap: true,
481
+ timeoutMs: workspaceRegisterTimeoutMs,
482
+ });
464
483
 
465
- // Act
466
- children[0].send({
467
- command: "setLatestMapValue",
468
- workspaceId,
469
- key: testKey,
470
- value: testValue,
484
+ testConsole.log(
485
+ ` Setup for "${this.currentTest?.title}" completed in ${performance.now() - startTime}ms`,
486
+ );
471
487
  });
472
- const updateEvents = await updateEventsPromise;
473
488
 
474
- // Check all events are from the expected attendee
475
- for (const updateEvent of updateEvents) {
476
- assert.strictEqual(updateEvent.attendeeId, containerCreatorAttendeeId);
477
- assert.strictEqual(updateEvent.key, testKey);
478
- assert.deepStrictEqual(updateEvent.value, testValue);
479
- }
480
-
481
- for (const child of remoteClients) {
482
- child.send({
483
- command: "getLatestMapValue",
489
+ it(`allows clients to read LatestMap values from other clients [${numClients} clients]`, async () => {
490
+ // Setup
491
+ const testKey = "cursor";
492
+ const testValue = { x: 150, y: 300 };
493
+ const updateEventsPromise = waitForLatestMapValueUpdates(
494
+ remoteClients,
495
+ workspaceId,
496
+ testKey,
497
+ childErrorPromise,
498
+ stateUpdateTimeoutMs,
499
+ { fromAttendeeId: containerCreatorAttendeeId, expectedValue: testValue },
500
+ );
501
+
502
+ // Act
503
+ children[0].send({
504
+ command: "setLatestMapValue",
484
505
  workspaceId,
485
506
  key: testKey,
486
- attendeeId: containerCreatorAttendeeId,
507
+ value: testValue,
487
508
  });
488
- }
489
- const getResponses = await getLatestMapValueResponses(
490
- remoteClients,
491
- workspaceId,
492
- testKey,
493
- childErrorPromise,
494
- getStateTimeoutMs,
495
- );
509
+ const updateEvents = await updateEventsPromise;
496
510
 
497
- // Verify
498
- for (const getResponse of getResponses) {
499
- assert.deepStrictEqual(getResponse.value, testValue);
500
- }
501
- });
502
-
503
- it(`returns per-key values on read [${numClients} clients]`, async function () {
504
- // AB#48866: Fix intermittently failing presence tests
505
- if (useAzure) {
506
- this.skip();
507
- }
508
- // Setup
509
- const allAttendeeIds = await Promise.all(attendeeIdPromises);
510
- const attendee0Id = containerCreatorAttendeeId;
511
- const attendee1Id = allAttendeeIds[1];
512
-
513
- const key1Recipients = children.filter((_, index) => index !== 0);
514
- const key2Recipients = children.filter((_, index) => index !== 1);
515
- const key1UpdateEventsPromise = waitForLatestMapValueUpdates(
516
- key1Recipients,
517
- workspaceId,
518
- key1,
519
- childErrorPromise,
520
- stateUpdateTimeoutMs,
521
- { fromAttendeeId: attendee0Id, expectedValue: value1 },
522
- );
523
- const key2UpdateEventsPromise = waitForLatestMapValueUpdates(
524
- key2Recipients,
525
- workspaceId,
526
- key2,
527
- childErrorPromise,
528
- stateUpdateTimeoutMs,
529
- { fromAttendeeId: attendee1Id, expectedValue: value2 },
530
- );
511
+ // Check all events are from the expected attendee
512
+ for (const updateEvent of updateEvents) {
513
+ assert.strictEqual(updateEvent.attendeeId, containerCreatorAttendeeId);
514
+ assert.strictEqual(updateEvent.key, testKey);
515
+ assert.deepStrictEqual(updateEvent.value, testValue);
516
+ }
531
517
 
532
- // Act
533
- children[0].send({
534
- command: "setLatestMapValue",
535
- workspaceId,
536
- key: key1,
537
- value: value1,
538
- });
539
- const key1UpdateEvents = await key1UpdateEventsPromise;
540
- children[1].send({
541
- command: "setLatestMapValue",
542
- workspaceId,
543
- key: key2,
544
- value: value2,
518
+ for (const child of remoteClients) {
519
+ child.send({
520
+ command: "getLatestMapValue",
521
+ workspaceId,
522
+ key: testKey,
523
+ attendeeId: containerCreatorAttendeeId,
524
+ });
525
+ }
526
+ const getResponses = await getLatestMapValueResponses(
527
+ remoteClients,
528
+ workspaceId,
529
+ testKey,
530
+ childErrorPromise,
531
+ getStateTimeoutMs,
532
+ );
533
+
534
+ // Verify
535
+ for (const getResponse of getResponses) {
536
+ assert.deepStrictEqual(getResponse.value, testValue);
537
+ }
545
538
  });
546
- const key2UpdateEvents = await key2UpdateEventsPromise;
547
539
 
548
- // Verify all events are from the expected attendees
549
- for (const updateEvent of key1UpdateEvents) {
550
- assert.strictEqual(updateEvent.attendeeId, attendee0Id);
551
- assert.strictEqual(updateEvent.key, key1);
552
- assert.deepStrictEqual(updateEvent.value, value1);
553
- }
554
- for (const updateEvent of key2UpdateEvents) {
555
- assert.strictEqual(updateEvent.attendeeId, attendee1Id);
556
- assert.strictEqual(updateEvent.key, key2);
557
- assert.deepStrictEqual(updateEvent.value, value2);
558
- }
540
+ it(`returns per-key values on read [${numClients} clients]`, async function () {
541
+ // Setup
542
+ const allAttendeeIds = await Promise.all(attendeeIdPromises);
543
+ const attendee0Id = containerCreatorAttendeeId;
544
+ const attendee1Id = allAttendeeIds[1];
559
545
 
560
- // Read key1 of attendee0 from all children
561
- for (const child of children) {
562
- child.send({
563
- command: "getLatestMapValue",
546
+ const key1Recipients = children.filter((_, index) => index !== 0);
547
+ const key2Recipients = children.filter((_, index) => index !== 1);
548
+ const key1UpdateEventsPromise = waitForLatestMapValueUpdates(
549
+ key1Recipients,
550
+ workspaceId,
551
+ key1,
552
+ childErrorPromise,
553
+ stateUpdateTimeoutMs,
554
+ { fromAttendeeId: attendee0Id, expectedValue: value1 },
555
+ );
556
+ const key2UpdateEventsPromise = waitForLatestMapValueUpdates(
557
+ key2Recipients,
558
+ workspaceId,
559
+ key2,
560
+ childErrorPromise,
561
+ stateUpdateTimeoutMs,
562
+ { fromAttendeeId: attendee1Id, expectedValue: value2 },
563
+ );
564
+
565
+ // Act
566
+ children[0].send({
567
+ command: "setLatestMapValue",
564
568
  workspaceId,
565
569
  key: key1,
566
- attendeeId: attendee0Id,
570
+ value: value1,
567
571
  });
568
- }
569
- const key1Responses = await getLatestMapValueResponses(
570
- children,
571
- workspaceId,
572
- key1,
573
- childErrorPromise,
574
- getStateTimeoutMs,
575
- );
576
-
577
- // Read key2 of attendee1 from all children
578
- for (const child of children) {
579
- child.send({
580
- command: "getLatestMapValue",
572
+ const key1UpdateEvents = await key1UpdateEventsPromise;
573
+ children[1].send({
574
+ command: "setLatestMapValue",
581
575
  workspaceId,
582
576
  key: key2,
583
- attendeeId: attendee1Id,
577
+ value: value2,
584
578
  });
585
- }
586
- const key2Responses = await getLatestMapValueResponses(
587
- children,
588
- workspaceId,
589
- key2,
590
- childErrorPromise,
591
- getStateTimeoutMs,
592
- );
579
+ const key2UpdateEvents = await key2UpdateEventsPromise;
593
580
 
594
- // Verify
595
- assert.strictEqual(
596
- key1Responses.length,
597
- numClients,
598
- "Expected responses from all clients for key1",
599
- );
600
- assert.strictEqual(
601
- key2Responses.length,
602
- numClients,
603
- "Expected responses from all clients for key2",
604
- );
581
+ // Verify all events are from the expected attendees
582
+ for (const updateEvent of key1UpdateEvents) {
583
+ assert.strictEqual(updateEvent.attendeeId, attendee0Id);
584
+ assert.strictEqual(updateEvent.key, key1);
585
+ assert.deepStrictEqual(updateEvent.value, value1);
586
+ }
587
+ for (const updateEvent of key2UpdateEvents) {
588
+ assert.strictEqual(updateEvent.attendeeId, attendee1Id);
589
+ assert.strictEqual(updateEvent.key, key2);
590
+ assert.deepStrictEqual(updateEvent.value, value2);
591
+ }
605
592
 
606
- for (const response of key1Responses) {
607
- assert.deepStrictEqual(response.value, value1, "Key1 value should match");
608
- }
609
- for (const response of key2Responses) {
610
- assert.deepStrictEqual(response.value, value2, "Key2 value should match");
611
- }
593
+ // Read key1 of attendee0 from all children
594
+ for (const child of children) {
595
+ child.send({
596
+ command: "getLatestMapValue",
597
+ workspaceId,
598
+ key: key1,
599
+ attendeeId: attendee0Id,
600
+ });
601
+ }
602
+ const key1Responses = await getLatestMapValueResponses(
603
+ children,
604
+ workspaceId,
605
+ key1,
606
+ childErrorPromise,
607
+ getStateTimeoutMs,
608
+ );
609
+
610
+ // Read key2 of attendee1 from all children
611
+ for (const child of children) {
612
+ child.send({
613
+ command: "getLatestMapValue",
614
+ workspaceId,
615
+ key: key2,
616
+ attendeeId: attendee1Id,
617
+ });
618
+ }
619
+ const key2Responses = await getLatestMapValueResponses(
620
+ children,
621
+ workspaceId,
622
+ key2,
623
+ childErrorPromise,
624
+ getStateTimeoutMs,
625
+ );
626
+
627
+ // Verify
628
+ assert.strictEqual(
629
+ key1Responses.length,
630
+ numClients,
631
+ "Expected responses from all clients for key1",
632
+ );
633
+ assert.strictEqual(
634
+ key2Responses.length,
635
+ numClients,
636
+ "Expected responses from all clients for key2",
637
+ );
638
+
639
+ for (const response of key1Responses) {
640
+ assert.deepStrictEqual(response.value, value1, "Key1 value should match");
641
+ }
642
+ for (const response of key2Responses) {
643
+ assert.deepStrictEqual(response.value, value2, "Key2 value should match");
644
+ }
645
+ });
612
646
  });
613
647
  }
614
648
  });