@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.
@@ -6,6 +6,13 @@ import { strict as assert } from "node:assert";
6
6
  import inspector from "node:inspector";
7
7
  import { timeoutAwait, timeoutPromise } from "@fluidframework/test-utils/internal";
8
8
  import { connectAndListenForAttendees, connectAndWaitForAttendees, connectChildProcesses, executeDebugReports, forkChildProcesses, getLatestMapValueResponses, getLatestValueResponses, registerWorkspaceOnChildren, testConsole, waitForLatestMapValueUpdates, waitForLatestValueUpdates, } from "./orchestratorUtils.js";
9
+ /**
10
+ * When true, slower (long running time) tests will be run.
11
+ * Otherwise, those test will not appear. Console output is used to show that
12
+ * they exist. (They could be skipped, though skipped test are often an
13
+ * indication of a problem.)
14
+ */
15
+ const shouldRunScaleTests = process.env.FLUID_TEST_SCALE !== undefined;
9
16
  const useAzure = process.env.FLUID_CLIENT === "azure";
10
17
  /**
11
18
  * Detects if the debugger is attached (when code loaded).
@@ -14,7 +21,7 @@ const debuggerAttached = inspector.url() !== undefined;
14
21
  /**
15
22
  * Set this to a high number when debugging to avoid timeouts from debugging time.
16
23
  */
