/
describe_spec.ts
313 lines (289 loc) · 9.58 KB
/
describe_spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import stringify from 'json-stable-stringify';
import { ExclusiveTestFunction, PendingTestFunction } from 'mocha';
import { queryEquals, QueryImpl } from '../../../src/core/query';
import { targetEquals, TargetImpl } from '../../../src/core/target';
import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence';
import { debugAssert } from '../../../src/util/assert';
import { primitiveComparator } from '../../../src/util/misc';
import { addEqualityMatcher } from '../../util/equality_matcher';
import { SpecBuilder } from './spec_builder';
import { SpecStep } from './spec_test_runner';
// Disables all other tests; useful for debugging. Multiple tests can have
// this tag and they'll all be run (but all others won't).
const EXCLUSIVE_TAG = 'exclusive';
// Explicit per-platform disable flags.
const NO_WEB_TAG = 'no-web';
const NO_ANDROID_TAG = 'no-android';
const NO_IOS_TAG = 'no-ios';
// The remaining tags specify features that must be present to run a given test
// Multi-client related tests (which imply persistence).
export const MULTI_CLIENT_TAG = 'multi-client';
const EAGER_GC_TAG = 'eager-gc';
const DURABLE_PERSISTENCE_TAG = 'durable-persistence';
const BENCHMARK_TAG = 'benchmark';
const KNOWN_TAGS = [
BENCHMARK_TAG,
EXCLUSIVE_TAG,
MULTI_CLIENT_TAG,
NO_WEB_TAG,
NO_ANDROID_TAG,
NO_IOS_TAG,
EAGER_GC_TAG,
DURABLE_PERSISTENCE_TAG
];
// TODO(mrschmidt): Make this configurable with mocha options.
const RUN_BENCHMARK_TESTS = false;
const BENCHMARK_TEST_TIMEOUT_MS = 10 * 1000;
// The format of one describeSpec written to a JSON file.
interface SpecOutputFormat {
describeName: string;
itName: string;
tags: string[];
comment?: string;
steps: SpecStep[];
}
// The name of the describeSpec that's currently running.
let describeName = '';
// Tags for the describeSpec that's current running.
let describeTags: string[] = [];
// A map of string name -> spec json for every `it` in this `describe`.
let specsInThisTest: { [name: string]: SpecOutputFormat };
// A function to write the specs with, if set.
let writeJSONFile: ((json: string) => void) | null = null;
/**
* If you call this function before your describeSpec, then the spec test will
* be written using the given function instead of running as a normal test.
*/
export function setSpecJSONHandler(writer: (json: string) => void): void {
writeJSONFile = writer;
}
/** Gets the test runner based on the specified tags. */
function getTestRunner(
tags: string[],
persistenceEnabled: boolean
): ExclusiveTestFunction | PendingTestFunction {
if (tags.indexOf(NO_WEB_TAG) >= 0) {
// eslint-disable-next-line no-restricted-properties
return it.skip;
} else if (
!persistenceEnabled &&
tags.indexOf(DURABLE_PERSISTENCE_TAG) !== -1
) {
// Test requires actual persistence, but it's not enabled. Skip it.
// eslint-disable-next-line no-restricted-properties
return it.skip;
} else if (persistenceEnabled && tags.indexOf(EAGER_GC_TAG) !== -1) {
// spec should have a comment explaining why it is being skipped.
// eslint-disable-next-line no-restricted-properties
return it.skip;
} else if (!persistenceEnabled && tags.indexOf(MULTI_CLIENT_TAG) !== -1) {
// eslint-disable-next-line no-restricted-properties
return it.skip;
} else if (tags.indexOf(BENCHMARK_TAG) >= 0 && !RUN_BENCHMARK_TESTS) {
// eslint-disable-next-line no-restricted-properties
return it.skip;
} else if (tags.indexOf(EXCLUSIVE_TAG) >= 0) {
// eslint-disable-next-line no-restricted-properties
return it.only;
} else {
return it;
}
}
/** If required, returns a custom test timeout for long-running tests */
function getTestTimeout(tags: string[]): number | undefined {
if (tags.indexOf(BENCHMARK_TAG) >= 0) {
return BENCHMARK_TEST_TIMEOUT_MS;
} else {
return undefined;
}
}
/**
* Like it(), but for spec tests.
* @param name - A name to give the test.
* @param tags - Tags to apply to the test (e.g. 'exclusive' to only run
* individual tests)
* @param builder - A function that returns a spec.
* If writeToJSONFile has been called, the spec will be stored in
* `specsInThisTest`. Otherwise, it will be run, just as it() would run it.
*/
export function specTest(
name: string,
tags: string[],
builder: () => SpecBuilder
): void;
export function specTest(
name: string,
tags: string[],
comment: string,
builder: () => SpecBuilder
): void;
export function specTest(
name: string,
tags: string[],
commentOrBuilder: string | (() => SpecBuilder),
maybeBuilder?: () => SpecBuilder
): void {
let comment: string | undefined;
let builder: () => SpecBuilder;
if (typeof commentOrBuilder === 'string') {
comment = commentOrBuilder;
builder = maybeBuilder!;
} else {
builder = commentOrBuilder;
}
debugAssert(!!builder, 'Missing spec builder');
// Union in the tags for the describeSpec().
tags = tags.concat(describeTags);
for (const tag of tags) {
debugAssert(
KNOWN_TAGS.indexOf(tag) >= 0,
'Unknown tag "' + tag + '" on test: ' + name
);
}
if (!writeJSONFile) {
const persistenceModes = IndexedDbPersistence.isAvailable()
? [true, false]
: [false];
for (const usePersistence of persistenceModes) {
const runner = getTestRunner(tags, usePersistence);
const timeout = getTestTimeout(tags);
const mode = usePersistence ? '(Persistence)' : '(Memory)';
const fullName = `${mode} ${name}`;
const queuedTest = runner(fullName, async () => {
const spec = builder();
const start = Date.now();
await spec.runAsTest(fullName, tags, usePersistence);
const end = Date.now();
if (tags.indexOf(BENCHMARK_TAG) >= 0) {
// eslint-disable-next-line no-console
console.log(`Runtime: ${end - start} ms.`);
}
});
if (timeout !== undefined) {
queuedTest.timeout(timeout);
}
}
} else {
debugAssert(
tags.indexOf(EXCLUSIVE_TAG) === -1,
`The 'exclusive' tag is only supported for development and should not be exported to ` +
`other platforms.`
);
const spec = builder();
const specJSON = spec.toJSON();
const json = {
describeName,
itName: name,
tags,
comment,
config: specJSON.config,
steps: specJSON.steps
};
if (name in specsInThisTest) {
throw new Error('duplicate spec test: "' + name + '"');
}
specsInThisTest[name] = json;
}
}
/**
* Like describe, but for spec tests.
* @param name - A name to give the test.
* @param tags - Tags to apply to all tests in the spec (e.g. 'exclusive' to
* only run individual tests)
* @param builder - A function that calls specTest for each test case.
* If writeToJSONFile has been called, the specs will be stored in
* that file. Otherwise, they will be run, just as describe would run.
*/
export function describeSpec(
name: string,
tags: string[],
builder: () => void
): void {
describeTags = tags;
describeName = name;
if (!writeJSONFile) {
describe(name, () => {
addEqualityMatcher(
{ equalsFn: targetEquals, forType: TargetImpl },
{ equalsFn: queryEquals, forType: QueryImpl }
);
return builder();
});
} else {
specsInThisTest = {};
builder();
// Note: We use json-stable-stringify instead of JSON.stringify() to ensure
// that the generated JSON does not produce diffs merely due to the order
// of the keys in an object changing.
const output = stringify(specsInThisTest, {
space: 2,
cmp: stringifyComparator
});
writeJSONFile(output);
}
}
/**
* The key ordering overrides used when sorting keys in the generated JSON.
* If both keys being compared are present in this array then they should be
* ordered in the generated JSON in the same relative order of this array.
*/
const stringifyCustomOrdering = [
'comment',
'describeName',
'itName',
'tags',
'config',
'steps'
];
/**
* Assigns a "group number" to the given key in the generated JSON.
*
* Keys with lower group numbers should be ordered before keys with greater
* group numbers. Keys with equal group numbers should be ordered
* alphabetically.
*/
function stringifyGroup(s: string): number {
const index = stringifyCustomOrdering.indexOf(s);
if (index >= 0) {
return index;
} else if (!s.startsWith('expected')) {
return stringifyCustomOrdering.length;
} else {
return stringifyCustomOrdering.length + 1;
}
}
/**
* A comparator function for stringify() that sorts keys in the JSON output.
*
* In order to produce JSON that has somewhat-intuitively-ordered keys, this
* comparator intentionally deviates from pure alphabetic ordering, placing
* some logically-first keys before others.
*/
function stringifyComparator(
a: stringify.Element,
b: stringify.Element
): number {
const aGroup = stringifyGroup(a.key);
const bGroup = stringifyGroup(b.key);
if (aGroup === bGroup) {
return primitiveComparator(a.key, b.key);
} else {
return primitiveComparator(aGroup, bGroup);
}
}