@fluidframework/test-utils 1.3.0 → 2.0.0-dev.1.4.5.105745

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.
Files changed (50) hide show
  1. package/dist/DriverWrappers.d.ts +29 -0
  2. package/dist/DriverWrappers.d.ts.map +1 -0
  3. package/dist/DriverWrappers.js +54 -0
  4. package/dist/DriverWrappers.js.map +1 -0
  5. package/dist/TestConfigs.d.ts +7 -0
  6. package/dist/TestConfigs.d.ts.map +1 -0
  7. package/dist/TestConfigs.js +15 -0
  8. package/dist/TestConfigs.js.map +1 -0
  9. package/dist/TestSummaryUtils.d.ts +18 -0
  10. package/dist/TestSummaryUtils.d.ts.map +1 -0
  11. package/dist/TestSummaryUtils.js +95 -0
  12. package/dist/TestSummaryUtils.js.map +1 -0
  13. package/dist/index.d.ts +3 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/loaderContainerTracker.d.ts +25 -9
  18. package/dist/loaderContainerTracker.d.ts.map +1 -1
  19. package/dist/loaderContainerTracker.js +58 -18
  20. package/dist/loaderContainerTracker.js.map +1 -1
  21. package/dist/localCodeLoader.d.ts.map +1 -1
  22. package/dist/localCodeLoader.js +6 -14
  23. package/dist/localCodeLoader.js.map +1 -1
  24. package/dist/packageVersion.d.ts +1 -1
  25. package/dist/packageVersion.d.ts.map +1 -1
  26. package/dist/packageVersion.js +1 -1
  27. package/dist/packageVersion.js.map +1 -1
  28. package/dist/testFluidObject.d.ts +16 -9
  29. package/dist/testFluidObject.d.ts.map +1 -1
  30. package/dist/testFluidObject.js +16 -9
  31. package/dist/testFluidObject.js.map +1 -1
  32. package/dist/testObjectProvider.d.ts +3 -2
  33. package/dist/testObjectProvider.d.ts.map +1 -1
  34. package/dist/testObjectProvider.js +9 -2
  35. package/dist/testObjectProvider.js.map +1 -1
  36. package/dist/timeoutUtils.d.ts +2 -0
  37. package/dist/timeoutUtils.d.ts.map +1 -1
  38. package/dist/timeoutUtils.js +8 -2
  39. package/dist/timeoutUtils.js.map +1 -1
  40. package/package.json +29 -28
  41. package/src/DriverWrappers.ts +77 -0
  42. package/src/TestConfigs.ts +14 -0
  43. package/src/TestSummaryUtils.ts +144 -0
  44. package/src/index.ts +3 -0
  45. package/src/loaderContainerTracker.ts +64 -18
  46. package/src/localCodeLoader.ts +6 -14
  47. package/src/packageVersion.ts +1 -1
  48. package/src/testFluidObject.ts +16 -9
  49. package/src/testObjectProvider.ts +10 -3
  50. package/src/timeoutUtils.ts +9 -1
@@ -2,17 +2,23 @@
2
2
  * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
3
  * Licensed under the MIT License.
4
4
  */
5
+ /* eslint-disable @typescript-eslint/strict-boolean-expressions */
5
6
 
6
7
  import { assert } from "@fluidframework/common-utils";
7
8
  import { IContainer, IDeltaQueue, IHostLoader } from "@fluidframework/container-definitions";
8
9
  import { Container } from "@fluidframework/container-loader";
10
+ import { canBeCoalescedByService } from "@fluidframework/driver-utils";
9
11
  import { IDocumentMessage, ISequencedDocumentMessage, MessageType } from "@fluidframework/protocol-definitions";
10
12
  import { debug } from "./debug";
11
13
  import { IOpProcessingController } from "./testObjectProvider";
14
+ import { timeoutAwait, timeoutPromise } from "./timeoutUtils";
12
15
 
13
16
  const debugOp = debug.extend("ops");
14
17
  const debugWait = debug.extend("wait");
15
18
 
