@fluidframework/test-utils 2.0.0-internal.2.4.0 → 2.0.0-internal.3.0.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.
@@ -3,14 +3,117 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
+ import { Container } from "@fluidframework/container-loader";
7
+ import { assert, Deferred } from "@fluidframework/common-utils";
8
+
9
+ // @deprecated this value is no longer used
6
10
  export const defaultTimeoutDurationMs = 250;
7
11
 
12
+ // TestTimeout class manage tracking of test timeout. It create a timer when timeout is in effect,
13
+ // and provide a promise that will be reject before the test timeout happen with a `timeBuffer` of 15 ms.
14
+ // Once rejected, a new TestTimeout object will be create for the timeout.
15
+
16
+ const timeBuffer = 15; // leave 15 ms leeway for finish processing
17
+
18
+ class TestTimeout {
19
+ private timeout: number = 0;
20
+ private timer: NodeJS.Timeout | undefined;
21
+ private readonly deferred: Deferred<void>;
22
+ private rejected = false;
23
+
24
+ private static instance: TestTimeout = new TestTimeout();
25
+ public static reset(runnable: Mocha.Runnable) {
26
+ TestTimeout.clear();
27
+ TestTimeout.instance.resetTimer(runnable);
28
+ }
29
+
30
+ public static clear() {
31
+ if (TestTimeout.instance.rejected) {
32
+ TestTimeout.instance = new TestTimeout();
33
+ } else {
34
+ TestTimeout.instance.clearTimer();
35
+ }
36
+ }
37
+
38
+ public static getInstance() {
39
+ return TestTimeout.instance;
40
+ }
41
+
42
+ public async getPromise() {
43
+ return this.deferred.promise;
44
+ }
45
+
46
+ public getTimeout() {
47
+ return this.timeout;
48
+ }
49
+
50
+ private constructor() {
51
+ this.deferred = new Deferred();
52
+ // Ignore rejection for timeout promise if no one is waiting for it.
53
+ this.deferred.promise.catch(() => { });
54
+ }
55
+
56
+ private resetTimer(runnable: Mocha.Runnable) {
57
+ assert(!this.timer, "clearTimer should have been called before reset");
58
+ assert(!this.deferred.isCompleted, "can't reset a completed TestTimeout");
59
+
60
+ // Check the test timeout setting
61
+ const timeout = runnable.timeout();
62
+ if (!(Number.isFinite(timeout) && timeout > 0)) { return; }
63
+
64
+ // subtract a buffer
65
+ this.timeout = Math.max(timeout - timeBuffer, 1);
66
+
67
+ // Set up timer to reject near the test timeout.
68
+ this.timer = setTimeout(() => {
69
+ this.deferred.reject(this);
70
+ this.rejected = true;
71
+ }, this.timeout);
72
+ }
73
+ private clearTimer() {
74
+ if (this.timer) {
75
+ clearTimeout(this.timer);
76
+ this.timer = undefined;
77
+ }
78
+ }
79
+ }
80
+
81
+ // only register if we are running with mocha-test-setup loaded
82
+ if (globalThis.getMochaModule !== undefined) {
83
+ // patching resetTimeout and clearTimeout on the runnable object
84
+ // so we can track when test timeout are enforced
85
+ const mochaModule = globalThis.getMochaModule() as typeof Mocha;
86
+ const runnablePrototype = mochaModule.Runnable.prototype;
87
+ // eslint-disable-next-line @typescript-eslint/unbound-method
88
+ const oldResetTimeoutFunc = runnablePrototype.resetTimeout;
89
+ runnablePrototype.resetTimeout = function(this: Mocha.Runnable) {
90
+ oldResetTimeoutFunc.call(this);
91
+ TestTimeout.reset(this);
92
+ };
93
+ // eslint-disable-next-line @typescript-eslint/unbound-method
94
+ const oldClearTimeoutFunc = runnablePrototype.clearTimeout;
95
+ runnablePrototype.clearTimeout = function(this: Mocha.Runnable) {
96
+ TestTimeout.clear();
97
+ oldClearTimeoutFunc.call(this);
98
+ };
99
+ }
100
+
8
101
  export interface TimeoutWithError {
102
+ /**
103
+ * Timeout duration in milliseconds, if it is great than 0 and not Infinity
104
+ * If it is undefined, then it will use test timeout if we are in side the test function
105
+ * Otherwise, there is no timeout
106
+ */
9
107
  durationMs?: number;
10
108
  reject?: true;
11
109
  errorMsg?: string;
12
110
  }
