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 | import * as log from 'lib0/logging'
|
47 | import { simpleDiffString } from './diff.js'
|
48 | import * as object from './object.js'
|
49 | import * as string from './string.js'
|
50 | import * as math from './math.js'
|
51 | import * as random from './random.js'
|
52 | import * as prng from './prng.js'
|
53 | import * as statistics from './statistics.js'
|
54 | import * as array from './array.js'
|
55 | import * as env from './environment.js'
|
56 | import * as json from './json.js'
|
57 | import * as time from './time.js'
|
58 | import * as promise from './promise.js'
|
59 | import * as performance from 'lib0/performance'
|
60 |
|
61 | export { production } from './environment.js'
|
62 |
|
63 | export const extensive = env.hasConf('extensive')
|
64 |
|
65 |
|
66 | export const envSeed = env.hasParam('--seed') ? Number.parseInt(env.getParam('--seed', '0')) : null
|
67 |
|
68 | export class TestCase {
|
69 | |
70 |
|
71 |
|
72 |
|
73 | constructor (moduleName, testName) {
|
74 | |
75 |
|
76 |
|
77 | this.moduleName = moduleName
|
78 | |
79 |
|
80 |
|
81 | this.testName = testName
|
82 | |
83 |
|
84 |
|
85 |
|
86 |
|
87 | this.meta = new Map()
|
88 | this._seed = null
|
89 | this._prng = null
|
90 | }
|
91 |
|
92 | resetSeed () {
|
93 | this._seed = null
|
94 | this._prng = null
|
95 | }
|
96 |
|
97 | |
98 |
|
99 |
|
100 |
|
101 | get seed () {
|
102 |
|
103 | if (this._seed === null) {
|
104 |
|
105 | this._seed = envSeed === null ? random.uint32() : envSeed
|
106 | }
|
107 | return this._seed
|
108 | }
|
109 |
|
110 | |
111 |
|
112 |
|
113 |
|
114 |
|
115 | get prng () {
|
116 |
|
117 | if (this._prng === null) {
|
118 | this._prng = prng.create(this.seed)
|
119 | }
|
120 | return this._prng
|
121 | }
|
122 | }
|
123 |
|
124 | export const repetitionTime = Number(env.getParam('--repetition-time', '50'))
|
125 |
|
126 | const testFilter = env.hasParam('--filter') ? env.getParam('--filter', '') : null
|
127 |
|
128 |
|
129 | const testFilterRegExp = testFilter !== null ? new RegExp(testFilter) : /.*/
|
130 |
|
131 | const repeatTestRegex = /^(repeat|repeating)\s/
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 | export const run = async (moduleName, name, f, i, numberOfTests) => {
|
141 | const uncamelized = string.fromCamelCase(name.slice(4), ' ')
|
142 | const filtered = !testFilterRegExp.test(`[${i + 1}/${numberOfTests}] ${moduleName}: ${uncamelized}`)
|
143 |
|
144 | if (filtered) {
|
145 | return true
|
146 | }
|
147 | const tc = new TestCase(moduleName, name)
|
148 | const repeat = repeatTestRegex.test(uncamelized)
|
149 | const groupArgs = [log.GREY, `[${i + 1}/${numberOfTests}] `, log.PURPLE, `${moduleName}: `, log.BLUE, uncamelized]
|
150 |
|
151 | if (testFilter === null) {
|
152 | log.groupCollapsed(...groupArgs)
|
153 | } else {
|
154 | log.group(...groupArgs)
|
155 | }
|
156 | const times = []
|
157 | const start = performance.now()
|
158 | let lastTime = start
|
159 | |
160 |
|
161 |
|
162 | let err = null
|
163 | performance.mark(`${name}-start`)
|
164 | do {
|
165 | try {
|
166 | const p = f(tc)
|
167 | if (promise.isPromise(p)) {
|
168 | await p
|
169 | }
|
170 | } catch (_err) {
|
171 | err = _err
|
172 | }
|
173 | const currTime = performance.now()
|
174 | times.push(currTime - lastTime)
|
175 | lastTime = currTime
|
176 | if (repeat && err === null && (lastTime - start) < repetitionTime) {
|
177 | tc.resetSeed()
|
178 | } else {
|
179 | break
|
180 | }
|
181 | } while (err === null && (lastTime - start) < repetitionTime)
|
182 | performance.mark(`${name}-end`)
|
183 |
|
184 | if (err !== null && err.constructor !== SkipError) {
|
185 | log.printError(err)
|
186 | }
|
187 | performance.measure(name, `${name}-start`, `${name}-end`)
|
188 | log.groupEnd()
|
189 | const duration = lastTime - start
|
190 | let success = true
|
191 | times.sort((a, b) => a - b)
|
192 |
|
193 | const againMessage = env.isBrowser
|
194 | ? ` - ${window.location.host + window.location.pathname}?filter=\\[${i + 1}/${tc._seed === null ? '' : `&seed=${tc._seed}`}`
|
195 | : `\nrepeat: npm run test -- --filter "\\[${i + 1}/" ${tc._seed === null ? '' : `--seed ${tc._seed}`}`
|
196 | const timeInfo = (repeat && err === null)
|
197 | ? ` - ${times.length} repetitions in ${time.humanizeDuration(duration)} (best: ${time.humanizeDuration(times[0])}, worst: ${time.humanizeDuration(array.last(times))}, median: ${time.humanizeDuration(statistics.median(times))}, average: ${time.humanizeDuration(statistics.average(times))})`
|
198 | : ` in ${time.humanizeDuration(duration)}`
|
199 | if (err !== null) {
|
200 |
|
201 | if (err.constructor === SkipError) {
|
202 | log.print(log.GREY, log.BOLD, 'Skipped: ', log.UNBOLD, uncamelized)
|
203 | } else {
|
204 | success = false
|
205 | log.print(log.RED, log.BOLD, 'Failure: ', log.UNBOLD, log.UNCOLOR, uncamelized, log.GREY, timeInfo, againMessage)
|
206 | }
|
207 |
|
208 | } else {
|
209 | log.print(log.GREEN, log.BOLD, 'Success: ', log.UNBOLD, log.UNCOLOR, uncamelized, log.GREY, timeInfo, againMessage)
|
210 | }
|
211 | return success
|
212 | }
|
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 | export const describe = (description, info = '') => log.print(log.BLUE, description, ' ', log.GREY, info)
|
227 |
|
228 |
|
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 |
|
236 |
|
237 |
|
238 | export const info = info => describe('', info)
|
239 |
|
240 | export const printDom = log.printDom
|
241 |
|
242 | export const printCanvas = log.printCanvas
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
256 |
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 | export const group = (description, f) => {
|
263 | log.group(log.BLUE, description)
|
264 | try {
|
265 | f()
|
266 | } finally {
|
267 | log.groupEnd()
|
268 | }
|
269 | }
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 | export const groupAsync = async (description, f) => {
|
290 | log.group(log.BLUE, description)
|
291 | try {
|
292 | await f()
|
293 | } finally {
|
294 | log.groupEnd()
|
295 | }
|
296 | }
|
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 |
|
303 |
|
304 |
|
305 |
|
306 |
|
307 |
|
308 |
|
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
314 |
|
315 |
|
316 | export const measureTime = (message, f) => {
|
317 | let duration
|
318 | const start = performance.now()
|
319 | try {
|
320 | f()
|
321 | } finally {
|
322 | duration = performance.now() - start
|
323 | log.print(log.PURPLE, message, log.GREY, ` ${time.humanizeDuration(duration)}`)
|
324 | }
|
325 | return duration
|
326 | }
|
327 |
|
328 |
|
329 |
|
330 |
|
331 |
|
332 |
|
333 |
|
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 |
|
341 |
|
342 |
|
343 |
|
344 |
|
345 |
|
346 | export const measureTimeAsync = async (message, f) => {
|
347 | let duration
|
348 | const start = performance.now()
|
349 | try {
|
350 | await f()
|
351 | } finally {
|
352 | duration = performance.now() - start
|
353 | log.print(log.PURPLE, message, log.GREY, ` ${time.humanizeDuration(duration)}`)
|
354 | }
|
355 | return duration
|
356 | }
|
357 |
|
358 |
|
359 |
|
360 |
|
361 |
|
362 |
|
363 |
|
364 |
|
365 | export const compareArrays = (as, bs, m = 'Arrays match') => {
|
366 | if (as.length !== bs.length) {
|
367 | fail(m)
|
368 | }
|
369 | for (let i = 0; i < as.length; i++) {
|
370 | if (as[i] !== bs[i]) {
|
371 | fail(m)
|
372 | }
|
373 | }
|
374 | return true
|
375 | }
|
376 |
|
377 |
|
378 |
|
379 |
|
380 |
|
381 |
|
382 |
|
383 | export const compareStrings = (a, b, m = 'Strings match') => {
|
384 | if (a !== b) {
|
385 | const diff = simpleDiffString(a, b)
|
386 | log.print(log.GREY, a.slice(0, diff.index), log.RED, a.slice(diff.index, diff.remove), log.GREEN, diff.insert, log.GREY, a.slice(diff.index + diff.remove))
|
387 | fail(m)
|
388 | }
|
389 | }
|
390 |
|
391 |
|
392 |
|
393 |
|
394 |
|
395 |
|
396 |
|
397 |
|
398 | export const compareObjects = (a, b, m = 'Objects match') => { object.equalFlat(a, b) || fail(m) }
|
399 |
|
400 |
|
401 |
|
402 |
|
403 |
|
404 |
|
405 |
|
406 |
|
407 | const compareValues = (_constructor, a, b, path) => {
|
408 | if (a !== b) {
|
409 | fail(`Values ${json.stringify(a)} and ${json.stringify(b)} don't match (${path})`)
|
410 | }
|
411 | return true
|
412 | }
|
413 |
|
414 |
|
415 |
|
416 |
|
417 |
|
418 |
|
419 |
|
420 | const _failMessage = (message, reason, path) => fail(
|
421 | message === null
|
422 | ? `${reason} ${path}`
|
423 | : `${message} (${reason}) ${path}`
|
424 | )
|
425 |
|
426 |
|
427 |
|
428 |
|
429 |
|
430 |
|
431 |
|
432 |
|
433 | const _compare = (a, b, path, message, customCompare) => {
|
434 |
|
435 | if (a == null || b == null) {
|
436 | return compareValues(null, a, b, path)
|
437 | }
|
438 | if (a.constructor !== b.constructor) {
|
439 | _failMessage(message, 'Constructors don\'t match', path)
|
440 | }
|
441 | let success = true
|
442 | switch (a.constructor) {
|
443 | case ArrayBuffer:
|
444 | a = new Uint8Array(a)
|
445 | b = new Uint8Array(b)
|
446 |
|
447 | case Uint8Array: {
|
448 | if (a.byteLength !== b.byteLength) {
|
449 | _failMessage(message, 'ArrayBuffer lengths match', path)
|
450 | }
|
451 | for (let i = 0; success && i < a.length; i++) {
|
452 | success = success && a[i] === b[i]
|
453 | }
|
454 | break
|
455 | }
|
456 | case Set: {
|
457 | if (a.size !== b.size) {
|
458 | _failMessage(message, 'Sets have different number of attributes', path)
|
459 | }
|
460 |
|
461 | a.forEach(value => {
|
462 | if (!b.has(value)) {
|
463 | _failMessage(message, `b.${path} does have ${value}`, path)
|
464 | }
|
465 | })
|
466 | break
|
467 | }
|
468 | case Map: {
|
469 | if (a.size !== b.size) {
|
470 | _failMessage(message, 'Maps have different number of attributes', path)
|
471 | }
|
472 |
|
473 | a.forEach((value, key) => {
|
474 | if (!b.has(key)) {
|
475 | _failMessage(message, `Property ${path}["${key}"] does not exist on second argument`, path)
|
476 | }
|
477 | _compare(value, b.get(key), `${path}["${key}"]`, message, customCompare)
|
478 | })
|
479 | break
|
480 | }
|
481 | case Object:
|
482 | if (object.length(a) !== object.length(b)) {
|
483 | _failMessage(message, 'Objects have a different number of attributes', path)
|
484 | }
|
485 | object.forEach(a, (value, key) => {
|
486 | if (!object.hasProperty(b, key)) {
|
487 | _failMessage(message, `Property ${path} does not exist on second argument`, path)
|
488 | }
|
489 | _compare(value, b[key], `${path}["${key}"]`, message, customCompare)
|
490 | })
|
491 | break
|
492 | case Array:
|
493 | if (a.length !== b.length) {
|
494 | _failMessage(message, 'Arrays have a different number of attributes', path)
|
495 | }
|
496 |
|
497 | a.forEach((value, i) => _compare(value, b[i], `${path}[${i}]`, message, customCompare))
|
498 | break
|
499 |
|
500 | default:
|
501 | if (!customCompare(a.constructor, a, b, path, compareValues)) {
|
502 | _failMessage(message, `Values ${json.stringify(a)} and ${json.stringify(b)} don't match`, path)
|
503 | }
|
504 | }
|
505 | assert(success, message)
|
506 | return true
|
507 | }
|
508 |
|
509 |
|
510 |
|
511 |
|
512 |
|
513 |
|
514 |
|
515 |
|
516 | export const compare = (a, b, message = null, customCompare = compareValues) => _compare(a, b, 'obj', message, customCompare)
|
517 |
|
518 |
|
519 |
|
520 |
|
521 |
|
522 |
|
523 |
|
524 |
|
525 |
|
526 | export const assert = (property, message = null) => { property || fail(`Assertion failed${message !== null ? `: ${message}` : ''}`) }
|
527 |
|
528 |
|
529 |
|
530 |
|
531 | export const promiseRejected = async f => {
|
532 | try {
|
533 | await f()
|
534 | } catch (err) {
|
535 | return
|
536 | }
|
537 | fail('Expected promise to fail')
|
538 | }
|
539 |
|
540 |
|
541 |
|
542 |
|
543 |
|
544 | export const fails = f => {
|
545 | try {
|
546 | f()
|
547 | } catch (_err) {
|
548 | log.print(log.GREEN, '⇖ This Error was expected')
|
549 | return
|
550 | }
|
551 | fail('Expected this to fail')
|
552 | }
|
553 |
|
554 |
|
555 |
|
556 |
|
557 |
|
558 | export const failsAsync = async f => {
|
559 | try {
|
560 | await f()
|
561 | } catch (_err) {
|
562 | log.print(log.GREEN, '⇖ This Error was expected')
|
563 | return
|
564 | }
|
565 | fail('Expected this to fail')
|
566 | }
|
567 |
|
568 |
|
569 |
|
570 |
|
571 | export const runTests = async tests => {
|
572 | |
573 |
|
574 |
|
575 | const filterTest = testname => testname.startsWith('test') || testname.startsWith('benchmark')
|
576 | const numberOfTests = object.map(tests, mod => object.map(mod, (f, fname) => f && filterTest(fname) ? 1 : 0).reduce(math.add, 0)).reduce(math.add, 0)
|
577 | let successfulTests = 0
|
578 | let testnumber = 0
|
579 | const start = performance.now()
|
580 | for (const modName in tests) {
|
581 | const mod = tests[modName]
|
582 | for (const fname in mod) {
|
583 | const f = mod[fname]
|
584 |
|
585 | if (f && filterTest(fname)) {
|
586 | const repeatEachTest = 1
|
587 | let success = true
|
588 | for (let i = 0; success && i < repeatEachTest; i++) {
|
589 | success = await run(modName, fname, f, testnumber, numberOfTests)
|
590 | }
|
591 | testnumber++
|
592 |
|
593 | if (success) {
|
594 | successfulTests++
|
595 | }
|
596 | }
|
597 | }
|
598 | }
|
599 | const end = performance.now()
|
600 | log.print('')
|
601 | const success = successfulTests === numberOfTests
|
602 |
|
603 | if (success) {
|
604 | log.print(log.GREEN, log.BOLD, 'All tests successful!', log.GREY, log.UNBOLD, ` in ${time.humanizeDuration(end - start)}`)
|
605 | log.printImgBase64(nyanCatImage, 50)
|
606 | } else {
|
607 | const failedTests = numberOfTests - successfulTests
|
608 | log.print(log.RED, log.BOLD, `> ${failedTests} test${failedTests > 1 ? 's' : ''} failed`)
|
609 | }
|
610 |
|
611 | return success
|
612 | }
|
613 |
|
614 | class TestError extends Error {}
|
615 |
|
616 |
|
617 |
|
618 |
|
619 |
|
620 | export const fail = reason => {
|
621 | log.print(log.RED, log.BOLD, 'X ', log.UNBOLD, reason)
|
622 | throw new TestError('Test Failed')
|
623 | }
|
624 |
|
625 | class SkipError extends Error {}
|
626 |
|
627 |
|
628 |
|
629 |
|
630 |
|
631 | export const skip = (cond = true) => {
|
632 | if (cond) {
|
633 | throw new SkipError('skipping..')
|
634 | }
|
635 | }
|
636 |
|
637 |
|
638 | const nyanCatImage = 'R0lGODlhjABMAPcAAMiSE0xMTEzMzUKJzjQ0NFsoKPc7//FM/9mH/z9x0HIiIoKCgmBHN+frGSkZLdDQ0LCwsDk71g0KCUzDdrQQEOFz/8yYdelmBdTiHFxcXDU2erR/mLrTHCgoKK5szBQUFNgSCTk6ymfpCB9VZS2Bl+cGBt2N8kWm0uDcGXhZRUvGq94NCFPhDiwsLGVlZTgqIPMDA1g3aEzS5D6xAURERDtG9JmBjJsZGWs2AD1W6Hp6eswyDeJ4CFNTU1LcEoJRmTMzSd14CTg5ser2GmDzBd17/xkZGUzMvoSMDiEhIfKruCwNAJaWlvRzA8kNDXDrCfi0pe1U/+GS6SZrAB4eHpZwVhoabsx9oiYmJt/TGHFxcYyMjOid0+Zl/0rF6j09PeRr/0zU9DxO6j+z0lXtBtp8qJhMAEssLGhoaPL/GVn/AAsWJ/9/AE3Z/zs9/3cAAOlf/+aa2RIyADo85uhh/0i84WtrazQ0UyMlmDMzPwUFBe16BTMmHau0E03X+g8pMEAoS1MBAf++kkzO8pBaqSZoe9uB/zE0BUQ3Sv///4WFheuiyzo880gzNDIyNissBNqF/8RiAOF2qG5ubj0vL1z6Avl5ASsgGkgUSy8vL/8n/z4zJy8lOv96uEssV1csAN5ZCDQ0Wz1a3tbEGHLeDdYKCg4PATE7PiMVFSoqU83eHEi43gUPAOZ8reGogeKU5dBBC8faHEez2lHYF4bQFMukFtl4CzY3kkzBVJfMGZkAAMfSFf27mP0t//g4/9R6Dfsy/1DRIUnSAPRD/0fMAFQ0Q+l7rnbaD0vEntCDD6rSGtO8GNpUCU/MK07LPNEfC7RaABUWWkgtOst+71v9AfD7GfDw8P19ATtA/NJpAONgB9yL+fm6jzIxMdnNGJxht1/2A9x//9jHGOSX3+5tBP27l35+fk5OTvZ9AhYgTjo0PUhGSDs9+LZjCFf2Aw0IDwcVAA8PD5lwg9+Q7YaChC0kJP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/wtYTVAgRGF0YVhNUDw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpGNEM2MUEyMzE0QTRFMTExOUQzRkE3QTBCRDNBMjdBQyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpERjQ0NEY0QkI2MTcxMUUxOUJEQkUzNUNGQTkwRTU2MiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpERjQ0NEY0QUI2MTcxMUUxOUJEQkUzNUNGQTkwRTU2MiIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo1OEE3RTIwRjcyQTlFMTExOTQ1QkY2QTU5QzVCQjJBOSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpGNEM2MUEyMzE0QTRFMTExOUQzRkE3QTBCRDNBMjdBQyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PgH//v38+/r5+Pf29fTz8vHw7+7t7Ovq6ejn5uXk4+Lh4N/e3dzb2tnY19bV1NPS0dDPzs3My8rJyMfGxcTDwsHAv769vLu6ubi3trW0s7KxsK+urayrqqmop6alpKOioaCfnp2cm5qZmJeWlZSTkpGQj46NjIuKiYiHhoWEg4KBgH9+fXx7enl4d3Z1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'
|