19
+ // set the maximum timeout value as 5 mins
20
+ const defaultMaxTimeout = 5 * 6000;
21
+
16
22
  interface ContainerRecord {
17
23
  // A short number for debug output
18
24
  index: number;
@@ -91,7 +97,7 @@ export class LoaderContainerTracker implements IOpProcessingController {
91
97
  private trackTrailingNoOps(container: IContainer, record: ContainerRecord) {
92
98
  container.deltaManager.outbound.on("op", (messages) => {
93
99
  for (const msg of messages) {
94
- if (msg.type === MessageType.NoOp) {
100
+ if (canBeCoalescedByService(msg)) {
95
101
  // Track the NoOp that was sent.
96
102
  if (record.trailingNoOps === 0) {
97
103
  // record the starting sequence number of the trailing no ops if we haven't been tracking yet.
@@ -107,7 +113,7 @@ export class LoaderContainerTracker implements IOpProcessingController {
107
113
 
108
114
  container.deltaManager.inbound.on("push", (message) => {
109
115
  // Received the no op back, update the record if we are tracking
110
- if (message.type === MessageType.NoOp
116
+ if (canBeCoalescedByService(message)
111
117
  && message.clientId === (container as Container).clientId
112
118
  && record.trailingNoOps !== 0
113
119
  && record.startTrailingNoOps <= message.clientSequenceNumber
@@ -147,18 +153,41 @@ export class LoaderContainerTracker implements IOpProcessingController {
147
153
  // REVIEW: do we need to unpatch the loaders?
148
154
  }
149
155
 
156
+ /**
157
+ * Ensure all tracked containers are synchronized
158
+ */
159
+ public async ensureSynchronized(...containers: IContainer[]): Promise<void> {
160
+ await this.processSynchronized(undefined, ...containers);
161
+ }
162
+
163
+ /**
164
+ * Ensure all tracked containers are synchronized with a time limit
165
+ */
166
+ public async ensureSynchronizedWithTimeout?(timeoutDuration: number | undefined, ...containers: IContainer[]) {
167
+ await this.processSynchronized(timeoutDuration, ...containers);
168
+ }
169
+
150
170
  /**
151
171
  * Make sure all the tracked containers are synchronized.
152
- * - No isDirty (non-readonly) containers
153
- * - No extra clientId in quorum of any container that is not tracked and still opened.
154
- * - i.e. no pending Join/Leave message.
155
- * - No unresolved proposal (minSeqNum \>= lastProposalSeqNum)
156
- * - lastSequenceNumber of all container is the same
157
- * - clientSequenceNumberObserved is the same as clientSequenceNumber sent
158
- * - this overlaps with !isDirty, but include task scheduler ops.
159
- * - Trailing NoOp is tracked and don't count as pending ops.
172
+ *
173
+ * No isDirty (non-readonly) containers
174
+ *
175
+ * No extra clientId in quorum of any container that is not tracked and still opened.
176
+ *
177
+ * - i.e. no pending Join/Leave message.
178
+ *
179
+ * No unresolved proposal (minSeqNum \>= lastProposalSeqNum)
180
+ *
181
+ * lastSequenceNumber of all container is the same
182
+ *
183
+ * clientSequenceNumberObserved is the same as clientSequenceNumber sent
184
+ *
185
+ * - this overlaps with !isDirty, but include task scheduler ops.
186
+ *
187
+ * - Trailing NoOp is tracked and don't count as pending ops.
160
188
  */
161
- public async ensureSynchronized(...containers: IContainer[]) {
189
+ private async processSynchronized(timeoutDuration: number | undefined, ...containers: IContainer[]) {
190
+ const start = Date.now();
162
191
  const resumed = this.resumeProcessing(...containers);
163
192
 
164
193
  let waitingSequenceNumberSynchronized = false;
@@ -184,20 +213,34 @@ export class LoaderContainerTracker implements IOpProcessingController {
184
213
  // Only write it out once
185
214
  waitingSequenceNumberSynchronized = true;
186
215
  debugWait("Waiting for sequence number synchronized");
187
- await this.waitForAnyInboundOps(containersToApply);
216
+ await timeoutAwait(this.waitForAnyInboundOps(containersToApply), {
217
+ durationMs: timeoutDuration ? timeoutDuration - (Date.now() - start) : defaultMaxTimeout,
218
+ errorMsg: "Timeout on waiting for sequence number synchronized",
219
+ });
188
220
  }
189
221
  } else {
190
222
  waitingSequenceNumberSynchronized = false;
191
- await this.waitForPendingClients(pendingClients);
223
+ await timeoutAwait(this.waitForPendingClients(pendingClients), {
224
+ durationMs: timeoutDuration ? timeoutDuration - (Date.now() - start) : defaultMaxTimeout,
225
+ errorMsg: "Timeout on waiting for pending join or leave op",
226
+ });
192
227
  }
193
228
  } else {
194
229
  // Wait for all the containers to be saved
195
230
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
196
231
  debugWait(`Waiting container to be saved ${dirtyContainers.map((c) => this.containers.get(c)!.index)}`);
197
232
  waitingSequenceNumberSynchronized = false;
233
+ const remainedDuration = timeoutDuration ? timeoutDuration - (Date.now() - start) : defaultMaxTimeout;
198
234
  await Promise.all(dirtyContainers.map(async (c) => Promise.race(
199
- [new Promise((resolve) => c.once("saved", resolve)),
200
- new Promise((resolve) => c.once("closed", resolve))],
235
+ [timeoutPromise(
236
+ (resolve) => c.once("saved", () => resolve()),
237
+ {
238
+ durationMs: remainedDuration,
239
+ errorMsg: "Timeout on waiting a container to be saved",
240
+ },
241
+ ),
242
+ new Promise((resolve) => c.once("closed", resolve)),
243
+ ],
201
244
  )));
202
245
  }
203
246
 
@@ -208,7 +251,10 @@ export class LoaderContainerTracker implements IOpProcessingController {
208
251
  // Pause all container that was resumed
209
252
  // don't call pause if resumed is empty and pause everything, which is not what we want
210
253
  if (resumed.length !== 0) {
211
- await this.pauseProcessing(...resumed);
254
+ await timeoutAwait(this.pauseProcessing(...resumed), {
255
+ durationMs: timeoutDuration ? timeoutDuration - (Date.now() - start) : defaultMaxTimeout,
256
+ errorMsg: "Timeout on waiting for pausing all resumed containers",
257
+ });
212
258
  }
213
259
 
214
260
  debugWait("Synchronized");
@@ -454,13 +500,13 @@ export class LoaderContainerTracker implements IOpProcessingController {
454
500
  private setupInOutTracker(container: IContainer, inflightTracker: Map<IContainer, number>) {
455
501
  const outHandler = (messages: IDocumentMessage[]) => {
456
502
  for (const message of messages) {
457
- if (message.type !== MessageType.NoOp) {
503
+ if (!canBeCoalescedByService(message)) {
458
504
  inflightTracker.set(container, message.clientSequenceNumber);
459
505
  }
460
506
  }
461
507
  };
462
508
  const inHandler = (message: ISequencedDocumentMessage) => {
463
- if (message.type !== MessageType.NoOp
509
+ if (!canBeCoalescedByService(message)
464
510
  && message.clientId === (container as Container).clientId
465
511
  && inflightTracker.get(container) === message.clientSequenceNumber) {
466
512
  inflightTracker.delete(container);
@@ -43,14 +43,10 @@ export class LocalCodeLoader implements ICodeDetailsLoader {
43
43
  // Store the entry point against a unique id in the fluidPackageCache.
44
44
  // For code details containing a package name, use the package name as the id.
45
45
  // For code details containing a Fluid package, create a unique id from the package name and version.
46
- let pkgId: string;
47
-
48
46
  const source = entry[0];
49
- if (typeof source.package === "string") {
50
- pkgId = source.package;
51
- } else {
52
- pkgId = `${source.package.name}@${source.package.version}`;
53
- }
47
+ const pkgId = typeof source.package === "string"
48
+ ? source.package
49
+ : `${source.package.name}@${source.package.version}`;
54
50
  let fluidModule = entry[1] as IFluidModule;
55
51
  if (fluidModule?.fluidExport === undefined) {
56
52
  const maybeExport = fluidModule as SupportedExportInterfaces;
@@ -98,13 +94,9 @@ export class LocalCodeLoader implements ICodeDetailsLoader {
98
94
  // Get the entry point for from the fluidPackageCache for the given code details.
99
95
  // For code details containing a package name, use the package name as the id.
100
96
  // For code details containing a Fluid package, create a unique id from the package name and version.
101
- let pkdId: string;
102
-
103
- if (typeof source.package === "string") {
104
- pkdId = source.package;
105
- } else {
106
- pkdId = `${source.package.name}@${source.package.version}`;
107
- }
97
+ const pkdId = typeof source.package === "string"
98
+ ? source.package
99
+ : `${source.package.name}@${source.package.version}`;
108
100
 
109
101
  const entryPoint = this.fluidPackageCache.get(pkdId);
110
102
  if (entryPoint === undefined) {
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/test-utils";
9
- export const pkgVersion = "1.3.0";
9
+ export const pkgVersion = "2.0.0-dev.1.4.5.105745";
@@ -109,17 +109,24 @@ export type ChannelFactoryRegistry = Iterable<[string | undefined, IChannelFacto
109
109
  * with the object factories in the entry list. All the entries with an id other than undefined are passed to the
110
110
  * Fluid object so that it can create a shared object for each.
111
111
  *
112
- * For example, the following will create a Fluid object that creates and loads a SharedString and SharedDirectory. It
113
- * will add SparseMatrix to the data store's factory so that it can be created later.
114
- * new TestFluidObjectFactory([
115
- * [ "sharedString", SharedString.getFactory() ],
116
- * [ "sharedDirectory", SharedDirectory.getFactory() ],
117
- * [ undefined, SparseMatrix.getFactory() ],
118
- * ]);
112
+ * @example
113
+ * The following will create a Fluid object that creates and loads a SharedString and SharedDirectory.
114
+ * It will add SparseMatrix to the data store's factory so that it can be created later.
115
+ *
116
+ * ```typescript
117
+ * new TestFluidObjectFactory([
118
+ * [ "sharedString", SharedString.getFactory() ],
119
+ * [ "sharedDirectory", SharedDirectory.getFactory() ],
120
+ * [ undefined, SparseMatrix.getFactory() ],
121
+ * ]);
122
+ * ```
119
123
  *
120
124
  * The SharedString and SharedDirectory can be retrieved via getSharedObject() on the TestFluidObject as follows:
121
- * sharedString = testFluidObject.getSharedObject<SharedString>("sharedString");
122
- * sharedDir = testFluidObject.getSharedObject<SharedDirectory>("sharedDirectory");
125
+ *
126
+ * ```typescript
127
+ * sharedString = testFluidObject.getSharedObject<SharedString>("sharedString");
128
+ * sharedDir = testFluidObject.getSharedObject<SharedDirectory>("sharedDirectory");
129
+ * ```
123
130
  */
124
131
  export class TestFluidObjectFactory implements IFluidDataStoreFactory {
125
132
  public get IFluidDataStoreFactory() { return this; }
@@ -66,7 +66,7 @@ export interface ITestObjectProvider {
66
66
  defaultCodeDetails: IFluidCodeDetails;
67
67
  opProcessingController: IOpProcessingController;
68
68
 
69
- ensureSynchronized(): Promise<void>;
69
+ ensureSynchronized(timeoutDuration?: number): Promise<void>;
70
70
  reset(): void;
71
71
 
72
72
  documentId: string;
@@ -231,6 +231,10 @@ export class TestObjectProvider implements ITestObjectProvider {
231
231
  return this._logger;
232
232
  }
233
233
 
234
+ set logger(logger: EventAndErrorTrackingLogger) {
235
+ this._logger = logger;
236
+ }
237
+
234
238
  get documentServiceFactory() {
235
239
  if (!this._documentServiceFactory) {
236
240
  this._documentServiceFactory = this.driver.createDocumentServiceFactory();
@@ -389,8 +393,11 @@ export class TestObjectProvider implements ITestObjectProvider {
389
393
  this._documentCreated = false;
390
394
  }
391
395
 
392
- public async ensureSynchronized() {
393
- return this._loaderContainerTracker.ensureSynchronized();
396
+ public async ensureSynchronized(timeoutDuration?: number): Promise<void> {
397
+ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
398
+ return !timeoutDuration
399
+ ? this._loaderContainerTracker.ensureSynchronized()
400
+ : this._loaderContainerTracker.ensureSynchronizedWithTimeout?.(timeoutDuration);
394
401
  }
395
402
 
396
403
  public async waitContainerToCatchUp(container: IContainer) {
@@ -3,6 +3,8 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
+ import { Container } from "@fluidframework/container-loader";
7
+
6
8
  export const defaultTimeoutDurationMs = 250;
7
9
 
8
10
  export interface TimeoutWithError {
@@ -23,6 +25,12 @@ export async function timeoutAwait<T = void>(
23
25
  return Promise.race([promise, timeoutPromise<T>(() => { }, timeoutOptions)]);
24
26
  }
25
27
 
28
+ export async function ensureContainerConnected(container: Container): Promise<void> {
29
+ if (!container.connected) {
30
+ return timeoutPromise((resolve) => container.once("connected", () => resolve()));
31
+ }
32
+ }
33
+
26
34
  export async function timeoutPromise<T = void>(
27
35
  executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void,
28
36
  timeoutOptions: TimeoutWithError | TimeoutWithValue<T> = {},
@@ -32,7 +40,7 @@ export async function timeoutPromise<T = void>(
32
40
  && Number.isFinite(timeoutOptions.durationMs)
33
41
  && timeoutOptions.durationMs > 0
34
42
  ? timeoutOptions.durationMs : defaultTimeoutDurationMs;
35
- // create the timeout error outside the async task, so it's callstack includes
43
+ // create the timeout error outside the async task, so its callstack includes
36
44
  // the original call site, this makes it easier to debug
37
45
  const err = timeoutOptions.reject === false
38
46
  ? undefined