17
- const timeoutMultiplier = debuggerAttached ? 1000 : useAzure ? 3 : 1;
24
+ const timeoutMultiplier = debuggerAttached ? 1000 : useAzure ? 5 : 1;
18
25
  /**
19
26
  * Sets the timeout for the given test context.
20
27
  *
@@ -69,141 +76,107 @@ describe(`Presence with AzureClient`, () => {
69
76
  }
70
77
  afterCleanUp.length = 0;
71
78
  });
72
- // Note that on slower systems 50+ clients may take too long to join.
73
- const numClientsForAttendeeTests = [5, 20, 50, 100];
74
- // TODO: AB#45620: "Presence: perf: update Join pattern for scale" may help, then remove .slice.
75
- for (const numClients of numClientsForAttendeeTests.slice(0, 2)) {
76
- assert(numClients > 1, "Must have at least two clients");
77
- /**
78
- * Timeout for child processes to connect to container ({@link ConnectedEvent})
79
- */
80
- const childConnectTimeoutMs = 1000 * numClients * timeoutMultiplier;
81
- /**
82
- * Timeout for presence attendees to join per first child perspective {@link AttendeeConnectedEvent}
83
- */
84
- const allAttendeesJoinedTimeoutMs = (1000 + 200 * numClients) * timeoutMultiplier;
85
- /**
86
- * Timeout for presence attendees to fully join (everyone knows about everyone) {@link AttendeeConnectedEvent}
87
- */
88
- const allAttendeesFullyJoinedTimeoutMs = (2000 + 300 * numClients) * timeoutMultiplier;
89
- for (const writeClients of [numClients, 1]) {
90
- it(`announces 'attendeeConnected' when remote client joins session [${numClients} clients, ${writeClients} writers]`, async function () {
91
- // AB#48866: Fix intermittently failing presence tests
92
- if (useAzure) {
93
- this.skip();
94
- }
95
- setTestTimeout(this, childConnectTimeoutMs + allAttendeesJoinedTimeoutMs + 1000);
96
- // Setup
97
- const { children, childErrorPromise } = await forkChildProcesses(numClients, afterCleanUp);
98
- // Further Setup with Act and Verify
99
- await connectAndWaitForAttendees(children, {
100
- writeClients,
101
- attendeeCountRequired: numClients - 1,
102
- childConnectTimeoutMs,
103
- allAttendeesJoinedTimeoutMs,
104
- }, childErrorPromise);
105
- });
106
- // Even at 5 clients reaching fully connected state is unreliable with current implementation.
107
- it.skip(`announces 'attendeeDisconnected' when remote client disconnects [${numClients} clients, ${writeClients} writers]`, async function () {
108
- // TODO: AB#45620: "Presence: perf: update Join pattern for scale" can handle
109
- // larger counts of read-only attendees. Without protocol changes tests with
110
- // 20+ attendees exceed current limits.
111
- if (numClients >= 20 && writeClients === 1) {
112
- this.skip();
113
- }
114
- if (useAzure && numClients > 50) {
115
- // Even with increased timeouts, more than 50 clients can be too large for AFR.
116
- // This may be due to slow responses/inactivity from the clients that are
117
- // creating pressure on ADO agent.
118
- this.skip();
119
- }
120
- const childDisconnectTimeoutMs = 10_000 * timeoutMultiplier;
121
- setTestTimeout(this, childConnectTimeoutMs +
122
- allAttendeesFullyJoinedTimeoutMs +
123
- childDisconnectTimeoutMs +
124
- 1000);
125
- // Setup
126
- const { children, childErrorPromise } = await forkChildProcesses(numClients, afterCleanUp);
127
- const startConnectAndFullJoin = performance.now();
128
- const connectResult = await connectAndListenForAttendees(children, {
129
- writeClients,
130
- attendeeCountRequired: numClients - 1,
131
- childConnectTimeoutMs,
79
+ describe("`attendees` support", () => {
80
+ const numClientsForAttendeeTests = [5, 40, 100, 250];
81
+ for (const numClients of numClientsForAttendeeTests) {
82
+ if (numClients > 50 && !shouldRunScaleTests) {
83
+ testConsole.log(`skipping Presence attendee scale tests with ${numClients} clients (set FLUID_TEST_SCALE=true to run)`);
84
+ continue;
85
+ }
86
+ assert(numClients > 1, "Must have at least two clients");
87
+ /**
88
+ * Timeout for child processes to connect to container ({@link ConnectedEvent})
89
+ */
90
+ const childConnectTimeoutMs = 1000 * numClients * timeoutMultiplier;
91
+ /**
92
+ * Timeout for presence attendees to join per first child perspective {@link AttendeeConnectedEvent}
93
+ */
94
+ const allAttendeesJoinedTimeoutMs = (1000 + 200 * numClients) * timeoutMultiplier;
95
+ /**
96
+ * Timeout for presence attendees to fully join (everyone knows about everyone) {@link AttendeeConnectedEvent}
97
+ */
98
+ const allAttendeesFullyJoinedTimeoutMs = (2000 + 300 * numClients) * timeoutMultiplier;
99
+ for (const writeClients of [numClients, 1]) {
100
+ it(`announces 'attendeeConnected' when remote client joins session [${numClients} clients, ${writeClients} writers]`, async function testAnnouncesAttendeeConnected() {
101
+ setTestTimeout(this, childConnectTimeoutMs + allAttendeesJoinedTimeoutMs + 1000);
102
+ // Setup
103
+ const { children, childErrorPromise } = await forkChildProcesses(this.test?.title ?? "", numClients, afterCleanUp);
104
+ // Further Setup with Act and Verify
105
+ await connectAndWaitForAttendees(children, {
106
+ writeClients,
107
+ attendeeCountRequired: numClients - 1,
108
+ childConnectTimeoutMs,
109
+ allAttendeesJoinedTimeoutMs,
110
+ }, childErrorPromise);
132
111
  });
133
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
134
- connectResult.attendeeCountRequiredPromises[0].then(() => testConsole.log(`[${new Date().toISOString()}] All attendees joined per child 0 after ${performance.now() - startConnectAndFullJoin}ms`));
135
- // Wait for all attendees to be fully joined
136
- // Keep a tally for debuggability
137
- let childrenFullyJoined = 0;
138
- const setNotFullyJoined = new Set();
139
- for (let i = 0; i < children.length; i++) {
140
- setNotFullyJoined.add(i);
141
- }
142
- const allAttendeesFullyJoined = Promise.all(connectResult.attendeeCountRequiredPromises.map(async (attendeeFullyJoinedPromise, index) => {
143
- await attendeeFullyJoinedPromise;
144
- childrenFullyJoined++;
145
- setNotFullyJoined.delete(index);
146
- }));
147
- let timedout = true;
148
- const allFullyJoinedOrChildError = Promise.race([
149
- allAttendeesFullyJoined,
150
- childErrorPromise,
151
- ]).finally(() => (timedout = false));
152
- await timeoutAwait(allFullyJoinedOrChildError, {
153
- durationMs: allAttendeesFullyJoinedTimeoutMs,
154
- errorMsg: "Not all attendees fully joined",
155
- }).catch(async (error) => {
156
- // Ideally this information would just be in the timeout error message, but that
157
- // must be a resolved string (not dynamic). So, just log it separately.
158
- testConsole.log(`[${new Date().toISOString()}] ${childrenFullyJoined} attendees fully joined before error...`);
159
- if (timedout) {
160
- // Gather additional timing data if timed out to understand what increased
161
- // timeout could work. Test will still fail if this secondary wait succeeds.
162
- const startAdditionalWait = performance.now();
163
- try {
164
- await timeoutAwait(allFullyJoinedOrChildError, {
165
- durationMs: allAttendeesFullyJoinedTimeoutMs,
166
- });
167
- testConsole.log(`[${new Date().toISOString()}] All attendees fully joined after additional wait (${performance.now() - startAdditionalWait}ms)`);
168
- }
169
- catch (secondaryError) {
170
- testConsole.log(`[${new Date().toISOString()}] Secondary await resulted in`, secondaryError);
171
- }
112
+ it(`announces 'attendeeDisconnected' when remote client disconnects [${numClients} clients, ${writeClients} writers]`, async function testAnnouncesAttendeeDisconnected() {
113
+ if (useAzure && numClients > 50) {
114
+ // Even with increased timeouts, more than 50 clients can be too large for AFR.
115
+ // This may be due to slow responses/inactivity from the clients that are
116
+ // creating pressure on ADO agent.
117
+ this.skip();
172
118
  }
173
- // Gather and report debug info from children
174
- // If there are less than 10 children, get all reports.
175
- // Otherwise, just child 0 and those not fully joined.
176
- setTestTimeout(this, 0); // Disable test timeout. Will throw within 20s below.
177
- const childrenRequestedToReport = children.length <= 10
178
- ? children
179
- : // Just those not fully joined
180
- children.filter((_, index) => index === 0 || setNotFullyJoined.has(index));
181
- await timeoutAwait(Promise.race([executeDebugReports(childrenRequestedToReport), childErrorPromise]), { durationMs: 20_000, errorMsg: "Debug report timeout" }).catch((debugAwaitError) => {
182
- testConsole.error("Debug report await resulted in", debugAwaitError);
119
+ const childDisconnectTimeoutMs = 10_000 * timeoutMultiplier;
120
+ setTestTimeout(this, childConnectTimeoutMs +
121
+ allAttendeesFullyJoinedTimeoutMs +
122
+ childDisconnectTimeoutMs +
123
+ 1000);
124
+ // Setup
125
+ const { children, childErrorPromise } = await forkChildProcesses(this.test?.title ?? "", numClients, afterCleanUp);
126
+ const startConnectAndFullJoin = performance.now();
127
+ const connectResult = await connectAndListenForAttendees(children, {
128
+ writeClients,
129
+ attendeeCountRequired: numClients - 1,
130
+ childConnectTimeoutMs,
183
131
  });
184
- throw error;
185
- });
186
- testConsole.log(`[${new Date().toISOString()}] All attendees fully joined after ${performance.now() - startConnectAndFullJoin}ms`);
187
- let child0ReportRequested = false;
188
- const waitForDisconnected = children.map(async (child, index) => index === 0
189
- ? Promise.resolve()
190
- : timeoutPromise((resolve) => {
191
- child.on("message", (msg) => {
192
- if (msg.event === "attendeeDisconnected" &&
193
- msg.attendeeId === connectResult.containerCreatorAttendeeId) {
194
- console.log(`Child[${index}] saw creator disconnect`);
195
- resolve();
196
- }
197
- });
198
- }, {
199
- durationMs: childDisconnectTimeoutMs,
200
- errorMsg: `Attendee[${index}] Disconnected Timeout`,
132
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
133
+ connectResult.attendeeCountRequiredPromises[0].then(() => testConsole.log(`[${new Date().toISOString()}] All attendees joined per child 0 after ${performance.now() - startConnectAndFullJoin}ms`));
134
+ // Wait for all attendees to be fully joined
135
+ // Keep a tally for debuggability
136
+ let childrenFullyJoined = 0;
137
+ const setNotFullyJoined = new Set();
138
+ for (let i = 0; i < children.length; i++) {
139
+ setNotFullyJoined.add(i);
140
+ }
141
+ const allAttendeesFullyJoined = Promise.all(connectResult.attendeeCountRequiredPromises.map(async (attendeeFullyJoinedPromise, index) => {
142
+ await attendeeFullyJoinedPromise;
143
+ childrenFullyJoined++;
144
+ setNotFullyJoined.delete(index);
145
+ }));
146
+ let timedout = true;
147
+ const allFullyJoinedOrChildError = Promise.race([
148
+ allAttendeesFullyJoined,
149
+ childErrorPromise,
150
+ ]).finally(() => (timedout = false));
151
+ await timeoutAwait(allFullyJoinedOrChildError, {
152
+ durationMs: allAttendeesFullyJoinedTimeoutMs,
153
+ errorMsg: "Not all attendees fully joined",
201
154
  }).catch(async (error) => {
202
- const childrenRequestedToReport = [child];
203
- if (!child0ReportRequested) {
204
- childrenRequestedToReport.unshift(children[0]);
205
- child0ReportRequested = true;
155
+ // Ideally this information would just be in the timeout error message, but that
156
+ // must be a resolved string (not dynamic). So, just log it separately.
157
+ testConsole.log(`[${new Date().toISOString()}] ${childrenFullyJoined} attendees fully joined before error...`);
158
+ if (timedout) {
159
+ // Gather additional timing data if timed out to understand what increased
160
+ // timeout could work. Test will still fail if this secondary wait succeeds.
161
+ const startAdditionalWait = performance.now();
162
+ try {
163
+ await timeoutAwait(allFullyJoinedOrChildError, {
164
+ durationMs: allAttendeesFullyJoinedTimeoutMs,
165
+ });
166
+ testConsole.log(`[${new Date().toISOString()}] All attendees fully joined after additional wait (${performance.now() - startAdditionalWait}ms)`);
167
+ }
168
+ catch (secondaryError) {
169
+ testConsole.log(`[${new Date().toISOString()}] Secondary await resulted in`, secondaryError);
170
+ }
206
171
  }
172
+ // Gather and report debug info from children
173
+ // If there are less than 10 children, get all reports.
174
+ // Otherwise, just child 0 and those not fully joined.
175
+ setTestTimeout(this, 0); // Disable test timeout. Will throw within 20s below.
176
+ const childrenRequestedToReport = children.length <= 10
177
+ ? children
178
+ : // Just those not fully joined
179
+ children.filter((_, index) => index === 0 || setNotFullyJoined.has(index));
207
180
  await timeoutAwait(Promise.race([
208
181
  executeDebugReports(childrenRequestedToReport),
209
182
  childErrorPromise,
@@ -211,14 +184,44 @@ describe(`Presence with AzureClient`, () => {
211
184
  testConsole.error("Debug report await resulted in", debugAwaitError);
212
185
  });
213
186
  throw error;
214
- }));
215
- // Act - disconnect first child process
216
- children[0].send({ command: "disconnectSelf" });
217
- // Verify - wait for all 'attendeeDisconnected' events
218
- await Promise.race([Promise.all(waitForDisconnected), childErrorPromise]);
219
- });
187
+ });
188
+ testConsole.log(`[${new Date().toISOString()}] All attendees fully joined after ${performance.now() - startConnectAndFullJoin}ms`);
189
+ let child0ReportRequested = false;
190
+ const waitForDisconnected = children.map(async (child, index) => index === 0
191
+ ? Promise.resolve()
192
+ : timeoutPromise((resolve) => {
193
+ child.on("message", (msg) => {
194
+ if (msg.event === "attendeeDisconnected" &&
195
+ msg.attendeeId === connectResult.containerCreatorAttendeeId) {
196
+ console.log(`Child[${index}] saw creator disconnect`);
197
+ resolve();
198
+ }
199
+ });
200
+ }, {
201
+ durationMs: childDisconnectTimeoutMs,
202
+ errorMsg: `Attendee[${index}] Disconnected Timeout`,
203
+ }).catch(async (error) => {
204
+ const childrenRequestedToReport = [child];
205
+ if (!child0ReportRequested) {
206
+ childrenRequestedToReport.unshift(children[0]);
207
+ child0ReportRequested = true;
208
+ }
209
+ await timeoutAwait(Promise.race([
210
+ executeDebugReports(childrenRequestedToReport),
211
+ childErrorPromise,
212
+ ]), { durationMs: 20_000, errorMsg: "Debug report timeout" }).catch((debugAwaitError) => {
213
+ testConsole.error("Debug report await resulted in", debugAwaitError);
214
+ });
215
+ throw error;
216
+ }));
217
+ // Act - disconnect first child process
218
+ children[0].send({ command: "disconnectSelf" });
219
+ // Verify - wait for all 'attendeeDisconnected' events
220
+ await Promise.race([Promise.all(waitForDisconnected), childErrorPromise]);
221
+ });
222
+ }
220
223
  }
221
- }
224
+ });
222
225
  {
223
226
  /**
224
227
  * Timeout for workspace registration {@link WorkspaceRegisteredEvent}
@@ -240,57 +243,65 @@ describe(`Presence with AzureClient`, () => {
240
243
  /**
241
244
  * Timeout for child processes to connect to container ({@link ConnectedEvent})
242
245
  */
243
- const childConnectTimeoutMs = 1000 * numClients * timeoutMultiplier;
244
- let children;
245
- let childErrorPromise;
246
- let containerCreatorAttendeeId;
247
- let attendeeIdPromises;
248
- let remoteClients;
249
- const testValue = "testValue";
250
- const workspaceId = "presenceTestWorkspace";
251
- beforeEach(async () => {
252
- ({ children, childErrorPromise } = await forkChildProcesses(numClients, afterCleanUp));
253
- ({ containerCreatorAttendeeId, attendeeIdPromises } = await connectChildProcesses(children, { writeClients: numClients, readyTimeoutMs: childConnectTimeoutMs }));
254
- await Promise.all(attendeeIdPromises);
255
- remoteClients = children.filter((_, index) => index !== 0);
256
- // NOTE: For testing purposes child clients will expect a Latest value of type string (StateFactory.latest<{ value: string }>).
257
- await registerWorkspaceOnChildren(children, workspaceId, {
258
- latest: true,
259
- timeoutMs: workspaceRegisterTimeoutMs,
260
- });
261
- });
262
- it(`allows clients to read Latest state from other clients [${numClients} clients]`, async function () {
263
- // AB#48866: Fix intermittently failing presence tests
264
- if (useAzure) {
265
- this.skip();
266
- }
267
- // Setup
268
- const updateEventsPromise = waitForLatestValueUpdates(remoteClients, workspaceId, childErrorPromise, stateUpdateTimeoutMs, { fromAttendeeId: containerCreatorAttendeeId, expectedValue: testValue });
269
- // Act - Trigger the update
270
- children[0].send({
271
- command: "setLatestValue",
272
- workspaceId,
273
- value: testValue,
246
+ const childConnectTimeoutMs = (4000 + 1000 * numClients) * timeoutMultiplier;
247
+ const testCaseTimeoutMs = 1000;
248
+ const testSetupAndActTimeoutMs = childConnectTimeoutMs + testCaseTimeoutMs;
249
+ // These tests use beforeEach to setup complex state that takes a lot of time
250
+ // and is dependent on number of clients. Keeping the work in beforeEach
251
+ // allows time reporting to report the tested scenario apart from the setup time.
252
+ // So this describe block isolates those beforeEach setups from each distinct
253
+ // client count. Test cases descriptions also have the client count for clarity.
254
+ describe(`with ${numClients} clients`, () => {
255
+ let children;
256
+ let childErrorPromise;
257
+ let containerCreatorAttendeeId;
258
+ let attendeeIdPromises;
259
+ let remoteClients;
260
+ const testValue = "testValue";
261
+ const workspaceId = "presenceTestWorkspace";
262
+ beforeEach(async function usingLatestStateObject_beforeEach() {
263
+ const startTime = performance.now();
264
+ setTestTimeout(this, testSetupAndActTimeoutMs);
265
+ ({ children, childErrorPromise } = await forkChildProcesses(this.currentTest?.title ?? "", numClients, afterCleanUp));
266
+ ({ containerCreatorAttendeeId, attendeeIdPromises } = await connectChildProcesses(children, { writeClients: numClients, readyTimeoutMs: childConnectTimeoutMs }));
267
+ await Promise.all(attendeeIdPromises);
268
+ remoteClients = children.filter((_, index) => index !== 0);
269
+ // NOTE: For testing purposes child clients will expect a Latest value of type string (StateFactory.latest<{ value: string }>).
270
+ await registerWorkspaceOnChildren(children, workspaceId, {
271
+ latest: true,
272
+ timeoutMs: workspaceRegisterTimeoutMs,
273
+ });
274
+ testConsole.log(` Setup for "${this.currentTest?.title}" completed in ${performance.now() - startTime}ms`);
274
275
  });
275
- const updateEvents = await updateEventsPromise;
276
- // Verify all events are from the expected attendee
277
- for (const updateEvent of updateEvents) {
278
- assert.strictEqual(updateEvent.attendeeId, containerCreatorAttendeeId);
279
- assert.deepStrictEqual(updateEvent.value, testValue);
280
- }
281
- // Act - Request each remote client to read latest state from container creator
282
- for (const child of remoteClients) {
283
- child.send({
284
- command: "getLatestValue",
276
+ it(`allows clients to read Latest state from other clients [${numClients} clients]`, async function () {
277
+ // Setup
278
+ const updateEventsPromise = waitForLatestValueUpdates(remoteClients, workspaceId, childErrorPromise, stateUpdateTimeoutMs, { fromAttendeeId: containerCreatorAttendeeId, expectedValue: testValue });
279
+ // Act - Trigger the update
280
+ children[0].send({
281
+ command: "setLatestValue",
285
282
  workspaceId,
286
- attendeeId: containerCreatorAttendeeId,
283
+ value: testValue,
287
284
  });
288
- }
289
- const getResponses = await getLatestValueResponses(remoteClients, workspaceId, childErrorPromise, getStateTimeoutMs);
290
- // Verify - all responses should contain the expected value
291
- for (const getResponse of getResponses) {
292
- assert.deepStrictEqual(getResponse.value, testValue);
293
- }
285
+ const updateEvents = await updateEventsPromise;
286
+ // Verify all events are from the expected attendee
287
+ for (const updateEvent of updateEvents) {
288
+ assert.strictEqual(updateEvent.attendeeId, containerCreatorAttendeeId);
289
+ assert.deepStrictEqual(updateEvent.value, testValue);
290
+ }
291
+ // Act - Request each remote client to read latest state from container creator
292
+ for (const child of remoteClients) {
293
+ child.send({
294
+ command: "getLatestValue",
295
+ workspaceId,
296
+ attendeeId: containerCreatorAttendeeId,
297
+ });
298
+ }
299
+ const getResponses = await getLatestValueResponses(remoteClients, workspaceId, childErrorPromise, getStateTimeoutMs);
300
+ // Verify - all responses should contain the expected value
301
+ for (const getResponse of getResponses) {
302
+ assert.deepStrictEqual(getResponse.value, testValue);
303
+ }
304
+ });
294
305
  });
295
306
  }
296
307
  });
@@ -302,129 +313,137 @@ describe(`Presence with AzureClient`, () => {
302
313
  /**
303
314
  * Timeout for child processes to connect to container ({@link ConnectedEvent})
304
315
  */
305
- const childConnectTimeoutMs = 1000 * numClients * timeoutMultiplier;
306
- let children;
307
- let childErrorPromise;
308
- let containerCreatorAttendeeId;
309
- let attendeeIdPromises;
310
- let remoteClients;
311
- const workspaceId = "presenceTestWorkspace";
312
- const key1 = "player1";
313
- const key2 = "player2";
314
- const value1 = { name: "Alice", score: 100 };
315
- const value2 = { name: "Bob", score: 200 };
316
- beforeEach(async () => {
317
- ({ children, childErrorPromise } = await forkChildProcesses(numClients, afterCleanUp));
318
- ({ containerCreatorAttendeeId, attendeeIdPromises } = await connectChildProcesses(children, { writeClients: numClients, readyTimeoutMs: childConnectTimeoutMs }));
319
- await Promise.all(attendeeIdPromises);
320
- remoteClients = children.filter((_, index) => index !== 0);
321
- // 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>).
322
- await registerWorkspaceOnChildren(children, workspaceId, {
323
- latestMap: true,
324
- timeoutMs: workspaceRegisterTimeoutMs,
325
- });
326
- });
327
- it(`allows clients to read LatestMap values from other clients [${numClients} clients]`, async () => {
328
- // Setup
329
- const testKey = "cursor";
330
- const testValue = { x: 150, y: 300 };
331
- const updateEventsPromise = waitForLatestMapValueUpdates(remoteClients, workspaceId, testKey, childErrorPromise, stateUpdateTimeoutMs, { fromAttendeeId: containerCreatorAttendeeId, expectedValue: testValue });
332
- // Act
333
- children[0].send({
334
- command: "setLatestMapValue",
335
- workspaceId,
336
- key: testKey,
337
- value: testValue,
316
+ const childConnectTimeoutMs = (4000 + 1000 * numClients) * timeoutMultiplier;
317
+ const testCaseTimeoutMs = 1000;
318
+ const testSetupAndActTimeoutMs = childConnectTimeoutMs + testCaseTimeoutMs;
319
+ // These tests use beforeEach to setup complex state that takes a lot of time
320
+ // and is dependent on number of clients. Keeping the work in beforeEach
321
+ // allows time reporting to report the tested scenario apart from the setup time.
322
+ // So this describe block isolates those beforeEach setups from each distinct
323
+ // client count. Test cases descriptions also have the client count for clarity.
324
+ describe(`with ${numClients} clients`, () => {
325
+ let children;
326
+ let childErrorPromise;
327
+ let containerCreatorAttendeeId;
328
+ let attendeeIdPromises;
329
+ let remoteClients;
330
+ const workspaceId = "presenceTestWorkspace";
331
+ const key1 = "player1";
332
+ const key2 = "player2";
333
+ const value1 = { name: "Alice", score: 100 };
334
+ const value2 = { name: "Bob", score: 200 };
335
+ beforeEach(async function usingLatestMapStateObject_beforeEach() {
336
+ const startTime = performance.now();
337
+ setTestTimeout(this, testSetupAndActTimeoutMs);
338
+ ({ children, childErrorPromise } = await forkChildProcesses(this.currentTest?.title ?? "", numClients, afterCleanUp));
339
+ ({ containerCreatorAttendeeId, attendeeIdPromises } = await connectChildProcesses(children, { writeClients: numClients, readyTimeoutMs: childConnectTimeoutMs }));
340
+ await Promise.all(attendeeIdPromises);
341
+ remoteClients = children.filter((_, index) => index !== 0);
342
+ // 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>).
343
+ await registerWorkspaceOnChildren(children, workspaceId, {
344
+ latestMap: true,
345
+ timeoutMs: workspaceRegisterTimeoutMs,
346
+ });
347
+ testConsole.log(` Setup for "${this.currentTest?.title}" completed in ${performance.now() - startTime}ms`);
338
348
  });
339
- const updateEvents = await updateEventsPromise;
340
- // Check all events are from the expected attendee
341
- for (const updateEvent of updateEvents) {
342
- assert.strictEqual(updateEvent.attendeeId, containerCreatorAttendeeId);
343
- assert.strictEqual(updateEvent.key, testKey);
344
- assert.deepStrictEqual(updateEvent.value, testValue);
345
- }
346
- for (const child of remoteClients) {
347
- child.send({
348
- command: "getLatestMapValue",
349
+ it(`allows clients to read LatestMap values from other clients [${numClients} clients]`, async () => {
350
+ // Setup
351
+ const testKey = "cursor";
352
+ const testValue = { x: 150, y: 300 };
353
+ const updateEventsPromise = waitForLatestMapValueUpdates(remoteClients, workspaceId, testKey, childErrorPromise, stateUpdateTimeoutMs, { fromAttendeeId: containerCreatorAttendeeId, expectedValue: testValue });
354
+ // Act
355
+ children[0].send({
356
+ command: "setLatestMapValue",
349
357
  workspaceId,
350
358
  key: testKey,
351
- attendeeId: containerCreatorAttendeeId,
359
+ value: testValue,
352
360
  });
353
- }
354
- const getResponses = await getLatestMapValueResponses(remoteClients, workspaceId, testKey, childErrorPromise, getStateTimeoutMs);
355
- // Verify
356
- for (const getResponse of getResponses) {
357
- assert.deepStrictEqual(getResponse.value, testValue);
358
- }
359
- });
360
- it(`returns per-key values on read [${numClients} clients]`, async function () {
361
- // AB#48866: Fix intermittently failing presence tests
362
- if (useAzure) {
363
- this.skip();
364
- }
365
- // Setup
366
- const allAttendeeIds = await Promise.all(attendeeIdPromises);
367
- const attendee0Id = containerCreatorAttendeeId;
368
- const attendee1Id = allAttendeeIds[1];
369
- const key1Recipients = children.filter((_, index) => index !== 0);
370
- const key2Recipients = children.filter((_, index) => index !== 1);
371
- const key1UpdateEventsPromise = waitForLatestMapValueUpdates(key1Recipients, workspaceId, key1, childErrorPromise, stateUpdateTimeoutMs, { fromAttendeeId: attendee0Id, expectedValue: value1 });
372
- const key2UpdateEventsPromise = waitForLatestMapValueUpdates(key2Recipients, workspaceId, key2, childErrorPromise, stateUpdateTimeoutMs, { fromAttendeeId: attendee1Id, expectedValue: value2 });
373
- // Act
374
- children[0].send({
375
- command: "setLatestMapValue",
376
- workspaceId,
377
- key: key1,
378
- value: value1,
379
- });
380
- const key1UpdateEvents = await key1UpdateEventsPromise;
381
- children[1].send({
382
- command: "setLatestMapValue",
383
- workspaceId,
384
- key: key2,
385
- value: value2,
361
+ const updateEvents = await updateEventsPromise;
362
+ // Check all events are from the expected attendee
363
+ for (const updateEvent of updateEvents) {
364
+ assert.strictEqual(updateEvent.attendeeId, containerCreatorAttendeeId);
365
+ assert.strictEqual(updateEvent.key, testKey);
366
+ assert.deepStrictEqual(updateEvent.value, testValue);
367
+ }
368
+ for (const child of remoteClients) {
369
+ child.send({
370
+ command: "getLatestMapValue",
371
+ workspaceId,
372
+ key: testKey,
373
+ attendeeId: containerCreatorAttendeeId,
374
+ });
375
+ }
376
+ const getResponses = await getLatestMapValueResponses(remoteClients, workspaceId, testKey, childErrorPromise, getStateTimeoutMs);
377
+ // Verify
378
+ for (const getResponse of getResponses) {
379
+ assert.deepStrictEqual(getResponse.value, testValue);
380
+ }
386
381
  });
387
- const key2UpdateEvents = await key2UpdateEventsPromise;
388
- // Verify all events are from the expected attendees
389
- for (const updateEvent of key1UpdateEvents) {
390
- assert.strictEqual(updateEvent.attendeeId, attendee0Id);
391
- assert.strictEqual(updateEvent.key, key1);
392
- assert.deepStrictEqual(updateEvent.value, value1);
393
- }
394
- for (const updateEvent of key2UpdateEvents) {
395
- assert.strictEqual(updateEvent.attendeeId, attendee1Id);
396
- assert.strictEqual(updateEvent.key, key2);
397
- assert.deepStrictEqual(updateEvent.value, value2);
398
- }
399
- // Read key1 of attendee0 from all children
400
- for (const child of children) {
401
- child.send({
402
- command: "getLatestMapValue",
382
+ it(`returns per-key values on read [${numClients} clients]`, async function () {
383
+ // Setup
384
+ const allAttendeeIds = await Promise.all(attendeeIdPromises);
385
+ const attendee0Id = containerCreatorAttendeeId;
386
+ const attendee1Id = allAttendeeIds[1];
387
+ const key1Recipients = children.filter((_, index) => index !== 0);
388
+ const key2Recipients = children.filter((_, index) => index !== 1);
389
+ const key1UpdateEventsPromise = waitForLatestMapValueUpdates(key1Recipients, workspaceId, key1, childErrorPromise, stateUpdateTimeoutMs, { fromAttendeeId: attendee0Id, expectedValue: value1 });
390
+ const key2UpdateEventsPromise = waitForLatestMapValueUpdates(key2Recipients, workspaceId, key2, childErrorPromise, stateUpdateTimeoutMs, { fromAttendeeId: attendee1Id, expectedValue: value2 });
391
+ // Act
392
+ children[0].send({
393
+ command: "setLatestMapValue",
403
394
  workspaceId,
404
395
  key: key1,
405
- attendeeId: attendee0Id,
396
+ value: value1,
406
397
  });
407
- }
408
- const key1Responses = await getLatestMapValueResponses(children, workspaceId, key1, childErrorPromise, getStateTimeoutMs);
409
- // Read key2 of attendee1 from all children
410
- for (const child of children) {
411
- child.send({
412
- command: "getLatestMapValue",
398
+ const key1UpdateEvents = await key1UpdateEventsPromise;
399
+ children[1].send({
400
+ command: "setLatestMapValue",
413
401
  workspaceId,
414
402
  key: key2,
415
- attendeeId: attendee1Id,
403
+ value: value2,
416
404
  });
417
- }
418
- const key2Responses = await getLatestMapValueResponses(children, workspaceId, key2, childErrorPromise, getStateTimeoutMs);
419
- // Verify
420
- assert.strictEqual(key1Responses.length, numClients, "Expected responses from all clients for key1");
421
- assert.strictEqual(key2Responses.length, numClients, "Expected responses from all clients for key2");
422
- for (const response of key1Responses) {
423
- assert.deepStrictEqual(response.value, value1, "Key1 value should match");
424
- }
425
- for (const response of key2Responses) {
426
- assert.deepStrictEqual(response.value, value2, "Key2 value should match");
427
- }
405
+ const key2UpdateEvents = await key2UpdateEventsPromise;
406
+ // Verify all events are from the expected attendees
407
+ for (const updateEvent of key1UpdateEvents) {
408
+ assert.strictEqual(updateEvent.attendeeId, attendee0Id);
409
+ assert.strictEqual(updateEvent.key, key1);
410
+ assert.deepStrictEqual(updateEvent.value, value1);
411
+ }
412
+ for (const updateEvent of key2UpdateEvents) {
413
+ assert.strictEqual(updateEvent.attendeeId, attendee1Id);
414
+ assert.strictEqual(updateEvent.key, key2);
415
+ assert.deepStrictEqual(updateEvent.value, value2);
416
+ }
417
+ // Read key1 of attendee0 from all children
418
+ for (const child of children) {
419
+ child.send({
420
+ command: "getLatestMapValue",
421
+ workspaceId,
422
+ key: key1,
423
+ attendeeId: attendee0Id,
424
+ });
425
+ }
426
+ const key1Responses = await getLatestMapValueResponses(children, workspaceId, key1, childErrorPromise, getStateTimeoutMs);
427
+ // Read key2 of attendee1 from all children
428
+ for (const child of children) {
429
+ child.send({
430
+ command: "getLatestMapValue",
431
+ workspaceId,
432
+ key: key2,
433
+ attendeeId: attendee1Id,
434
+ });
435
+ }
436
+ const key2Responses = await getLatestMapValueResponses(children, workspaceId, key2, childErrorPromise, getStateTimeoutMs);
437
+ // Verify
438
+ assert.strictEqual(key1Responses.length, numClients, "Expected responses from all clients for key1");
439
+ assert.strictEqual(key2Responses.length, numClients, "Expected responses from all clients for key2");
440
+ for (const response of key1Responses) {
441
+ assert.deepStrictEqual(response.value, value1, "Key1 value should match");
442
+ }
443
+ for (const response of key2Responses) {
444
+ assert.deepStrictEqual(response.value, value2, "Key2 value should match");
445
+ }
446
+ });
428
447
  });
429
448
  }
430
449
  });