class BaseTest { numberDifferenceRatio = 0.000_001; setUp() {} tearDown() {} /** @var {TestCaseRunner|null} */ currentRunner = null; fail(failMessage=null) { throw new FailureError(failMessage || 'failed'); } assertTrue(test, failMessage=null) { if (!test) this.fail(failMessage || `expected true, got ${test}`); } assertFalse(test, failMessage=null) { if (test) this.fail(failMessage || `expected false, got ${test}`); } assertEqual(expected, actual, failMessage=null) { if (BaseTest.#equal(expected, actual, this.numberDifferenceRatio)) return; const expectedStr = (typeof expected == 'string') ? `"${expected}"` : `${expected}`; const actualStr = (typeof actual == 'string') ? `"${actual}"` : `${actual}`; if (expectedStr.length > 20 || actualStr.length > 20) { this.fail(failMessage || `equality failed:\nexpected:\n${expectedStr}\n!=\nactual:\n${actualStr}`); } else { this.fail(failMessage || `equality failed: expected ${expectedStr} != actual ${actualStr}`); } } expectError(e=true) { if (this.currentRunner) this.currentRunner.expectedError = e; } /** * @param {number} maxTime maximum time in seconds * @param {function} timedCode code to run and time */ profile(maxTime, timedCode) { const startTime = performance.now(); const result = timedCode(); const endTime = performance.now(); const seconds = (endTime - startTime) / 1000.0; if (seconds > maxTime) { this.fail(`Expected <= ${maxTime}s execution time, actual ${seconds}s.`); } return result; } /** * @param {Array} a * @param {Array} b * @returns {boolean} */ static #equalArrays(a, b) { if (a === b) return true; if (!(a instanceof Array) || !(b instanceof Array)) return false; if (a == null || b == null) return false; if (a.length != b.length) return false; for (var i = 0; i < a.length; i++) { if (!this.#equal(a[i], b[i])) return false; } return true; } /** * @param {object} a * @param {object} b * @returns {boolean} */ static #equalObjects(a, b) { if (a === b) return true; if (!(a instanceof Object) || !(b instanceof Object)) return false; if (a == null || b == null) return false; if (a.equals !== undefined) { return a.equals(b); } for (const key of Object.keys(a)) { if (!this.#equal(a[key], b[key])) return false; } for (const key of Object.keys(b)) { if (!this.#equal(a[key], b[key])) return false; } return true; } /** * @param {number} a * @param {number} b * @returns {boolean} */ static #equalNumbers(a, b, differenceRatio=0.000001) { if (a === b) return true; const largestAllowableDelta = Math.max(Math.abs(a), Math.abs(b)) * differenceRatio; const delta = Math.abs(a - b); return delta <= largestAllowableDelta; } /** * Tests for equality on lots of different kinds of values including objects * and arrays. Will use `.equals` on objects that implement it. * * @param {any} a * @param {any} b * @param {number} numberDifferenceRatio * @returns {boolean} */ static #equal(a, b, numberDifferenceRatio=0.000_001) { if (a instanceof Array && b instanceof Array) { return this.#equalArrays(a, b); } if (a instanceof Object && b instanceof Object) { return this.#equalObjects(a, b); } if (typeof a === 'number' && typeof b === 'number') { return this.#equalNumbers(a, b, numberDifferenceRatio); } return a === b; } }