@fluidframework/test-utils 1.2.7 → 2.0.0-dev.1.3.0.96595
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/dist/DriverWrappers.d.ts +29 -0
- package/dist/DriverWrappers.d.ts.map +1 -0
- package/dist/DriverWrappers.js +54 -0
- package/dist/DriverWrappers.js.map +1 -0
- package/dist/TestConfigs.d.ts +7 -0
- package/dist/TestConfigs.d.ts.map +1 -0
- package/dist/TestConfigs.js +15 -0
- package/dist/TestConfigs.js.map +1 -0
- package/dist/TestSummaryUtils.d.ts +18 -0
- package/dist/TestSummaryUtils.d.ts.map +1 -0
- package/dist/TestSummaryUtils.js +95 -0
- package/dist/TestSummaryUtils.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/loaderContainerTracker.d.ts +25 -9
- package/dist/loaderContainerTracker.d.ts.map +1 -1
- package/dist/loaderContainerTracker.js +58 -17
- package/dist/loaderContainerTracker.js.map +1 -1
- package/dist/localCodeLoader.d.ts.map +1 -1
- package/dist/localCodeLoader.js +6 -14
- package/dist/localCodeLoader.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.d.ts.map +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/testFluidObject.d.ts +16 -9
- package/dist/testFluidObject.d.ts.map +1 -1
- package/dist/testFluidObject.js +16 -9
- package/dist/testFluidObject.js.map +1 -1
- package/dist/testObjectProvider.d.ts +3 -2
- package/dist/testObjectProvider.d.ts.map +1 -1
- package/dist/testObjectProvider.js +9 -2
- package/dist/testObjectProvider.js.map +1 -1
- package/dist/timeoutUtils.d.ts +2 -0
- package/dist/timeoutUtils.d.ts.map +1 -1
- package/dist/timeoutUtils.js +8 -2
- package/dist/timeoutUtils.js.map +1 -1
- package/package.json +29 -28
- package/src/DriverWrappers.ts +77 -0
- package/src/TestConfigs.ts +14 -0
- package/src/TestSummaryUtils.ts +144 -0
- package/src/index.ts +3 -0
- package/src/loaderContainerTracker.ts +66 -17
- package/src/localCodeLoader.ts +6 -14
- package/src/packageVersion.ts +1 -1
- package/src/testFluidObject.ts +16 -9
- package/src/testObjectProvider.ts +10 -3
- package/src/timeoutUtils.ts +9 -1
- package/tsconfig.json +1 -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
|
|
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
|
|
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
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* -
|
|
158
|
-
*
|
|
159
|
-
*
|
|
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
|
-
|
|
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,18 +213,35 @@ 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;
|
|
198
|
-
|
|
233
|
+
const remainedDuration = timeoutDuration ? timeoutDuration - (Date.now() - start) : defaultMaxTimeout;
|
|
234
|
+
await Promise.all(dirtyContainers.map(async (c) => Promise.race(
|
|
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
|
+
],
|
|
244
|
+
)));
|
|
199
245
|
}
|
|
200
246
|
|
|
201
247
|
// yield a turn to allow side effect of the ops we just processed execute before we check again
|
|
@@ -205,7 +251,10 @@ export class LoaderContainerTracker implements IOpProcessingController {
|
|
|
205
251
|
// Pause all container that was resumed
|
|
206
252
|
// don't call pause if resumed is empty and pause everything, which is not what we want
|
|
207
253
|
if (resumed.length !== 0) {
|
|
208
|
-
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
|
+
});
|
|
209
258
|
}
|
|
210
259
|
|
|
211
260
|
debugWait("Synchronized");
|
|
@@ -451,13 +500,13 @@ export class LoaderContainerTracker implements IOpProcessingController {
|
|
|
451
500
|
private setupInOutTracker(container: IContainer, inflightTracker: Map<IContainer, number>) {
|
|
452
501
|
const outHandler = (messages: IDocumentMessage[]) => {
|
|
453
502
|
for (const message of messages) {
|
|
454
|
-
if (message
|
|
503
|
+
if (!canBeCoalescedByService(message)) {
|
|
455
504
|
inflightTracker.set(container, message.clientSequenceNumber);
|
|
456
505
|
}
|
|
457
506
|
}
|
|
458
507
|
};
|
|
459
508
|
const inHandler = (message: ISequencedDocumentMessage) => {
|
|
460
|
-
if (message
|
|
509
|
+
if (!canBeCoalescedByService(message)
|
|
461
510
|
&& message.clientId === (container as Container).clientId
|
|
462
511
|
&& inflightTracker.get(container) === message.clientSequenceNumber) {
|
|
463
512
|
inflightTracker.delete(container);
|
package/src/localCodeLoader.ts
CHANGED
|
@@ -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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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) {
|
package/src/packageVersion.ts
CHANGED
package/src/testFluidObject.ts
CHANGED
|
@@ -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
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
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
|
-
*
|
|
122
|
-
*
|
|
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
|
-
|
|
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) {
|
package/src/timeoutUtils.ts
CHANGED
|
@@ -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
|
|
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
|
package/tsconfig.json
CHANGED