diff --git a/package-lock.json b/package-lock.json index 7be002a0..00e802b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5031,10 +5031,37 @@ "dev": true }, "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "dev": true + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "dev": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } }, "node-int64": { "version": "0.4.0", diff --git a/package.json b/package.json index 9310f51f..9cce3f8a 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "jest-localstorage-mock": "^2.4.3", "js-yaml": "^3.14.0", "lodash": "^4.17.21", - "node-fetch": "^2.6.1", + "node-fetch": "^2.6.7", "redis-server": "1.2.2", "rimraf": "^3.0.2", "ts-jest": "^27.0.5", diff --git a/src/evaluator/condition/__tests__/engineUtils.spec.ts b/src/evaluator/condition/__tests__/engineUtils.spec.ts index 8682c851..72753bbd 100644 --- a/src/evaluator/condition/__tests__/engineUtils.spec.ts +++ b/src/evaluator/condition/__tests__/engineUtils.spec.ts @@ -18,7 +18,6 @@ test('ENGINE / should always evaluate to "off"', () => { expect(engineUtils.getTreatment(loggerMock, bucketingKey, seed, treatmentsMock) === 'off').toBe(true); // treatment should be 'off' - let endTime = Date.now(); console.log(`Evaluation takes ${(endTime - startTime) / 1000} seconds`); @@ -38,28 +37,24 @@ test('ENGINE / should always evaluate to "on"', () => { }); test('ENGINE / shouldApplyRollout - trafficAllocation 100', () => { - const shouldApplyRollout = engineUtils.shouldApplyRollout(100, 'asd', 14); expect(shouldApplyRollout).toBe(true); // Should return true as traffic allocation is 100. }); test('ENGINE / shouldApplyRollout - algo murmur | trafficAllocation 53 | bucket 51', () => { - const shouldApplyRollout = engineUtils.shouldApplyRollout(53, 'a', 29); expect(shouldApplyRollout).toBe(true); // Should return true as traffic allocation is 100. }); test('ENGINE / shouldApplyRollout - algo murmur | trafficAllocation 53 | bucket 56', () => { - const shouldApplyRollout = engineUtils.shouldApplyRollout(53, 'a', 31); expect(shouldApplyRollout).toBe(false); // Should return false as bucket is higher than trafficAllocation. }); test('ENGINE / shouldApplyRollout - algo murmur | trafficAllocation 1 | bucket 1', () => { - const shouldApplyRollout = engineUtils.shouldApplyRollout(1, 'aaaaaaklmnbv', -1667452163); expect(shouldApplyRollout).toBe(true); // Should return true as bucket is higher or equal than trafficAllocation. diff --git a/src/sync/streaming/__tests__/mySegmentsV2utils.spec.ts b/src/sync/streaming/__tests__/mySegmentsV2utils.spec.ts index b2826848..b24c6cad 100644 --- a/src/sync/streaming/__tests__/mySegmentsV2utils.spec.ts +++ b/src/sync/streaming/__tests__/mySegmentsV2utils.spec.ts @@ -16,19 +16,19 @@ test('parseKeyList', () => { addedUserKeys.forEach(userKey => { const hash = hash64(userKey); expect(added.has(hash.dec)).toBe(true); // key hash belongs to added list - expect(removed.has(hash.dec)).toBe(false); // t belong to removed list + expect(removed.has(hash.dec)).toBe(false); // key hash doesn\'t belong to removed list }); removedUserKeys.forEach(userKey => { const hash = hash64(userKey); - expect(added.has(hash.dec)).toBe(false); // t belong to added list + expect(added.has(hash.dec)).toBe(false); // key hash doesn\'t belong to added list expect(removed.has(hash.dec)).toBe(true); // key hash belongs to removed list }); otherUserKeys.forEach(userKey => { const hash = hash64(userKey); - expect(added.has(hash.dec)).toBe(false); // t belong to added list - expect(removed.has(hash.dec)).toBe(false); // t belong to removed list + expect(added.has(hash.dec)).toBe(false); // key hash doesn\'t belong to added list + expect(removed.has(hash.dec)).toBe(false); // key hash doesn\'t belong to removed list }); }); }); @@ -47,7 +47,7 @@ test('parseBitmap & isInBitmap', () => { falseUserKeys.forEach(userKey => { const hash = hash64(userKey); - expect(isInBitmap(actualBitmap, hash.hex)).toBe(false); // t belong to Bitmap + expect(isInBitmap(actualBitmap, hash.hex)).toBe(false); // key hash doesn\'t belong to Bitmap }); }); }); diff --git a/src/utils/__tests__/Backoff.spec.ts b/src/utils/__tests__/Backoff.spec.ts new file mode 100644 index 00000000..7fd5c083 --- /dev/null +++ b/src/utils/__tests__/Backoff.spec.ts @@ -0,0 +1,44 @@ +import { Backoff } from '../Backoff'; + +test('Backoff', (done) => { + + let start = Date.now(); + let backoff: Backoff; + + let alreadyReset = false; + const callback = () => { + const delta = Date.now() - start; + start += delta; + const expectedMillis = Math.min(backoff.baseMillis * Math.pow(2, backoff.attempts - 1), backoff.maxMillis); + + expect(delta > expectedMillis - 20 && delta < expectedMillis + 20).toBe(true); // executes callback at expected time + if (backoff.attempts <= 3) { + backoff.scheduleCall(); + } else { + backoff.reset(); + expect(backoff.attempts).toBe(0); // restarts attempts when `reset` called + expect(backoff.timeoutID).toBe(undefined); // restarts timeoutId when `reset` called + + // init the schedule cycle or finish the test + if (alreadyReset) { + done(); + } else { + alreadyReset = true; + backoff.scheduleCall(); + } + } + }; + + backoff = new Backoff(callback); + expect(backoff.cb).toBe(callback); // contains given callback + expect(backoff.baseMillis).toBe(Backoff.DEFAULT_BASE_MILLIS); // contains default baseMillis + expect(backoff.maxMillis).toBe(Backoff.DEFAULT_MAX_MILLIS); // contains default maxMillis + + const CUSTOM_BASE = 200; + const CUSTOM_MAX = 700; + backoff = new Backoff(callback, CUSTOM_BASE, CUSTOM_MAX); + expect(backoff.baseMillis).toBe(CUSTOM_BASE); // contains given baseMillis + expect(backoff.maxMillis).toBe(CUSTOM_MAX); // contains given maxMillis + + expect(backoff.scheduleCall()).toBe(backoff.baseMillis); // scheduleCall returns the scheduled delay time +}); diff --git a/src/utils/key/__tests__/index.spec.ts.spec.ts b/src/utils/key/__tests__/index.spec.ts similarity index 73% rename from src/utils/key/__tests__/index.spec.ts.spec.ts rename to src/utils/key/__tests__/index.spec.ts index ba1257f6..cd76e944 100644 --- a/src/utils/key/__tests__/index.spec.ts.spec.ts +++ b/src/utils/key/__tests__/index.spec.ts @@ -1,7 +1,6 @@ -import { SplitIO } from '../../../types'; import { keyParser, getMatching, getBucketing } from '../index'; -test('keyParser / if a string is passed as param, it should return a object', () => { +test('keyParser / if a string is passed as a param, it should return a object', () => { const key = 'some key'; const keyParsed = keyParser(key); @@ -24,7 +23,7 @@ test('keyParser / should return the keys configurations', () => { expect(keyParsed.bucketingKey).toBe(bucketingKey); // matching key should be equal to bucketingKey }); -test('getMatching / if a string is passed as a param it should return a string', () => { +test('getMatching / if a string is passed as a param, it should return a string', () => { const key = 'some key'; const keyParsed = getMatching(key); @@ -33,7 +32,7 @@ test('getMatching / if a string is passed as a param it should return a string', expect(keyParsed).toBe(key); // key parsed should be equal to key }); -test('getMatching / if a object is passed as a param it should return a string', () => { +test('getMatching / if a object is passed as a param, it should return a string', () => { const key = { matchingKey: 'some key', @@ -46,7 +45,7 @@ test('getMatching / if a object is passed as a param it should return a string', expect(keyParsed).toBe(key.matchingKey); // key parsed should be equal to key }); -test('getBucketing / should return undefined if a string is passed as a param and return undefined is set', () => { +test('getBucketing / if a string is passed as a param, it should return undefined', () => { const key = 'simple key'; @@ -55,11 +54,11 @@ test('getBucketing / should return undefined if a string is passed as a param an expect(keyParsed).toBe(undefined); // key parsed should return undefined }); -test('getBucketing / should return undefined if a string is passed as a param and return undefined is set', () => { +test('getBucketing / if a object is passed as a param, it should return the bucketingKey value', () => { - const key = { bucketingKey: 'test_buck_key' }; + const key: any = { bucketingKey: 'test_buck_key' }; - const keyParsed = getBucketing(key as SplitIO.SplitKeyObject); + const keyParsed = getBucketing(key); - expect(keyParsed).toBe('test_buck_key'); // key parsed should return undefined + expect(keyParsed).toBe('test_buck_key'); // key parsed should return the bucketingKey }); diff --git a/src/utils/lang/__tests__/binarySearch.spec.ts b/src/utils/lang/__tests__/binarySearch.spec.ts new file mode 100644 index 00000000..9b7a5510 --- /dev/null +++ b/src/utils/lang/__tests__/binarySearch.spec.ts @@ -0,0 +1,132 @@ +/** +Copyright 2022 Split Software + +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 { binarySearch } from '../binarySearch'; + +test('BINARY SEARCH / given [1,3,5,7,10] as dataset look for several elements', () => { + let searchFor = binarySearch.bind(null, [1, 3, 5, 7, 10]); + let index = undefined; + let value = -1; + + index = searchFor(value); + expect(index).toBe(0); // expected value 0 + + value++; // 0 + index = searchFor(value); + expect(index).toBe(0); // expected value 0 + + value++; // 1 + index = searchFor(value); + expect(index).toBe(0); // expected value 0 + + value++; // 2 + index = searchFor(value); + expect(index).toBe(0); // expected value 0 + + value++; // 3 + index = searchFor(value); + expect(index).toBe(1); // expected value 1 + + value++; // 4 + index = searchFor(value); + expect(index).toBe(1); // expected value 1 + + value++; // 5 + index = searchFor(value); + expect(index).toBe(2); // expected value 2 + + value++; // 6 + index = searchFor(value); + expect(index).toBe(2); // expected value 2 + + value++; // 7 + index = searchFor(value); + expect(index).toBe(3); // expected value 3 + + value++; // 8 + index = searchFor(value); + expect(index).toBe(3); // expected value 3 + + value++; // 9 + index = searchFor(value); + expect(index).toBe(3); // expected value 3 + + value++; // 10 + index = searchFor(value); + expect(index).toBe(4); // expected value 4 + + value++; // 11 + index = searchFor(value); + expect(index).toBe(4); // expected value 4 + + value++; // 12 + index = searchFor(value); + expect(index).toBe(4); // expected value 4 +}); + +test('BINARY SEARCH / run test using integer keys', () => { + const KEYS = [ + 1000, 1500, 2250, 3375, 5063, + 7594, 11391, 17086, 25629, 38443, + 57665, 86498, 129746, 194620, 291929, + 437894, 656841, 985261, 1477892, 2216838, + 3325257, 4987885, 7481828 + ]; + + let searchFor = binarySearch.bind(null, KEYS); + + let index = searchFor(10); + expect(index).toBe(0); // expected value 0 + + index = searchFor(1001); + expect(index).toBe(0); // expected value 0 + + index = searchFor(1499); + expect(index).toBe(0); // expected value 0 + + index = searchFor(1500); + expect(index).toBe(1); // expected value 1 + + index = searchFor(2200); + expect(index).toBe(1); // expected value 1 + + index = searchFor(2251); + expect(index).toBe(2); // expected value 2 +}); + +test('BINARY SEARCH / run test using float keys', () => { + const KEYS = [ + 1, 1.5, 2.25, 3.38, 5.06, 7.59, 11.39, 17.09, 25.63, 38.44, + 57.67, 86.5, 129.75, 194.62, 291.93, 437.89, 656.84, 985.26, 1477.89, + 2216.84, 3325.26, 4987.89, 7481.83 + ]; + + let searchFor = binarySearch.bind(null, KEYS); + + let index = searchFor(3.38); + expect(index).toBe(3); // expected value 3 + + index = searchFor(6); + expect(index).toBe(4); // expected value 4 + + index = searchFor(500.55); + expect(index).toBe(15); // expected value 15 + + index = searchFor(7481.83); + expect(index).toBe(22); // expected value 22 + + index = searchFor(80000); + expect(index).toBe(22); // expected value 22 +}); diff --git a/src/utils/murmur3/__tests__/legacy.spec.ts b/src/utils/murmur3/__tests__/legacy.spec.ts new file mode 100644 index 00000000..39ad41cb --- /dev/null +++ b/src/utils/murmur3/__tests__/legacy.spec.ts @@ -0,0 +1,21 @@ +// @ts-nocheck +import * as utils from '../legacy'; +import csv from 'csv-streamify'; +import fs from 'fs'; + +test('ENGINE / validate hashing behavior using sample data', (done) => { + let parser = csv({ objectMode: false }); + + parser.on('data', (line) => { + let [seed, key, hash, bucket] = JSON.parse(line.toString('utf8').trim()); + + seed = parseInt(seed, 10); + hash = parseInt(hash, 10); + bucket = parseInt(bucket, 10); + + expect(utils.hash(key, seed)).toBe(hash); // matching using int32 hash value + expect(utils.bucket(key, seed)).toBe(bucket); // matching using int32 bucket value + }).on('end', done); + + fs.createReadStream(require.resolve('./mocks/small-data.csv')).pipe(parser); +}); diff --git a/src/utils/murmur3/legacy.ts b/src/utils/murmur3/legacy.ts new file mode 100644 index 00000000..1797fb47 --- /dev/null +++ b/src/utils/murmur3/legacy.ts @@ -0,0 +1,48 @@ +// Deprecated hashing function, used for split bucketing. Replaced by murmur3 + +// +// JAVA reference implementation for the hashing function. +// +// int h = 0; +// for (int i = 0; i < key.length(); i++) { +// h = 31 * h + key.charAt(i); +// } +// return h ^ seed; // XOR the hash and seed +// + +function ToInteger(x: number) { + x = Number(x); + return x < 0 ? Math.ceil(x) : Math.floor(x); +} + +function modulo(a: number, b: number) { + return a - Math.floor(a / b) * b; +} + +function ToUint32(x: number) { + return modulo(ToInteger(x), Math.pow(2, 32)); +} + +function ToInt32(x: number) { + let uint32 = ToUint32(x); + + if (uint32 >= Math.pow(2, 31)) { + return uint32 - Math.pow(2, 32); + } else { + return uint32; + } +} + +export function hash(str: string, seed: number): number { + let h = 0; + + for (let i = 0; i < str.length; i++) { + h = ToInt32(ToInt32(31 * h) + str.charCodeAt(i)); + } + + return ToInt32(h ^ seed); +} + +export function bucket(str: string, seed: number): number { + return Math.abs(hash(str, seed) % 100) + 1; +} diff --git a/src/utils/promise/__tests__/thenable.spec.ts b/src/utils/promise/__tests__/thenable.spec.ts new file mode 100644 index 00000000..82dd9ff9 --- /dev/null +++ b/src/utils/promise/__tests__/thenable.spec.ts @@ -0,0 +1,22 @@ +import { thenable } from '../thenable'; + +test('Promise utils / thenable', () => { + const prom = new Promise(() => { }); + const promResolved = Promise.resolve(); + const promRejected = Promise.reject(); + const thenableThing = { then: () => { } }; + const nonThenableThing = { then: 'not a function' }; + + expect(thenable(prom)).toBe(true); // Promises and thenable-like objects should pass the test. + expect(thenable(promResolved)).toBe(true); // Promises and thenable-like objects should pass the test. + expect(thenable(promRejected)).toBe(true); // Promises and thenable-like objects should pass the test. + expect(thenable(thenableThing)).toBe(true); // Promises and thenable-like objects should pass the test. + + expect(thenable(nonThenableThing)).toBe(false); // Non thenable objects should fail the test. + expect(thenable('string')).toBe(false); // Non thenable objects should fail the test. + expect(thenable(123)).toBe(false); // Non thenable objects should fail the test. + expect(thenable({})).toBe(false); // Non thenable objects should fail the test. + expect(thenable({ catch: () => { } })).toBe(false); // Non thenable objects should fail the test. + expect(thenable([prom, promResolved])).toBe(false); // Non thenable objects should fail the test. + expect(thenable(() => { })).toBe(false); // Non thenable objects should fail the test. +}); diff --git a/src/utils/promise/__tests__/timeout.spec.ts b/src/utils/promise/__tests__/timeout.spec.ts new file mode 100644 index 00000000..da6e4446 --- /dev/null +++ b/src/utils/promise/__tests__/timeout.spec.ts @@ -0,0 +1,76 @@ +import { timeout } from '../timeout'; + +const baseTimeoutInMs = 20; +const resolutionValue = 'random_Value'; + +test('Promise utils / timeout - What happens in the event of a timeout or no timeout at all', async () => { + const prom = new Promise(() => { }); + + expect(timeout(0, prom)).toBe(prom); // If we set the timeout with a value less than 1, we just get the original promise (no timeout). + expect(timeout(-1, prom)).toBe(prom); // If we set the timeout with a value less than 1, we just get the original promise (no timeout). + + prom.then( + () => expect('This should not execute').toBeFalsy(), + () => expect('This should execute on timeout expiration.') + ); + + const ts = Date.now(); + const wrapperProm = timeout(baseTimeoutInMs, prom); + + expect(wrapperProm).not.toBe(prom); // If we actually set a timeout it should return a wrapping promise. + + try { + // This should be rejected after 10ms + await wrapperProm; + expect('Should not execute').toBeFalsy(); + } catch (error) { + // The promise was rejected not resolved. Give it an error margin of 10ms since it's not predictable + expect((Date.now() - ts) < baseTimeoutInMs + 20).toBe(true); // The timeout should have rejected the promise. + expect(error.message).toMatch(/^Operation timed out because it exceeded the configured time limit of/); // The timeout should have rejected the promise with a Split Timeout Error. + } +}); + +test('Promise utils / timeout - What happens if the promise resolves before the timeout.', async () => { + let promiseResolver: any = null; + const prom = new Promise(res => { promiseResolver = res; }); + const wrapperProm = timeout(baseTimeoutInMs * 100, prom); + + expect(wrapperProm).not.toBe(prom); // If we actually set a timeout it should return a wrapping promise. + + setTimeout(() => { + // Resolve the promise before the timeout + promiseResolver(resolutionValue); + }, baseTimeoutInMs * 10); + + // This one should not reject but be resolved + try { + // await prom; + const result = await wrapperProm; + + expect(result).toBe(resolutionValue); // The wrapper should resolve to the same value the original promise resolves. + } catch (error) { + expect('Should not execute').toBeFalsy(); + } +}); + +test('Promise utils / timeout - What happens if the promise rejects before the timeout.', async () => { + let promiseRejecter: any = null; + const prom = new Promise((res, rej) => { promiseRejecter = rej; }); + const wrapperProm = timeout(baseTimeoutInMs * 100, prom); + + expect(wrapperProm).not.toBe(prom); // If we actually set a timeout it should return a wrapping promise. + + setTimeout(() => { + // Reject the promise before the timeout + promiseRejecter(resolutionValue); + }, baseTimeoutInMs * 10); + + // This one should not resolve but be rejected + try { + await wrapperProm; + + expect('Should not execute').toBeFalsy(); + } catch (error) { + expect(error).toBe(resolutionValue); // The wrapper should reject to the same error than the original promise. + } +}); diff --git a/src/utils/promise/__tests__/wrapper.spec.ts b/src/utils/promise/__tests__/wrapper.spec.ts new file mode 100644 index 00000000..ca0d418a --- /dev/null +++ b/src/utils/promise/__tests__/wrapper.spec.ts @@ -0,0 +1,162 @@ +// @ts-nocheck +import { promiseWrapper } from '../wrapper'; + +test('Promise utils / promise wrapper', function (done) { + expect.assertions(58); // number of passHandler, passHandlerFinally, passHandlerWithThrow and `hasOnFulfilled` asserts + + const value = 'value'; + const failHandler = (val) => { done.fail(val); }; + const passHandler = (val) => { expect(val).toBe(value); return val; }; + const passHandlerFinally = (val) => { expect(val).toBeUndefined(); }; + const passHandlerWithThrow = (val) => { expect(val).toBe(value); throw val; }; + const createResolvedPromise = () => new Promise((res) => { setTimeout(() => { res(value); }, 100); }); + const createRejectedPromise = () => new Promise((_, rej) => { setTimeout(() => { rej(value); }, 100); }); + + // resolved promises + let wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(false); + + wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); + wrappedPromise.then(passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); + wrappedPromise.finally(passHandlerFinally); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); + wrappedPromise.then(passHandler, failHandler).finally(passHandlerFinally); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); + wrappedPromise.then(passHandler).catch(failHandler).finally(passHandlerFinally); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); + wrappedPromise.then(passHandler).catch(failHandler).then(passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); + wrappedPromise.then(passHandler).then(passHandler).catch(failHandler).finally(passHandlerFinally).then(passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createResolvedPromise(), failHandler); + wrappedPromise.then(passHandler).then(passHandlerWithThrow).catch(passHandler).then(passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + const wrappedPromise2 = promiseWrapper(createResolvedPromise(), failHandler); + wrappedPromise2.then(() => { + wrappedPromise2.then(passHandler); + }); + expect(wrappedPromise2.hasOnFulfilled()).toBe(true); + + Promise.all([ + promiseWrapper(createResolvedPromise(), failHandler), + promiseWrapper(createResolvedPromise(), failHandler)] + ).then((val) => { expect(val).toEqual([value, value]); }); + + // rejected promises + wrappedPromise = promiseWrapper(createRejectedPromise(), passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(false); + + wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); + wrappedPromise.catch(passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(false); + + wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); + wrappedPromise.catch(passHandler).then(passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(false); + + // caveat: setting an `onFinally` handler as the first handler, requires an `onRejected` handler if promise is rejected + wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); + wrappedPromise.finally(passHandlerFinally).catch(passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createRejectedPromise(), passHandler); + wrappedPromise.then(undefined, passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(false); + + wrappedPromise = promiseWrapper(createRejectedPromise(), passHandler); + wrappedPromise.then(failHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); + wrappedPromise.then(failHandler).then(failHandler).catch(passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createRejectedPromise(), passHandler); + wrappedPromise.then(failHandler).then(failHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); + wrappedPromise.then(failHandler, passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); + wrappedPromise.then(failHandler).catch(passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); + wrappedPromise.then(failHandler).then(failHandler, passHandler); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + wrappedPromise = promiseWrapper(createRejectedPromise(), failHandler); + wrappedPromise.then(failHandler).catch(passHandler).then(passHandler).finally(passHandlerFinally); + expect(wrappedPromise.hasOnFulfilled()).toBe(true); + + const wrappedPromise3 = promiseWrapper(createRejectedPromise(), failHandler); + wrappedPromise3.catch(() => { + wrappedPromise3.catch(passHandler); + }); + expect(wrappedPromise3.hasOnFulfilled()).toBe(false); + + Promise.all([ + promiseWrapper(createResolvedPromise(), failHandler), + promiseWrapper(createRejectedPromise(), failHandler)]).catch(passHandler); + + setTimeout(() => { + done(); + }, 1000); + +}); + +test('Promise utils / promise wrapper: async/await', async function () { + + expect.assertions(8); // number of passHandler, passHandlerWithThrow and passHandlerFinally + + const value = 'value'; + const failHandler = (val) => { throw val; }; + const passHandler = (val) => { expect(val).toBe(value); return val; }; + const passHandlerFinally = (val) => { expect(val).toBeUndefined(); }; + const passHandlerWithThrow = (val) => { expect(val).toBe(value); throw val; }; + const createResolvedPromise = () => new Promise((res) => { res(value); }); + const createRejectedPromise = () => new Promise((res, rej) => { rej(value); }); + + try { + const result = await promiseWrapper(createResolvedPromise(), failHandler); + passHandler(result); + } catch (result) { + failHandler(result); + } finally { + passHandlerFinally(); + } + + try { + const result = await promiseWrapper(createRejectedPromise(), failHandler); + failHandler(result); + } catch (result) { + passHandler(result); + } + + let result; + try { + result = await promiseWrapper(createResolvedPromise(), failHandler); + passHandler(result); + passHandlerWithThrow(result); + } catch (error) { + result = passHandler(error); + } finally { + passHandlerFinally(); + } + passHandler(result); +}); diff --git a/src/utils/promise/timeout.ts b/src/utils/promise/timeout.ts index 9d3c12ee..5b8bebd7 100644 --- a/src/utils/promise/timeout.ts +++ b/src/utils/promise/timeout.ts @@ -3,7 +3,7 @@ export function timeout(ms: number, promise: Promise): Promise { return new Promise((resolve, reject) => { const tid = setTimeout(() => { - reject(new Error(`Operation timed out because it exceeded the configured time limit of ${ms}ms.`)); + reject(new Error(`Operation timed out because it exceeded the configured time limit of ${ms} ms.`)); }, ms); promise.then((res) => { diff --git a/src/utils/time/__tests__/index.spec.ts b/src/utils/time/__tests__/index.spec.ts new file mode 100644 index 00000000..9bd0c8fe --- /dev/null +++ b/src/utils/time/__tests__/index.spec.ts @@ -0,0 +1,9 @@ +import { truncateTimeFrame } from '..'; + +test('Test truncateTimeFrame', () => { + expect(truncateTimeFrame(new Date(2020, 9, 2, 10, 53, 12).getTime())).toBe(new Date(2020, 9, 2, 10, 0, 0).getTime()); + expect(truncateTimeFrame(new Date(2020, 9, 2, 10, 0, 0).getTime())).toBe(new Date(2020, 9, 2, 10, 0, 0).getTime()); + expect(truncateTimeFrame(new Date(2020, 9, 2, 10, 53, 0).getTime())).toBe(new Date(2020, 9, 2, 10, 0, 0).getTime()); + expect(truncateTimeFrame(new Date(2020, 9, 2, 10, 0, 12).getTime())).toBe(new Date(2020, 9, 2, 10, 0, 0).getTime()); + expect(truncateTimeFrame(new Date(1970, 1, 0, 0, 0, 0).getTime())).toBe(new Date(1970, 1, 0, 0, 0, 0).getTime()); +}); diff --git a/src/utils/timeTracker/now/__tests__/now.spec.ts b/src/utils/timeTracker/now/__tests__/now.spec.ts new file mode 100644 index 00000000..ded2c815 --- /dev/null +++ b/src/utils/timeTracker/now/__tests__/now.spec.ts @@ -0,0 +1,30 @@ +/** +Copyright 2022 Split Software + +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 { now as nowBrowser } from '../browser'; +import { now as nowNode } from '../node'; + +[nowBrowser, nowNode].forEach(now => { + test('NOW / should generate a value each time you call it', () => { + let n1 = now(); + let n2 = now(); + let n3 = now(); + + expect(Number.isFinite(n1)).toBe(true); // is a finite value? + expect(Number.isFinite(n2)).toBe(true); // is a finite value? + expect(Number.isFinite(n3)).toBe(true); // is a finite value? + }); +});