13
111
  export interface TimeoutWithValue<T = void> {
112
+ /**
113
+ * Timeout duration in milliseconds, if it is great than 0 and not Infinity
114
+ * If it is undefined, then it will use test timeout if we are in side the test function
115
+ * Otherwise, there is no timeout
116
+ */
14
117
  durationMs?: number;
15
118
  reject: false;
16
119
  value: T;
@@ -25,23 +128,32 @@ export async function timeoutAwait<T = void>(
25
128
  return Promise.race([promise, timeoutPromise<T>(() => { }, timeoutOptions)]);
26
129
  }
27
130
 
28
- export async function timeoutPromise<T = void>(
131
+ export async function ensureContainerConnected(container: Container): Promise<void> {
132
+ if (!container.connected) {
133
+ return timeoutPromise((resolve) => container.once("connected", () => resolve()));
134
+ }
135
+ }
136
+
137
+ // Create a promise based on the timeout options
138
+ async function getTimeoutPromise<T = void>(
29
139
  executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void,
30
- timeoutOptions: TimeoutWithError | TimeoutWithValue<T> = {},
31
- ): Promise<T> {
32
- const timeout =
33
- timeoutOptions.durationMs !== undefined
34
- && Number.isFinite(timeoutOptions.durationMs)
35
- && timeoutOptions.durationMs > 0
36
- ? timeoutOptions.durationMs : defaultTimeoutDurationMs;
37
- // create the timeout error outside the async task, so its callstack includes
38
- // the original call site, this makes it easier to debug
39
- const err = timeoutOptions.reject === false
40
- ? undefined
41
- : new Error(`${timeoutOptions.errorMsg ?? "Timed out"}(${timeout}ms)`);
140
+ timeoutOptions: TimeoutWithError | TimeoutWithValue<T>,
141
+ err: Error | undefined,
142
+ ) {
143
+ const timeout = timeoutOptions.durationMs ?? 0;
144
+ if (timeout <= 0 || !Number.isFinite(timeout)) {
145
+ return new Promise(executor);
146
+ }
147
+
42
148
  return new Promise<T>((resolve, reject) => {
149
+ const timeoutRejections = () => {
150
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
151
+ const errorObject = err!;
152
+ errorObject.message = `${errorObject.message} (${timeout}ms)`;
153
+ reject(err);
154
+ };
43
155
  const timer = setTimeout(
44
- () => timeoutOptions.reject === false ? resolve(timeoutOptions.value) : reject(err),
156
+ () => timeoutOptions.reject === false ? resolve(timeoutOptions.value) : timeoutRejections(),
45
157
  timeout);
46
158
 
47
159
  executor(
@@ -55,3 +167,34 @@ export async function timeoutPromise<T = void>(
55
167
  });
56
168
  });
57
169
  }
170
+
171
+ // Create a promise based on test timeout and the timeout options
172
+ export async function timeoutPromise<T = void>(
173
+ executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void,
174
+ timeoutOptions: TimeoutWithError | TimeoutWithValue<T> = {},
175
+ ): Promise<T> {
176
+ // create the timeout error outside the async task, so its callstack includes
177
+ // the original call site, this makes it easier to debug
178
+ const err = timeoutOptions.reject === false
179
+ ? undefined
180
+ : new Error(timeoutOptions.errorMsg ?? "Timed out");
181
+ const executorPromise = getTimeoutPromise(executor, timeoutOptions, err);
182
+
183
+ const currentTestTimeout = TestTimeout.getInstance();
184
+ if (currentTestTimeout === undefined) { return executorPromise; }
185
+
186
+ return Promise.race([executorPromise, currentTestTimeout.getPromise()]).catch((e) => {
187
+ if (e === currentTestTimeout) {
188
+ if (timeoutOptions.reject !== false) {
189
+ // If the rejection is because of the timeout then
190
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
191
+ const errorObject = err!;
192
+ errorObject.message =
193
+ `${timeoutOptions.errorMsg ?? "Test timed out"} (${currentTestTimeout.getTimeout()}ms)`;
194
+ throw errorObject;
195
+ }
196
+ return timeoutOptions.value;
197
+ }
198
+ throw e;
199
+ }) as Promise<T>;
200
+ }
package/tsconfig.json CHANGED
@@ -6,8 +6,11 @@
6
6
  "compilerOptions": {
7
7
  "rootDir": "./src",
8
8
  "outDir": "./dist",
9
- "types": ["node"],
10
- "composite": true
9
+ "composite": true,
10
+ "types": [
11
+ "mocha",
12
+ "node"
13
+ ]
11
14
  },
12
15
  "include": [
13
16
  "src/**/*"