mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Moving client and common tests to core
Summary: - Moved /test/client and /test/common to core. - Moved two files (CircularArray and RecentItems) from app/common to core/app/common. - Moved resetOrg test to gen-server. - `testrun.sh` is now invoking common and client test from core. - Added missing packages to core's package.json (and revealed underscore as it is used in the main app). - Removed Coord.js as it is not used anywhere. Test Plan: Existing tests Reviewers: paulfitz Reviewed By: paulfitz Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D3590
This commit is contained in:
144
test/common/ACLPermissions.ts
Normal file
144
test/common/ACLPermissions.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import {emptyPermissionSet, PartialPermissionSet,
|
||||
summarizePermissions, summarizePermissionSet} from 'app/common/ACLPermissions';
|
||||
import {makePartialPermissions, parsePermissions, permissionSetToText} from 'app/common/ACLPermissions';
|
||||
import {mergePartialPermissions, mergePermissions} from 'app/common/ACLPermissions';
|
||||
import {assert} from 'chai';
|
||||
|
||||
describe("ACLPermissions", function() {
|
||||
const empty = emptyPermissionSet();
|
||||
|
||||
it('should convert short permissions to permissionSet', function() {
|
||||
assert.deepEqual(parsePermissions('all'),
|
||||
{ read: "allow", create: "allow", update: "allow", delete: "allow", schemaEdit: "allow" });
|
||||
assert.deepEqual(parsePermissions('none'),
|
||||
{ read: "deny", create: "deny", update: "deny", delete: "deny", schemaEdit: "deny" });
|
||||
assert.deepEqual(parsePermissions('all'), parsePermissions('+CRUDS'));
|
||||
assert.deepEqual(parsePermissions('none'), parsePermissions('-CRUDS'));
|
||||
|
||||
assert.deepEqual(parsePermissions('+R'), {...empty, read: "allow"});
|
||||
assert.deepEqual(parsePermissions('-R'), {...empty, read: "deny"});
|
||||
assert.deepEqual(parsePermissions('+S'), {...empty, schemaEdit: "allow"});
|
||||
assert.deepEqual(parsePermissions(''), empty);
|
||||
assert.deepEqual(parsePermissions('+CUD-R'),
|
||||
{create: "allow", update: "allow", delete: "allow", read: "deny", schemaEdit: ""});
|
||||
assert.deepEqual(parsePermissions('-R+CUD'),
|
||||
{create: "allow", update: "allow", delete: "allow", read: "deny", schemaEdit: ""});
|
||||
assert.deepEqual(parsePermissions('+R-CUD'),
|
||||
{create: "deny", update: "deny", delete: "deny", read: "allow", schemaEdit: ""});
|
||||
assert.deepEqual(parsePermissions('-CUD+R'),
|
||||
{create: "deny", update: "deny", delete: "deny", read: "allow", schemaEdit: ""});
|
||||
|
||||
assert.throws(() => parsePermissions('R'), /Invalid permissions specification "R"/);
|
||||
assert.throws(() => parsePermissions('x'), /Invalid permissions specification "x"/);
|
||||
assert.throws(() => parsePermissions('-R\n'), /Invalid permissions specification "-R\\n"/);
|
||||
});
|
||||
|
||||
it('should convert permissionSets to short string', function() {
|
||||
assert.equal(permissionSetToText({read: "allow"}), '+R');
|
||||
assert.equal(permissionSetToText({read: "deny"}), '-R');
|
||||
assert.equal(permissionSetToText({schemaEdit: "allow"}), '+S');
|
||||
assert.equal(permissionSetToText({}), '');
|
||||
assert.equal(permissionSetToText({create: "allow", update: "allow", delete: "allow", read: "deny"}), '+CUD-R');
|
||||
assert.equal(permissionSetToText({create: "deny", update: "deny", delete: "deny", read: "allow"}), '+R-CUD');
|
||||
|
||||
assert.equal(permissionSetToText(parsePermissions('+CRUDS')), 'all');
|
||||
assert.equal(permissionSetToText(parsePermissions('-CRUDS')), 'none');
|
||||
});
|
||||
|
||||
it('should allow merging PermissionSets', function() {
|
||||
function mergeDirect(a: string, b: string) {
|
||||
const aParsed = parsePermissions(a);
|
||||
const bParsed = parsePermissions(b);
|
||||
return permissionSetToText(mergePermissions([aParsed, bParsed], ([_a, _b]) => _a || _b));
|
||||
}
|
||||
testMerge(mergeDirect);
|
||||
});
|
||||
|
||||
it('should allow merging PermissionSets via PartialPermissionSet', function() {
|
||||
// In practice, we work with more generalized PartialPermissionValues. Ensure that this
|
||||
// pathway produces the same results.
|
||||
function mergeViaPartial(a: string, b: string) {
|
||||
const aParsed = parsePermissions(a);
|
||||
const bParsed = parsePermissions(b);
|
||||
return permissionSetToText(mergePartialPermissions(aParsed, bParsed));
|
||||
}
|
||||
testMerge(mergeViaPartial);
|
||||
});
|
||||
|
||||
function testMerge(merge: (a: string, b: string) => string) {
|
||||
assert.equal(merge("+R", "-R"), "+R");
|
||||
assert.equal(merge("+C-D", "+CDS-RU"), "+CS-RUD");
|
||||
assert.equal(merge("all", "+R-CUDS"), "all");
|
||||
assert.equal(merge("none", "-R+CUDS"), "none");
|
||||
assert.equal(merge("all", "none"), "all");
|
||||
assert.equal(merge("none", "all"), "none");
|
||||
assert.equal(merge("", "+RU-CD"), "+RU-CD");
|
||||
assert.equal(merge("-S", "+RU-CD"), "+RU-CDS");
|
||||
}
|
||||
|
||||
|
||||
it('should merge PartialPermissionSets', function() {
|
||||
function merge(a: Partial<PartialPermissionSet>, b: Partial<PartialPermissionSet>): PartialPermissionSet {
|
||||
return mergePartialPermissions({...empty, ...a}, {...empty, ...b});
|
||||
}
|
||||
|
||||
// Combining single bits.
|
||||
assert.deepEqual(merge({read: 'allow'}, {read: 'deny'}), {...empty, read: 'allow'});
|
||||
assert.deepEqual(merge({read: 'deny'}, {read: 'allow'}), {...empty, read: 'deny'});
|
||||
assert.deepEqual(merge({read: 'mixed'}, {read: 'deny'}), {...empty, read: 'mixed'});
|
||||
assert.deepEqual(merge({read: 'mixed'}, {read: 'allow'}), {...empty, read: 'mixed'});
|
||||
assert.deepEqual(merge({read: 'allowSome'}, {read: 'allow'}), {...empty, read: 'allow'});
|
||||
assert.deepEqual(merge({read: 'allowSome'}, {read: 'allowSome'}), {...empty, read: 'allowSome'});
|
||||
assert.deepEqual(merge({read: 'allowSome'}, {read: 'deny'}), {...empty, read: 'mixed'});
|
||||
assert.deepEqual(merge({read: 'allowSome'}, {read: 'denySome'}), {...empty, read: 'mixed'});
|
||||
assert.deepEqual(merge({read: 'denySome'}, {read: 'deny'}), {...empty, read: 'deny'});
|
||||
assert.deepEqual(merge({read: 'denySome'}, {read: 'denySome'}), {...empty, read: 'denySome'});
|
||||
assert.deepEqual(merge({read: 'denySome'}, {read: 'allow'}), {...empty, read: 'mixed'});
|
||||
assert.deepEqual(merge({read: 'denySome'}, {read: 'allowSome'}), {...empty, read: 'mixed'});
|
||||
|
||||
// Combining multiple bits.
|
||||
assert.deepEqual(merge(
|
||||
{read: 'allowSome', create: 'allow', update: 'denySome', delete: 'deny'},
|
||||
{read: 'deny', create: 'denySome', update: 'deny', delete: 'denySome', schemaEdit: 'deny'}
|
||||
),
|
||||
{read: 'mixed', create: 'allow', update: 'deny', delete: 'deny', schemaEdit: 'deny'}
|
||||
);
|
||||
|
||||
assert.deepEqual(merge(makePartialPermissions(parsePermissions("all")), parsePermissions("+U-D")),
|
||||
{read: 'allowSome', create: 'allowSome', update: 'allow', delete: 'mixed', schemaEdit: 'allowSome'}
|
||||
);
|
||||
assert.deepEqual(merge(parsePermissions("+U-D"), makePartialPermissions(parsePermissions("all"))),
|
||||
{read: 'allowSome', create: 'allowSome', update: 'allow', delete: 'deny', schemaEdit: 'allowSome'}
|
||||
);
|
||||
});
|
||||
|
||||
it ('should allow summarization of permission sets', function() {
|
||||
assert.deepEqual(summarizePermissionSet(parsePermissions("+U-D")), 'mixed');
|
||||
assert.deepEqual(summarizePermissionSet(parsePermissions("+U+D")), 'allow');
|
||||
assert.deepEqual(summarizePermissionSet(parsePermissions("-U-D")), 'deny');
|
||||
assert.deepEqual(summarizePermissionSet(parsePermissions("-U-D")), 'deny');
|
||||
assert.deepEqual(summarizePermissionSet(parsePermissions("none")), 'deny');
|
||||
assert.deepEqual(summarizePermissionSet(parsePermissions("all")), 'allow');
|
||||
assert.deepEqual(summarizePermissionSet(parsePermissions("")), 'mixed');
|
||||
assert.deepEqual(summarizePermissionSet(parsePermissions("+CRUDS")), 'allow');
|
||||
assert.deepEqual(summarizePermissionSet(parsePermissions("-CRUDS")), 'deny');
|
||||
assert.deepEqual(summarizePermissionSet({...empty, read: 'allow', update: 'allowSome'}), 'allow');
|
||||
assert.deepEqual(summarizePermissionSet({...empty, read: 'allowSome', update: 'allow'}), 'allow');
|
||||
assert.deepEqual(summarizePermissionSet({...empty, read: 'allowSome', update: 'allowSome'}), 'allow');
|
||||
assert.deepEqual(summarizePermissionSet({...empty, read: 'allow', update: 'denySome'}), 'mixed');
|
||||
assert.deepEqual(summarizePermissionSet({...empty, read: 'denySome', update: 'allowSome'}), 'mixed');
|
||||
assert.deepEqual(summarizePermissionSet({...empty, read: 'denySome', update: 'deny'}), 'deny');
|
||||
});
|
||||
|
||||
it ('should allow summarization of permissions', function() {
|
||||
assert.deepEqual(summarizePermissions(['allow', 'deny']), 'mixed');
|
||||
assert.deepEqual(summarizePermissions(['allow', 'allow']), 'allow');
|
||||
assert.deepEqual(summarizePermissions(['deny', 'allow']), 'mixed');
|
||||
assert.deepEqual(summarizePermissions(['deny', 'deny']), 'deny');
|
||||
assert.deepEqual(summarizePermissions(['allow']), 'allow');
|
||||
assert.deepEqual(summarizePermissions(['deny']), 'deny');
|
||||
assert.deepEqual(summarizePermissions([]), 'mixed');
|
||||
assert.deepEqual(summarizePermissions(['allow', 'allow', 'deny']), 'mixed');
|
||||
assert.deepEqual(summarizePermissions(['allow', 'allow', 'allow']), 'allow');
|
||||
});
|
||||
});
|
||||
144
test/common/AsyncCreate.ts
Normal file
144
test/common/AsyncCreate.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import {AsyncCreate, asyncOnce, mapGetOrSet} from 'app/common/AsyncCreate';
|
||||
import {assert} from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
describe('AsyncCreate', function() {
|
||||
it('should call create func on first use and after failure', async function() {
|
||||
const createFunc = sinon.stub();
|
||||
const cp = new AsyncCreate(createFunc);
|
||||
sinon.assert.notCalled(createFunc);
|
||||
|
||||
const value = {hello: 'world'};
|
||||
createFunc.returns(Promise.resolve(value));
|
||||
|
||||
// Check that .get() calls the createFunc and returns the expected value.
|
||||
assert.strictEqual(await cp.get(), value);
|
||||
sinon.assert.calledOnce(createFunc);
|
||||
createFunc.resetHistory();
|
||||
|
||||
// Subsequent calls return the cached value.
|
||||
assert.strictEqual(await cp.get(), value);
|
||||
sinon.assert.notCalled(createFunc);
|
||||
|
||||
// After clearing, .get() calls createFunc again. We'll make this one fail.
|
||||
cp.clear();
|
||||
createFunc.returns(Promise.reject(new Error('fake-error1')));
|
||||
await assert.isRejected(cp.get(), /fake-error1/);
|
||||
sinon.assert.calledOnce(createFunc);
|
||||
createFunc.resetHistory();
|
||||
|
||||
// After failure, subsequent calls try again.
|
||||
createFunc.returns(Promise.reject(new Error('fake-error2')));
|
||||
await assert.isRejected(cp.get(), /fake-error2/);
|
||||
sinon.assert.calledOnce(createFunc);
|
||||
createFunc.resetHistory();
|
||||
|
||||
// While a createFunc() is pending we do NOT call it again.
|
||||
createFunc.returns(Promise.reject(new Error('fake-error3')));
|
||||
await Promise.all([
|
||||
assert.isRejected(cp.get(), /fake-error3/),
|
||||
assert.isRejected(cp.get(), /fake-error3/),
|
||||
]);
|
||||
sinon.assert.calledOnce(createFunc); // Called just once here.
|
||||
createFunc.resetHistory();
|
||||
});
|
||||
|
||||
it('asyncOnce should call func once and after failure', async function() {
|
||||
const createFunc = sinon.stub();
|
||||
let onceFunc = asyncOnce(createFunc);
|
||||
sinon.assert.notCalled(createFunc);
|
||||
|
||||
const value = {hello: 'world'};
|
||||
createFunc.returns(Promise.resolve(value));
|
||||
|
||||
// Check that .get() calls the createFunc and returns the expected value.
|
||||
assert.strictEqual(await onceFunc(), value);
|
||||
sinon.assert.calledOnce(createFunc);
|
||||
createFunc.resetHistory();
|
||||
|
||||
// Subsequent calls return the cached value.
|
||||
assert.strictEqual(await onceFunc(), value);
|
||||
sinon.assert.notCalled(createFunc);
|
||||
|
||||
// Create a new onceFunc. We'll make this one fail.
|
||||
onceFunc = asyncOnce(createFunc);
|
||||
createFunc.returns(Promise.reject(new Error('fake-error1')));
|
||||
await assert.isRejected(onceFunc(), /fake-error1/);
|
||||
sinon.assert.calledOnce(createFunc);
|
||||
createFunc.resetHistory();
|
||||
|
||||
// After failure, subsequent calls try again.
|
||||
createFunc.returns(Promise.reject(new Error('fake-error2')));
|
||||
await assert.isRejected(onceFunc(), /fake-error2/);
|
||||
sinon.assert.calledOnce(createFunc);
|
||||
createFunc.resetHistory();
|
||||
|
||||
// While a createFunc() is pending we do NOT call it again.
|
||||
createFunc.returns(Promise.reject(new Error('fake-error3')));
|
||||
await Promise.all([
|
||||
assert.isRejected(onceFunc(), /fake-error3/),
|
||||
assert.isRejected(onceFunc(), /fake-error3/),
|
||||
]);
|
||||
sinon.assert.calledOnce(createFunc); // Called just once here.
|
||||
createFunc.resetHistory();
|
||||
});
|
||||
|
||||
describe("mapGetOrSet", function() {
|
||||
it('should call create func on first use and after failure', async function() {
|
||||
const createFunc = sinon.stub();
|
||||
const amap = new Map<string, any>();
|
||||
|
||||
createFunc.callsFake(async (key: string) => ({myKey: key.toUpperCase()}));
|
||||
|
||||
// Check that mapGetOrSet() calls the createFunc and returns the expected value.
|
||||
assert.deepEqual(await mapGetOrSet(amap, "foo", createFunc), {myKey: "FOO"});
|
||||
assert.deepEqual(await mapGetOrSet(amap, "bar", createFunc), {myKey: "BAR"});
|
||||
sinon.assert.calledTwice(createFunc);
|
||||
createFunc.resetHistory();
|
||||
|
||||
// Subsequent calls return the cached value.
|
||||
assert.deepEqual(await mapGetOrSet(amap, "foo", createFunc), {myKey: "FOO"});
|
||||
assert.deepEqual(await mapGetOrSet(amap, "bar", createFunc), {myKey: "BAR"});
|
||||
sinon.assert.notCalled(createFunc);
|
||||
|
||||
// Calls to plain .get() also return the cached value.
|
||||
assert.deepEqual(await amap.get("foo"), {myKey: "FOO"});
|
||||
assert.deepEqual(await amap.get("bar"), {myKey: "BAR"});
|
||||
sinon.assert.notCalled(createFunc);
|
||||
|
||||
// After clearing, .get() returns undefined. (The usual Map behavior.)
|
||||
amap.delete("foo");
|
||||
assert.strictEqual(await amap.get("foo"), undefined);
|
||||
|
||||
// After clearing, mapGetOrSet() calls createFunc again. We'll make this one fail.
|
||||
createFunc.callsFake((key: string) => Promise.reject(new Error('fake-error1-' + key)));
|
||||
await assert.isRejected(mapGetOrSet(amap, "foo", createFunc), /fake-error1-foo/);
|
||||
assert.strictEqual(await amap.get("foo"), undefined);
|
||||
sinon.assert.calledOnce(createFunc);
|
||||
createFunc.resetHistory();
|
||||
|
||||
// Other keys should be unaffected.
|
||||
assert.deepEqual(await mapGetOrSet(amap, "bar", createFunc), {myKey: "BAR"});
|
||||
assert.deepEqual(await amap.get("bar"), {myKey: "BAR"});
|
||||
sinon.assert.notCalled(createFunc);
|
||||
|
||||
// After failure, subsequent calls try again.
|
||||
createFunc.callsFake((key: string) => Promise.reject(new Error('fake-error2-' + key)));
|
||||
await assert.isRejected(mapGetOrSet(amap, "foo", createFunc), /fake-error2-foo/);
|
||||
sinon.assert.calledOnce(createFunc);
|
||||
createFunc.resetHistory();
|
||||
|
||||
// While a createFunc() is pending we do NOT call it again.
|
||||
createFunc.callsFake((key: string) => Promise.reject(new Error('fake-error3-' + key)));
|
||||
amap.delete("bar");
|
||||
await Promise.all([
|
||||
assert.isRejected(mapGetOrSet(amap, "foo", createFunc), /fake-error3-foo/),
|
||||
assert.isRejected(mapGetOrSet(amap, "bar", createFunc), /fake-error3-bar/),
|
||||
assert.isRejected(mapGetOrSet(amap, "foo", createFunc), /fake-error3-foo/),
|
||||
assert.isRejected(mapGetOrSet(amap, "bar", createFunc), /fake-error3-bar/),
|
||||
]);
|
||||
sinon.assert.calledTwice(createFunc); // Called just twice, once for each value.
|
||||
createFunc.resetHistory();
|
||||
});
|
||||
});
|
||||
});
|
||||
21
test/common/BigInt.ts
Normal file
21
test/common/BigInt.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {BigInt} from 'app/common/BigInt';
|
||||
import {assert} from 'chai';
|
||||
import {times} from 'lodash';
|
||||
|
||||
describe('BigInt', function() {
|
||||
it('should represent and convert various numbers correctly', function() {
|
||||
assert.strictEqual(new BigInt(16, [0xF, 0xA], +1).toString(16), "af");
|
||||
assert.strictEqual(new BigInt(16, [0xA, 0xF], -1).toString(16), "-fa");
|
||||
assert.strictEqual(new BigInt(16, [0xF, 0xF], +1).toString(10), "255");
|
||||
assert.strictEqual(new BigInt(16, [0xF, 0xF], -1).toString(10), "-255");
|
||||
|
||||
assert.strictEqual(new BigInt(10, times(20, () => 5), 1).toString(10), "55555555555555555555");
|
||||
assert.strictEqual(new BigInt(100, times(20, () => 5), 1).toString(10),
|
||||
"505050505050505050505050505050505050505");
|
||||
assert.strictEqual(new BigInt(1000, times(20, () => 5), 1).toString(10),
|
||||
"5005005005005005005005005005005005005005005005005005005005");
|
||||
|
||||
assert.strictEqual(new BigInt(0x10000, [0xABCD, 0x1234, 0xF0F0, 0x5678], -1).toString(16),
|
||||
"-5678f0f01234abcd");
|
||||
});
|
||||
});
|
||||
244
test/common/BinaryIndexedTree.js
Normal file
244
test/common/BinaryIndexedTree.js
Normal file
@@ -0,0 +1,244 @@
|
||||
/* global describe, before, it */
|
||||
|
||||
var assert = require('assert');
|
||||
var BinaryIndexedTree = require('app/common/BinaryIndexedTree');
|
||||
|
||||
describe("BinaryIndexedTree", function() {
|
||||
describe('#leastSignificantOne', function() {
|
||||
it("should only keep the least significant one", function() {
|
||||
assert.equal(BinaryIndexedTree.leastSignificantOne(1), 1);
|
||||
assert.equal(BinaryIndexedTree.leastSignificantOne(6), 2);
|
||||
assert.equal(BinaryIndexedTree.leastSignificantOne(15), 1);
|
||||
assert.equal(BinaryIndexedTree.leastSignificantOne(16), 16);
|
||||
assert.equal(BinaryIndexedTree.leastSignificantOne(0), 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#stripLeastSignificantOne', function() {
|
||||
it("should strip the least significant one", function() {
|
||||
assert.equal(BinaryIndexedTree.stripLeastSignificantOne(1), 0);
|
||||
assert.equal(BinaryIndexedTree.stripLeastSignificantOne(6), 4);
|
||||
assert.equal(BinaryIndexedTree.stripLeastSignificantOne(15), 14);
|
||||
assert.equal(BinaryIndexedTree.stripLeastSignificantOne(16), 0);
|
||||
assert.equal(BinaryIndexedTree.stripLeastSignificantOne(0), 0);
|
||||
assert.equal(BinaryIndexedTree.stripLeastSignificantOne(24), 16);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#mostSignificantOne', function() {
|
||||
it("should keep the most significant one", function() {
|
||||
assert.equal(BinaryIndexedTree.mostSignificantOne(1), 1);
|
||||
assert.equal(BinaryIndexedTree.mostSignificantOne(6), 4);
|
||||
assert.equal(BinaryIndexedTree.mostSignificantOne(15), 8);
|
||||
assert.equal(BinaryIndexedTree.mostSignificantOne(16), 16);
|
||||
assert.equal(BinaryIndexedTree.mostSignificantOne(24), 16);
|
||||
assert.equal(BinaryIndexedTree.mostSignificantOne(0), 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#cumulToValues', function() {
|
||||
it("should convert cumulative array to regular values", function() {
|
||||
assert.deepEqual(BinaryIndexedTree.cumulToValues([1, 3, 6, 10]), [1, 2, 3, 4]);
|
||||
assert.deepEqual(BinaryIndexedTree.cumulToValues([1, 3, 6, 10, 15, 21]), [1, 2, 3, 4, 5, 6]);
|
||||
assert.deepEqual(BinaryIndexedTree.cumulToValues([]), []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#valuesToCumul', function() {
|
||||
it("should convert value array to cumulative array", function() {
|
||||
assert.deepEqual(BinaryIndexedTree.valuesToCumul([1, 2, 3, 4]), [1, 3, 6, 10]);
|
||||
assert.deepEqual(BinaryIndexedTree.valuesToCumul([1, 2, 3, 4, 5, 6]), [1, 3, 6, 10, 15, 21]);
|
||||
assert.deepEqual(BinaryIndexedTree.valuesToCumul([]), []);
|
||||
});
|
||||
});
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
// Test array of length 25.
|
||||
var data1 = [47, 17, 28, 96, 10, 2, 11, 43, 7, 94, 37, 81, 75, 2, 33, 57, 68, 71, 68, 86, 27, 44, 64, 41, 23];
|
||||
|
||||
// Test array of length 64.
|
||||
var data2 = [722, 106, 637, 881, 752, 940, 989, 295, 344, 716, 283, 609, 482, 268, 884, 782, 628, 778, 442, 456, 171, 821, 346, 367, 12, 46, 582, 164, 876, 421, 749, 357, 586, 319, 847, 79, 649, 353, 545, 353, 609, 865, 229, 476, 697, 579, 109, 935, 412, 286, 701, 712, 288, 45, 990, 176, 775, 143, 187, 241, 721, 691, 162, 460];
|
||||
var cdata1, cdata2; // Cumulative versions.
|
||||
|
||||
function dumbGetCumulativeValue(array, index) {
|
||||
for (var i = 0, x = 0; i <= index; i++) {
|
||||
x += array[i];
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
/*
|
||||
function dumbGetIndex(array, cumulValue) {
|
||||
for (var i = 0, x = 0; i <= array.length && x <= cumulValue; i++) {
|
||||
x += array[i];
|
||||
}
|
||||
return i;
|
||||
}
|
||||
*/
|
||||
|
||||
before(function() {
|
||||
cdata1 = data1.map(function(value, i) { return dumbGetCumulativeValue(data1, i); });
|
||||
cdata2 = data2.map(function(value, i) { return dumbGetCumulativeValue(data2, i); });
|
||||
});
|
||||
|
||||
describe('BinaryIndexedTree class', function() {
|
||||
it("should construct trees with zeroes", function() {
|
||||
var bit = new BinaryIndexedTree();
|
||||
assert.equal(bit.size(), 0);
|
||||
bit.fillFromValues([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
var bit2 = new BinaryIndexedTree(10);
|
||||
assert.deepEqual(bit, bit2);
|
||||
});
|
||||
|
||||
it("should convert from cumulative array and back", function() {
|
||||
var bit = new BinaryIndexedTree();
|
||||
bit.fillFromCumulative(cdata1);
|
||||
assert.equal(bit.size(), 25);
|
||||
assert.deepEqual(bit.toCumulativeArray(), cdata1);
|
||||
assert.deepEqual(bit.toValueArray(), data1);
|
||||
|
||||
bit.fillFromCumulative([]);
|
||||
assert.equal(bit.size(), 0);
|
||||
assert.deepEqual(bit.toCumulativeArray(), []);
|
||||
assert.deepEqual(bit.toValueArray(), []);
|
||||
|
||||
bit.fillFromCumulative(cdata2);
|
||||
assert.equal(bit.size(), 64);
|
||||
assert.deepEqual(bit.toCumulativeArray(), cdata2);
|
||||
assert.deepEqual(bit.toValueArray(), data2);
|
||||
});
|
||||
|
||||
it("should convert from value array and back", function() {
|
||||
var bit = new BinaryIndexedTree();
|
||||
bit.fillFromValues(data1);
|
||||
assert.equal(bit.size(), 25);
|
||||
assert.deepEqual(bit.toCumulativeArray(), cdata1);
|
||||
assert.deepEqual(bit.toValueArray(), data1);
|
||||
|
||||
bit.fillFromValues([]);
|
||||
assert.equal(bit.size(), 0);
|
||||
assert.deepEqual(bit.toCumulativeArray(), []);
|
||||
assert.deepEqual(bit.toValueArray(), []);
|
||||
|
||||
bit.fillFromValues(data2);
|
||||
assert.equal(bit.size(), 64);
|
||||
assert.deepEqual(bit.toCumulativeArray(), cdata2);
|
||||
assert.deepEqual(bit.toValueArray(), data2);
|
||||
|
||||
bit.fillFromValues([1, 2, 3, 4, 5]);
|
||||
assert.equal(bit.size(), 5);
|
||||
assert.deepEqual(bit.toCumulativeArray(), [1, 3, 6, 10, 15]);
|
||||
assert.deepEqual(bit.toValueArray(), [1, 2, 3, 4, 5]);
|
||||
});
|
||||
|
||||
it("should compute individual and cumulative values", function() {
|
||||
var i, bit = new BinaryIndexedTree();
|
||||
bit.fillFromValues(data1);
|
||||
assert.equal(bit.size(), 25);
|
||||
for (i = 0; i < 25; i++) {
|
||||
assert.equal(bit.getValue(i), data1[i]);
|
||||
assert.equal(bit.getCumulativeValue(i), cdata1[i]);
|
||||
assert.equal(bit.getSumTo(i), cdata1[i] - data1[i]);
|
||||
}
|
||||
assert.equal(bit.getTotal(), data1.reduce(function(a, b) { return a + b; }));
|
||||
|
||||
bit.fillFromValues(data2);
|
||||
assert.equal(bit.size(), 64);
|
||||
for (i = 0; i < 64; i++) {
|
||||
assert.equal(bit.getValue(i), data2[i]);
|
||||
assert.equal(bit.getCumulativeValue(i), cdata2[i]);
|
||||
assert.equal(bit.getSumTo(i), cdata2[i] - data2[i]);
|
||||
}
|
||||
assert.equal(bit.getTotal(), data2.reduce(function(a, b) { return a + b; }));
|
||||
});
|
||||
|
||||
it("should compute cumulative range values", function() {
|
||||
var i, bit = new BinaryIndexedTree();
|
||||
bit.fillFromValues(data1);
|
||||
|
||||
assert.equal(bit.getCumulativeValueRange(0, data1.length),
|
||||
bit.getCumulativeValue(data1.length-1));
|
||||
for(i = 1; i < 25; i++) {
|
||||
assert.equal(bit.getCumulativeValueRange(i, 25),
|
||||
cdata1[24] - cdata1[i-1]);
|
||||
}
|
||||
for(i = 24; i >= 0; i-- ){
|
||||
assert.equal(bit.getCumulativeValueRange(0, i+1), cdata1[i]);
|
||||
}
|
||||
|
||||
bit.fillFromValues(data2);
|
||||
assert.equal(bit.getCumulativeValueRange(0, 64),
|
||||
bit.getCumulativeValue(63));
|
||||
for(i = 1; i < 64; i++) {
|
||||
assert.equal(bit.getCumulativeValueRange(i, 64),
|
||||
cdata2[63] - cdata2[i-1]);
|
||||
}
|
||||
for(i = 63; i >= 0; i-- ){
|
||||
assert.equal(bit.getCumulativeValueRange(0, i+1), cdata2[i]);
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
it("should search by cumulative value", function() {
|
||||
var bit = new BinaryIndexedTree();
|
||||
bit.fillFromValues([1, 2, 3, 4]);
|
||||
assert.equal(bit.getIndex(-1), 0);
|
||||
assert.equal(bit.getIndex(0), 0);
|
||||
assert.equal(bit.getIndex(1), 0);
|
||||
assert.equal(bit.getIndex(2), 1);
|
||||
assert.equal(bit.getIndex(3), 1);
|
||||
assert.equal(bit.getIndex(4), 2);
|
||||
assert.equal(bit.getIndex(5), 2);
|
||||
assert.equal(bit.getIndex(6), 2);
|
||||
assert.equal(bit.getIndex(7), 3);
|
||||
assert.equal(bit.getIndex(8), 3);
|
||||
assert.equal(bit.getIndex(9), 3);
|
||||
assert.equal(bit.getIndex(10), 3);
|
||||
assert.equal(bit.getIndex(11), 4);
|
||||
|
||||
bit.fillFromValues(data1);
|
||||
// data1 is [47,17,28,96,10,2,11,43,7,94,37,81,75,2,33,57,68,71,68,86,27,44,64,41,23];
|
||||
assert.equal(bit.getIndex(0), 0);
|
||||
assert.equal(bit.getIndex(1), 0);
|
||||
assert.equal(bit.getIndex(46.9), 0);
|
||||
assert.equal(bit.getIndex(47), 0);
|
||||
assert.equal(bit.getIndex(63), 1);
|
||||
assert.equal(bit.getIndex(64), 1);
|
||||
assert.equal(bit.getIndex(64.1), 2);
|
||||
assert.equal(bit.getIndex(bit.getCumulativeValue(5)), 5);
|
||||
assert.equal(bit.getIndex(bit.getCumulativeValue(20)), 20);
|
||||
assert.equal(bit.getIndex(bit.getCumulativeValue(24)), 24);
|
||||
assert.equal(bit.getIndex(1000000), 25);
|
||||
});
|
||||
|
||||
it("should support add and set", function() {
|
||||
var i, bit = new BinaryIndexedTree(4);
|
||||
bit.setValue(1, 2);
|
||||
assert.deepEqual(bit.toValueArray(), [0, 2, 0, 0]);
|
||||
bit.setValue(3, 4);
|
||||
assert.deepEqual(bit.toValueArray(), [0, 2, 0, 4]);
|
||||
bit.setValue(0, 1);
|
||||
assert.deepEqual(bit.toValueArray(), [1, 2, 0, 4]);
|
||||
bit.addValue(2, 1);
|
||||
assert.deepEqual(bit.toValueArray(), [1, 2, 1, 4]);
|
||||
bit.addValue(2, 1);
|
||||
assert.deepEqual(bit.toValueArray(), [1, 2, 2, 4]);
|
||||
bit.addValue(2, 1);
|
||||
assert.deepEqual(bit.toValueArray(), [1, 2, 3, 4]);
|
||||
|
||||
bit.fillFromValues(data1);
|
||||
for (i = 0; i < data1.length; i++) {
|
||||
bit.addValue(i, -data1[i]);
|
||||
}
|
||||
assert.deepEqual(bit.toValueArray(), data1.map(function() { return 0; }));
|
||||
|
||||
bit.fillFromValues(data1);
|
||||
for (i = data1.length - 1; i >= 0; i--) {
|
||||
bit.addValue(i, data1[i]);
|
||||
}
|
||||
assert.deepEqual(bit.toValueArray(), data1.map(function(x) { return 2*x; }));
|
||||
});
|
||||
});
|
||||
});
|
||||
70
test/common/ChoiceListParser.ts
Normal file
70
test/common/ChoiceListParser.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {DocumentSettings} from 'app/common/DocumentSettings';
|
||||
import {createParserRaw} from 'app/common/ValueParser';
|
||||
import {assert} from 'chai';
|
||||
|
||||
|
||||
const parser = createParserRaw("ChoiceList", {}, {} as DocumentSettings);
|
||||
|
||||
function testParse(input: string, expected?: string[]) {
|
||||
const result = parser.cleanParse(input);
|
||||
if (expected) {
|
||||
assert.deepEqual(result, ["L", ...expected], input);
|
||||
} else {
|
||||
assert.isNull(result, input);
|
||||
}
|
||||
}
|
||||
|
||||
describe('ChoiceListParser', function() {
|
||||
|
||||
it('should handle empty values', function() {
|
||||
testParse("");
|
||||
testParse(" ");
|
||||
testParse(" , ");
|
||||
testParse(",,,");
|
||||
testParse(" , , , ");
|
||||
testParse("[]");
|
||||
testParse('[""]');
|
||||
testParse('["", null, null, ""]');
|
||||
testParse('""');
|
||||
});
|
||||
|
||||
it('should parse JSON', function() {
|
||||
testParse("[1]", ["1"]);
|
||||
testParse('["a"]', ["a"]);
|
||||
testParse('["a", "aa"]', ["a", "aa"]);
|
||||
testParse(' ["a", "aa"] ', ["a", "aa"]);
|
||||
testParse("[0, 1, 2]", ["0", "1", "2"]);
|
||||
testParse('[0, 1, 2, "a", "b", "c"]', ["0", "1", "2", "a", "b", "c"]);
|
||||
|
||||
// Remove nulls and empty strings
|
||||
testParse('["a", null, "aa", "", null]', ["a", "aa"]);
|
||||
|
||||
// Format nested JSON arrays and objects with formatDecoded
|
||||
testParse('[0, 1, 2, "a", "b", "c", ["d", "x", "y, z"], [["e"], "f"], {"g": ["h"]}]',
|
||||
["0", "1", "2", "a", "b", "c", 'd, x, "y, z"', '[["e"], "f"]', '{"g": ["h"]}']);
|
||||
|
||||
// These are valid JSON but they're not arrays so _parseJSON doesn't touch them
|
||||
testParse('null', ["null"]);
|
||||
testParse('123', ["123"]);
|
||||
testParse('"123"', ["123"]);
|
||||
testParse('"abc"', ["abc"]);
|
||||
});
|
||||
|
||||
it('should parse CSVs', function() {
|
||||
testParse('"a", "aa"', ["a", "aa"]);
|
||||
testParse('"a", aa', ["a", "aa"]);
|
||||
testParse(' " a " , aa', ["a", "aa"]);
|
||||
testParse('a, aa', ["a", "aa"]);
|
||||
testParse('a,aa', ["a", "aa"]);
|
||||
testParse('a,aa b c', ["a", "aa b c"]);
|
||||
testParse(' "a", "aa" ', ["a", "aa"]);
|
||||
testParse("0, 1, 2", ["0", "1", "2"]);
|
||||
testParse('0, 1, 2, "a", "b", "c"', ["0", "1", "2", "a", "b", "c"]);
|
||||
|
||||
testParse('"a", null, "aa", "", null', ["a", "null", "aa", "null"]);
|
||||
});
|
||||
|
||||
it('should split on newlines', function() {
|
||||
testParse('a,b \r\n c,d \n e \n\n\n f \n \n\n \n g', ["a", "b", "c", "d", "e", "f", "g"]);
|
||||
});
|
||||
});
|
||||
38
test/common/CircularArray.js
Normal file
38
test/common/CircularArray.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/* global describe, it */
|
||||
|
||||
var assert = require('assert');
|
||||
var CircularArray = require('app/common/CircularArray');
|
||||
|
||||
describe("CircularArray", function() {
|
||||
it("should lose old items", function() {
|
||||
var c = new CircularArray(5);
|
||||
assert.equal(c.maxLength, 5);
|
||||
assert.equal(c.length, 0);
|
||||
c.push("a");
|
||||
assert.equal(c.get(0), "a");
|
||||
c.push("b");
|
||||
c.push("c");
|
||||
assert.equal(c.length, 3);
|
||||
assert.equal(c.get(2), "c");
|
||||
assert.deepEqual(c.getArray(), ["a", "b", "c"]);
|
||||
c.push("d");
|
||||
c.push("e");
|
||||
assert.equal(c.length, 5);
|
||||
assert.equal(c.get(4), "e");
|
||||
assert.deepEqual(c.getArray(), ["a", "b", "c", "d", "e"]);
|
||||
c.push("f");
|
||||
assert.equal(c.length, 5);
|
||||
assert.equal(c.get(0), "b");
|
||||
assert.equal(c.get(4), "f");
|
||||
assert.deepEqual(c.getArray(), ["b", "c", "d", "e", "f"]);
|
||||
c.push("g");
|
||||
c.push("h");
|
||||
c.push("i");
|
||||
c.push("j");
|
||||
assert.equal(c.length, 5);
|
||||
assert.equal(c.get(0), "f");
|
||||
assert.equal(c.get(4), "j");
|
||||
assert.deepEqual(c.getArray(), ["f", "g", "h", "i", "j"]);
|
||||
assert.equal(c.maxLength, 5);
|
||||
});
|
||||
});
|
||||
33
test/common/DocActions.ts
Normal file
33
test/common/DocActions.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {fromTableDataAction, TableDataAction, toTableDataAction} from 'app/common/DocActions';
|
||||
import {assert} from 'chai';
|
||||
|
||||
describe('DocActions', function() {
|
||||
|
||||
it('should convert correctly with toTableDataAction', () => {
|
||||
const colValues = {id: [2, 4, 6], foo: ["a", "b", "c"], bar: [false, "y", null]};
|
||||
|
||||
assert.deepEqual(toTableDataAction("Hello", colValues),
|
||||
['TableData', "Hello", [2, 4, 6],
|
||||
{ foo: ["a", "b", "c"], bar: [false, "y", null] }]);
|
||||
|
||||
// Make sure colValues that was passed-in didn't get changed.
|
||||
assert.deepEqual(colValues,
|
||||
{id: [2, 4, 6], foo: ["a", "b", "c"], bar: [false, "y", null]});
|
||||
|
||||
assert.deepEqual(toTableDataAction("Foo", {id: []}), ['TableData', "Foo", [], {}]);
|
||||
});
|
||||
|
||||
it('should convert correctly with fromTableDataAction', () => {
|
||||
const tableData: TableDataAction = ['TableData', "Hello", [2, 4, 6],
|
||||
{ foo: ["a", "b", "c"], bar: [false, "y", null] }];
|
||||
|
||||
assert.deepEqual(fromTableDataAction(tableData),
|
||||
{id: [2, 4, 6], foo: ["a", "b", "c"], bar: [false, "y", null]});
|
||||
|
||||
// Make sure tableData itself is unchanged.
|
||||
assert.deepEqual(tableData, ['TableData', "Hello", [2, 4, 6],
|
||||
{ foo: ["a", "b", "c"], bar: [false, "y", null] }]);
|
||||
|
||||
assert.deepEqual(fromTableDataAction(['TableData', "Foo", [], {}]), {id: []});
|
||||
});
|
||||
});
|
||||
110
test/common/InactivityTimer.ts
Normal file
110
test/common/InactivityTimer.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import {InactivityTimer} from 'app/common/InactivityTimer';
|
||||
import {delay} from 'bluebird';
|
||||
import {assert} from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
|
||||
describe("InactivityTimer", function() {
|
||||
|
||||
let spy: sinon.SinonSpy, timer: InactivityTimer;
|
||||
|
||||
beforeEach(() => {
|
||||
spy = sinon.spy();
|
||||
timer = new InactivityTimer(spy, 100);
|
||||
});
|
||||
|
||||
it("if no activity, should trigger when time elapses after ping", async function() {
|
||||
timer.ping();
|
||||
assert(spy.callCount === 0);
|
||||
await delay(150);
|
||||
assert.equal(spy.callCount, 1);
|
||||
});
|
||||
|
||||
it("disableUntilFinish should clear timeout, and set it back after promise resolved", async function() {
|
||||
timer.ping();
|
||||
timer.disableUntilFinish(delay(100)); // eslint-disable-line @typescript-eslint/no-floating-promises
|
||||
await delay(150);
|
||||
assert.equal(spy.callCount, 0);
|
||||
await delay(100);
|
||||
assert.equal(spy.callCount, 1);
|
||||
});
|
||||
|
||||
it("should not trigger during async monitoring", async function() {
|
||||
timer.disableUntilFinish(delay(300)); // eslint-disable-line @typescript-eslint/no-floating-promises
|
||||
|
||||
// do not triggers after a ping
|
||||
timer.ping();
|
||||
await delay(150);
|
||||
assert.equal(spy.callCount, 0);
|
||||
|
||||
// nor after an async monitored call
|
||||
timer.disableUntilFinish(delay(0)); // eslint-disable-line @typescript-eslint/no-floating-promises
|
||||
await delay(150);
|
||||
assert.equal(spy.callCount, 0);
|
||||
|
||||
// finally triggers callback
|
||||
await delay(150);
|
||||
assert.equal(spy.callCount, 1);
|
||||
});
|
||||
|
||||
it("should support disabling", async function() {
|
||||
timer.disable();
|
||||
assert.equal(timer.isEnabled(), false);
|
||||
|
||||
// While disabled, ping doesn't trigger anything.
|
||||
timer.ping();
|
||||
assert.equal(timer.isScheduled(), false);
|
||||
await delay(200);
|
||||
assert.equal(spy.callCount, 0);
|
||||
|
||||
// When enabled, it triggers as usual.
|
||||
timer.enable();
|
||||
assert.equal(timer.isEnabled(), true);
|
||||
assert.equal(timer.isScheduled(), true);
|
||||
await delay(150);
|
||||
assert.equal(spy.callCount, 1);
|
||||
spy.resetHistory();
|
||||
|
||||
// When enabled, ping and disableUntilFinish both trigger the callback.
|
||||
timer.disableUntilFinish(delay(50)).catch(() => null);
|
||||
timer.disableUntilFinish(delay(150)).catch(() => null);
|
||||
await delay(100);
|
||||
assert.equal(spy.callCount, 0);
|
||||
assert.equal(timer.isScheduled(), false);
|
||||
await delay(100);
|
||||
assert.equal(timer.isScheduled(), true);
|
||||
assert.equal(spy.callCount, 0);
|
||||
await delay(100);
|
||||
assert.equal(spy.callCount, 1);
|
||||
spy.resetHistory();
|
||||
|
||||
// When disabled, nothing is triggered.
|
||||
timer.disableUntilFinish(delay(50)).catch(() => null);
|
||||
timer.disableUntilFinish(delay(150)).catch(() => null);
|
||||
await delay(100);
|
||||
assert.equal(spy.callCount, 0);
|
||||
assert.equal(timer.isEnabled(), true);
|
||||
assert.equal(timer.isScheduled(), false);
|
||||
timer.disable();
|
||||
timer.ping();
|
||||
timer.disableUntilFinish(delay(150)).catch(() => null);
|
||||
assert.equal(timer.isEnabled(), false);
|
||||
assert.equal(timer.isScheduled(), false);
|
||||
|
||||
// Nothing called even after disableUntilFinished have resumed.
|
||||
await delay(200);
|
||||
assert.equal(spy.callCount, 0);
|
||||
assert.equal(timer.isScheduled(), false);
|
||||
|
||||
// Re-enabling will schedule after a new delay.
|
||||
timer.enable();
|
||||
assert.equal(timer.isEnabled(), true);
|
||||
assert.equal(timer.isScheduled(), true);
|
||||
await delay(50);
|
||||
assert.equal(spy.callCount, 0);
|
||||
await delay(150);
|
||||
assert.equal(spy.callCount, 1);
|
||||
assert.equal(timer.isEnabled(), true);
|
||||
assert.equal(timer.isScheduled(), false);
|
||||
});
|
||||
});
|
||||
77
test/common/KeyedMutex.ts
Normal file
77
test/common/KeyedMutex.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import {KeyedMutex} from 'app/common/KeyedMutex';
|
||||
import {delay} from 'bluebird';
|
||||
import {assert} from 'chai';
|
||||
|
||||
describe('KeyedMutex', function() {
|
||||
it('orders actions correctly', async function() {
|
||||
const m = new KeyedMutex();
|
||||
let v1: number = 0;
|
||||
let v2: number = 0;
|
||||
|
||||
const fastAdd2 = m.acquire('2').then(unlock => {
|
||||
v2++;
|
||||
unlock();
|
||||
});
|
||||
const slowDouble2 = m.acquire('2').then(async unlock => {
|
||||
await delay(1000);
|
||||
v2 *= 2;
|
||||
unlock();
|
||||
});
|
||||
assert.equal(m.size, 1);
|
||||
|
||||
const slowAdd1 = m.acquire('1').then(async unlock => {
|
||||
await delay(500);
|
||||
v1++;
|
||||
unlock();
|
||||
});
|
||||
const immediateDouble1 = m.acquire('1').then(unlock => {
|
||||
v1 *= 2;
|
||||
unlock();
|
||||
});
|
||||
assert.equal(m.size, 2);
|
||||
|
||||
await Promise.all([slowAdd1, immediateDouble1]);
|
||||
assert.equal(m.size, 1);
|
||||
assert.equal(v1, 2);
|
||||
assert.equal(v2, 1);
|
||||
|
||||
await Promise.all([fastAdd2, slowDouble2]);
|
||||
assert.equal(m.size, 0);
|
||||
assert.equal(v1, 2);
|
||||
assert.equal(v2, 2);
|
||||
});
|
||||
|
||||
it('runs operations exclusively', async function() {
|
||||
const m = new KeyedMutex();
|
||||
let v1: number = 0;
|
||||
let v2: number = 0;
|
||||
|
||||
const fastAdd2 = m.runExclusive('2', async () => {
|
||||
v2++;
|
||||
});
|
||||
const slowDouble2 = m.runExclusive('2', async () => {
|
||||
await delay(1000);
|
||||
v2 *= 2;
|
||||
});
|
||||
assert.equal(m.size, 1);
|
||||
|
||||
const slowAdd1 = m.runExclusive('1', async () => {
|
||||
await delay(500);
|
||||
v1++;
|
||||
});
|
||||
const immediateDouble1 = m.runExclusive('1', async () => {
|
||||
v1 *= 2;
|
||||
});
|
||||
assert.equal(m.size, 2);
|
||||
|
||||
await Promise.all([slowAdd1, immediateDouble1]);
|
||||
assert.equal(m.size, 1);
|
||||
assert.equal(v1, 2);
|
||||
assert.equal(v2, 1);
|
||||
|
||||
await Promise.all([fastAdd2, slowDouble2]);
|
||||
assert.equal(m.size, 0);
|
||||
assert.equal(v1, 2);
|
||||
assert.equal(v2, 2);
|
||||
});
|
||||
});
|
||||
127
test/common/MemBuffer.js
Normal file
127
test/common/MemBuffer.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/* global describe, it */
|
||||
|
||||
var assert = require('assert');
|
||||
var MemBuffer = require('app/common/MemBuffer');
|
||||
|
||||
function repeat(str, n) {
|
||||
return new Array(n+1).join(str);
|
||||
}
|
||||
|
||||
describe("MemBuffer", function() {
|
||||
describe('#reserve', function() {
|
||||
it("should reserve exponentially", function() {
|
||||
var mbuf = new MemBuffer();
|
||||
assert.equal(mbuf.size(), 0);
|
||||
|
||||
var str = "";
|
||||
var lastRes = mbuf.reserved();
|
||||
var countReallocs = 0;
|
||||
|
||||
// Append 1 char at a time, 1000 times, and make sure we don't have more than 10 reallocs.
|
||||
for (var i = 0; i < 1000; i++) {
|
||||
var ch = 'a'.charCodeAt(0) + (i % 10);
|
||||
str += String.fromCharCode(ch);
|
||||
|
||||
mbuf.writeUint8(ch);
|
||||
|
||||
assert.equal(mbuf.size(), i + 1);
|
||||
assert.equal(mbuf.toString(), str);
|
||||
assert.ok(mbuf.reserved() >= mbuf.size());
|
||||
// Count reallocs.
|
||||
if (mbuf.reserved() != lastRes) {
|
||||
lastRes = mbuf.reserved();
|
||||
countReallocs++;
|
||||
}
|
||||
}
|
||||
assert.ok(countReallocs < 10 && countReallocs >= 2);
|
||||
});
|
||||
|
||||
it("should not realloc when it can move data", function() {
|
||||
var mbuf = new MemBuffer();
|
||||
mbuf.writeString(repeat("x", 100));
|
||||
assert.equal(mbuf.size(), 100);
|
||||
assert.ok(mbuf.reserved() >= 100 && mbuf.reserved() < 200);
|
||||
|
||||
// Consume 99 characters, and produce 99 more, and the buffer shouldn't keep being reused.
|
||||
var cons = mbuf.makeConsumer();
|
||||
var value = mbuf.readString(cons, 99);
|
||||
mbuf.consume(cons);
|
||||
assert.equal(value, repeat("x", 99));
|
||||
assert.equal(mbuf.size(), 1);
|
||||
|
||||
var prevBuffer = mbuf.buffer;
|
||||
mbuf.writeString(repeat("y", 99));
|
||||
assert.strictEqual(mbuf.buffer, prevBuffer);
|
||||
assert.equal(mbuf.size(), 100);
|
||||
assert.ok(mbuf.reserved() >= 100 && mbuf.reserved() < 200);
|
||||
|
||||
// Consume the whole buffer, and produce a new one, and it's still being reused.
|
||||
cons = mbuf.makeConsumer();
|
||||
value = mbuf.readString(cons, 100);
|
||||
mbuf.consume(cons);
|
||||
assert.equal(value, "x" + repeat("y", 99));
|
||||
assert.equal(mbuf.size(), 0);
|
||||
|
||||
mbuf.writeString(repeat("z", 100));
|
||||
assert.strictEqual(mbuf.buffer, prevBuffer);
|
||||
assert.equal(mbuf.size(), 100);
|
||||
assert.equal(mbuf.toString(), repeat("z", 100));
|
||||
|
||||
// But if we produce enough new data (twice should do), it should have to realloc.
|
||||
mbuf.writeString(repeat("w", 100));
|
||||
assert.notStrictEqual(mbuf.buffer, prevBuffer);
|
||||
assert.equal(mbuf.size(), 200);
|
||||
assert.equal(mbuf.toString(), repeat("z", 100) + repeat("w", 100));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#write', function() {
|
||||
it("should append to the buffer", function() {
|
||||
var mbuf = new MemBuffer();
|
||||
mbuf.writeString("a");
|
||||
mbuf.writeString(repeat("x", 100));
|
||||
assert.equal(mbuf.toString(), "a" + repeat("x", 100));
|
||||
|
||||
var y = repeat("y", 10000);
|
||||
mbuf.writeString(y);
|
||||
assert.equal(mbuf.toString(), "a" + repeat("x", 100) + y);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#consume', function() {
|
||||
it("should remove from start of buffer", function() {
|
||||
var mbuf = new MemBuffer();
|
||||
mbuf.writeString(repeat("x", 90));
|
||||
mbuf.writeString(repeat("y", 10));
|
||||
assert.equal(mbuf.toString(), repeat("x", 90) + repeat("y", 10));
|
||||
var cons = mbuf.makeConsumer();
|
||||
assert.equal(mbuf.readString(cons, 1), "x");
|
||||
assert.equal(mbuf.readString(cons, 90), repeat("x", 89) + "y");
|
||||
mbuf.consume(cons);
|
||||
assert.equal(mbuf.toString(), repeat("y", 9));
|
||||
|
||||
// Trying to read past the end should throw.
|
||||
assert.throws(function() {
|
||||
mbuf.readString(cons, 10);
|
||||
}, function(err) {
|
||||
assert.ok(err.needMoreData);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Should leave the buffer empty if consume to the end.
|
||||
assert.equal(mbuf.readString(cons, 9), repeat("y", 9));
|
||||
mbuf.consume(cons);
|
||||
assert.equal(mbuf.size(), 0);
|
||||
});
|
||||
|
||||
it("should read large strings", function() {
|
||||
var mbuf = new MemBuffer();
|
||||
var y = repeat("y", 10000);
|
||||
mbuf.writeString(y);
|
||||
var cons = mbuf.makeConsumer();
|
||||
assert.equal(mbuf.readString(cons, 10000), y);
|
||||
mbuf.consume(cons);
|
||||
assert.equal(mbuf.size(), 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
115
test/common/NumberFormat.ts
Normal file
115
test/common/NumberFormat.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import {buildNumberFormat} from 'app/common/NumberFormat';
|
||||
import {assert} from 'chai';
|
||||
|
||||
describe("NumberFormat", function() {
|
||||
const defaultDocSettings = {
|
||||
locale: 'en-US'
|
||||
};
|
||||
|
||||
it("should convert Grist options into Intr.NumberFormat", function() {
|
||||
assert.ownInclude(buildNumberFormat({}, defaultDocSettings).resolvedOptions(), {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 10,
|
||||
style: 'decimal',
|
||||
useGrouping: false,
|
||||
});
|
||||
assert.ownInclude(buildNumberFormat({numMode: 'decimal'}, defaultDocSettings).resolvedOptions(), {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 3,
|
||||
style: 'decimal',
|
||||
useGrouping: true,
|
||||
});
|
||||
assert.ownInclude(buildNumberFormat({numMode: 'percent'}, defaultDocSettings).resolvedOptions(), {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
// style: 'percent', // In node v14.17.0 style is 'decimal' (unclear why)
|
||||
// so we check final formatting instead in this case.
|
||||
useGrouping: true,
|
||||
});
|
||||
assert.equal(buildNumberFormat({numMode: 'percent'}, defaultDocSettings).format(0.5), '50%');
|
||||
assert.ownInclude(buildNumberFormat({numMode: 'currency'}, defaultDocSettings).resolvedOptions(), {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
style: 'currency',
|
||||
useGrouping: true,
|
||||
currency: 'USD',
|
||||
});
|
||||
assert.ownInclude(buildNumberFormat({numMode: 'scientific'}, defaultDocSettings).resolvedOptions(), {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 3,
|
||||
style: 'decimal',
|
||||
// notation: 'scientific', // Should be set, but node doesn't support it until node 12.
|
||||
});
|
||||
|
||||
// Ensure we don't hit errors when max digits is less than the min (which could be implicit).
|
||||
assert.ownInclude(buildNumberFormat({numMode: 'currency', maxDecimals: 1}, defaultDocSettings).resolvedOptions(),
|
||||
{ minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
assert.ownInclude(
|
||||
buildNumberFormat({numMode: 'currency', decimals: 0, maxDecimals: 1}, defaultDocSettings).resolvedOptions(),
|
||||
{ minimumFractionDigits: 0, maximumFractionDigits: 1 });
|
||||
assert.ownInclude(buildNumberFormat({decimals: 5}, defaultDocSettings).resolvedOptions(),
|
||||
{ minimumFractionDigits: 5, maximumFractionDigits: 10 });
|
||||
assert.ownInclude(buildNumberFormat({decimals: 15}, defaultDocSettings).resolvedOptions(),
|
||||
{ minimumFractionDigits: 15, maximumFractionDigits: 15 });
|
||||
});
|
||||
|
||||
it('should clamp min/max decimals to valid values', function() {
|
||||
assert.ownInclude(buildNumberFormat({}, defaultDocSettings).resolvedOptions(),
|
||||
{ minimumFractionDigits: 0, maximumFractionDigits: 10 });
|
||||
assert.ownInclude(buildNumberFormat({decimals: 5}, defaultDocSettings).resolvedOptions(),
|
||||
{ minimumFractionDigits: 5, maximumFractionDigits: 10 });
|
||||
assert.ownInclude(buildNumberFormat({maxDecimals: 5}, defaultDocSettings).resolvedOptions(),
|
||||
{ minimumFractionDigits: 0, maximumFractionDigits: 5 });
|
||||
assert.ownInclude(buildNumberFormat({decimals: -10, maxDecimals: 50}, defaultDocSettings).resolvedOptions(),
|
||||
{ minimumFractionDigits: 0, maximumFractionDigits: 20 });
|
||||
assert.ownInclude(buildNumberFormat({decimals: 21, maxDecimals: 1}, defaultDocSettings).resolvedOptions(),
|
||||
{ minimumFractionDigits: 20, maximumFractionDigits: 20 });
|
||||
assert.ownInclude(buildNumberFormat({numMode: 'currency', maxDecimals: 1}, defaultDocSettings).resolvedOptions(),
|
||||
{ minimumFractionDigits: 2, maximumFractionDigits: 2 }); // Currency overrides the minimum
|
||||
});
|
||||
|
||||
it('should convert locales to local currency', function() {
|
||||
assert.ownInclude(buildNumberFormat({numMode: 'currency'}, {locale: 'fr-BE'}).resolvedOptions(), {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
style: 'currency',
|
||||
useGrouping: true,
|
||||
currency: 'EUR',
|
||||
});
|
||||
assert.ownInclude(buildNumberFormat({numMode: 'currency'}, {locale: 'en-NZ'}).resolvedOptions(), {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
style: 'currency',
|
||||
useGrouping: true,
|
||||
currency: 'NZD',
|
||||
});
|
||||
assert.ownInclude(buildNumberFormat({numMode: 'currency'}, {locale: 'de-CH'}).resolvedOptions(), {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
style: 'currency',
|
||||
useGrouping: true,
|
||||
currency: 'CHF',
|
||||
});
|
||||
assert.ownInclude(buildNumberFormat({numMode: 'currency'}, {locale: 'es-AR'}).resolvedOptions(), {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
style: 'currency',
|
||||
useGrouping: true,
|
||||
currency: 'ARS',
|
||||
});
|
||||
assert.ownInclude(buildNumberFormat({numMode: 'currency'}, {locale: 'zh-TW'}).resolvedOptions(), {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
style: 'currency',
|
||||
useGrouping: true,
|
||||
currency: 'TWD',
|
||||
});
|
||||
assert.ownInclude(buildNumberFormat({numMode: 'currency'}, {locale: 'en-AU'}).resolvedOptions(), {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
style: 'currency',
|
||||
useGrouping: true,
|
||||
currency: 'AUD',
|
||||
});
|
||||
});
|
||||
});
|
||||
392
test/common/NumberParse.ts
Normal file
392
test/common/NumberParse.ts
Normal file
@@ -0,0 +1,392 @@
|
||||
import {getCurrency, locales} from 'app/common/Locales';
|
||||
import {NumMode, parseNumMode} from 'app/common/NumberFormat';
|
||||
import NumberParse from 'app/common/NumberParse';
|
||||
import {assert} from 'chai';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
describe("NumberParse", function() {
|
||||
let parser = new NumberParse("en", "USD");
|
||||
|
||||
function check(str: string, expected: number | null) {
|
||||
const parsed = parser.parse(str);
|
||||
assert.equal(parsed?.result ?? null, expected);
|
||||
}
|
||||
|
||||
it("can do basic parsing", function() {
|
||||
check("123", 123);
|
||||
check("-123", -123);
|
||||
check("-123.456", -123.456);
|
||||
check("-1.234e56", -1.234e56);
|
||||
check("1.234e-56", 1.234e-56);
|
||||
check("(1.234e56)", -1.234e56);
|
||||
check("($1.23)", -1.23);
|
||||
check("($ 1.23)", -1.23);
|
||||
check("$ 1.23", 1.23);
|
||||
check("$1.23", 1.23);
|
||||
check("12.34%", 0.1234);
|
||||
check("1,234,567.89", 1234567.89);
|
||||
check(".89", .89);
|
||||
check(".89000", .89);
|
||||
check("0089", 89);
|
||||
|
||||
// The digit separator is ',' but spaces are always removed anyway
|
||||
check("1 234 567.89", 1234567.89);
|
||||
|
||||
assert.equal(parser.parse(""), null);
|
||||
check(" ", null);
|
||||
check("()", null);
|
||||
check(" ( ) ", null);
|
||||
check(" (,) ", null);
|
||||
check(" (.) ", null);
|
||||
check(",", null);
|
||||
check(",.", null);
|
||||
check(".,", null);
|
||||
check(",,,", null);
|
||||
check("...", null);
|
||||
check(".", null);
|
||||
check("%", null);
|
||||
check("$", null);
|
||||
check("(ABC)", null);
|
||||
check("ABC", null);
|
||||
check("USD", null);
|
||||
|
||||
check("NaN", null);
|
||||
check("NAN", null);
|
||||
check("nan", null);
|
||||
|
||||
// Currency symbol can only appear once
|
||||
check("$$1.23", null);
|
||||
|
||||
// Other currency symbols not allowed
|
||||
check("USD 1.23", null);
|
||||
check("€ 1.23", null);
|
||||
check("£ 1.23", null);
|
||||
check("$ 1.23", 1.23);
|
||||
|
||||
// Parentheses represent negative numbers,
|
||||
// so the number inside can't also be negative or 0
|
||||
check("(0)", null);
|
||||
check("(-1.23)", null);
|
||||
check("(1.23)", -1.23);
|
||||
check("-1.23", -1.23);
|
||||
|
||||
// Only one % allowed
|
||||
check("12.34%%", null);
|
||||
check("12.34%", 0.1234);
|
||||
});
|
||||
|
||||
it("can handle different minus sign positions", function() {
|
||||
parser = new NumberParse("fy", "EUR");
|
||||
let formatter = Intl.NumberFormat("fy", {style: "currency", currency: "EUR"});
|
||||
|
||||
assert.isTrue(parser.currencyEndsInMinusSign);
|
||||
|
||||
// Note the '-' is at the end
|
||||
assert.equal(formatter.format(-1), "€ 1,00-");
|
||||
|
||||
// The parser can handle this, it also allows the '-' in the beginning as usual
|
||||
check("€ 1,00-", -1);
|
||||
check("€ -1,00", -1);
|
||||
check("-€ 1,00", -1);
|
||||
|
||||
// But it's only allowed at the end for currency amounts, to match the formatter
|
||||
check("1,00-", null);
|
||||
check("-1,00", -1);
|
||||
|
||||
// By contrast, this locale doesn't put '-' at the end so the parser doesn't allow that
|
||||
parser = new NumberParse("en", "USD");
|
||||
formatter = Intl.NumberFormat("en", {style: "currency", currency: "USD"});
|
||||
|
||||
assert.isFalse(parser.currencyEndsInMinusSign);
|
||||
|
||||
assert.equal(formatter.format(-1), "-$1.00");
|
||||
|
||||
check("-$1.00", -1);
|
||||
check("$-1.00", -1);
|
||||
check("$1.00-", null);
|
||||
|
||||
check("-1.00", -1);
|
||||
check("1.00-", null);
|
||||
});
|
||||
|
||||
it("can handle different separators", function() {
|
||||
let formatter = Intl.NumberFormat("en", {useGrouping: true});
|
||||
assert.equal(formatter.format(123456789.123), "123,456,789.123");
|
||||
|
||||
parser = new NumberParse("en", "USD");
|
||||
|
||||
assert.equal(parser.digitGroupSeparator, ",");
|
||||
assert.equal(parser.digitGroupSeparatorCurrency, ",");
|
||||
assert.equal(parser.decimalSeparator, ".");
|
||||
|
||||
check("123,456,789.123", 123456789.123);
|
||||
|
||||
// The typical separator is ',' but spaces are always removed anyway
|
||||
check("123 456 789.123", 123456789.123);
|
||||
|
||||
// There must be at least two digits after the separator
|
||||
check("123,456", 123456);
|
||||
check("12,34,56", 123456);
|
||||
check("1,2,3,4,5,6", null);
|
||||
check("123,,456", null);
|
||||
check("1,234", 1234);
|
||||
check("123,4", null);
|
||||
|
||||
// This locale uses 'opposite' separators to the above, i.e. ',' and '.' have swapped roles
|
||||
formatter = Intl.NumberFormat("de-AT", {useGrouping: true, currency: "EUR", style: "currency"});
|
||||
assert.equal(formatter.format(123456789.123), '€ 123.456.789,12');
|
||||
|
||||
// But only for currency amounts! Non-currency amounts use NBSP (non-breaking space) for the digit separator
|
||||
formatter = Intl.NumberFormat("de-AT", {useGrouping: true});
|
||||
assert.equal(formatter.format(123456789.123), '123 456 789,123');
|
||||
|
||||
parser = new NumberParse("de-AT", "EUR");
|
||||
|
||||
assert.equal(parser.digitGroupSeparator, " ");
|
||||
assert.equal(parser.digitGroupSeparatorCurrency, ".");
|
||||
assert.equal(parser.decimalSeparator, ",");
|
||||
|
||||
check("€ 123.456.789,123", 123456789.123);
|
||||
check("€ 123 456 789,123", 123456789.123);
|
||||
// The parser allows the currency separator for non-currency amounts
|
||||
check(" 123.456.789,123", 123456789.123);
|
||||
check(" 123 456 789,123", 123456789.123); // normal space
|
||||
check(" 123 456 789,123", 123456789.123); // NBSP
|
||||
|
||||
formatter = Intl.NumberFormat("en-ZA", {useGrouping: true});
|
||||
assert.equal(formatter.format(123456789.123), '123 456 789,123');
|
||||
|
||||
parser = new NumberParse("en-ZA", "ZAR");
|
||||
|
||||
assert.equal(parser.digitGroupSeparator, " ");
|
||||
assert.equal(parser.digitGroupSeparatorCurrency, " ");
|
||||
assert.equal(parser.decimalSeparator, ",");
|
||||
|
||||
// ',' is the official decimal separator of this locale,
|
||||
// but in general '.' will also work as long as it's not the digit separator.
|
||||
check("123 456 789,123", 123456789.123);
|
||||
check("123 456 789.123", 123456789.123);
|
||||
});
|
||||
|
||||
it("returns basic info about formatting options for a single string", function() {
|
||||
parser = new NumberParse("en", "USD");
|
||||
|
||||
assert.isNull(parser.parse(""));
|
||||
assert.isNull(parser.parse("a b"));
|
||||
|
||||
const defaultOptions = {
|
||||
isCurrency: false,
|
||||
isParenthesised: false,
|
||||
hasDigitGroupSeparator: false,
|
||||
isScientific: false,
|
||||
isPercent: false,
|
||||
};
|
||||
assert.deepEqual(parser.parse("1"),
|
||||
{result: 1, cleaned: "1", options: defaultOptions});
|
||||
assert.deepEqual(parser.parse("$1"),
|
||||
{result: 1, cleaned: "1", options: {...defaultOptions, isCurrency: true}});
|
||||
assert.deepEqual(parser.parse("100%"),
|
||||
{result: 1, cleaned: "100", options: {...defaultOptions, isPercent: true}});
|
||||
assert.deepEqual(parser.parse("1,000"),
|
||||
{result: 1000, cleaned: "1000", options: {...defaultOptions, hasDigitGroupSeparator: true}});
|
||||
assert.deepEqual(parser.parse("1E2"),
|
||||
{result: 100, cleaned: "1e2", options: {...defaultOptions, isScientific: true}});
|
||||
assert.deepEqual(parser.parse("$1,000"),
|
||||
{result: 1000, cleaned: "1000", options: {...defaultOptions, isCurrency: true, hasDigitGroupSeparator: true}});
|
||||
});
|
||||
|
||||
it("guesses formatting options", function() {
|
||||
parser = new NumberParse("en", "USD");
|
||||
|
||||
assert.deepEqual(parser.guessOptions([]), {});
|
||||
assert.deepEqual(parser.guessOptions([""]), {});
|
||||
assert.deepEqual(parser.guessOptions([null]), {});
|
||||
assert.deepEqual(parser.guessOptions(["", null]), {});
|
||||
assert.deepEqual(parser.guessOptions(["abc"]), {});
|
||||
assert.deepEqual(parser.guessOptions(["1"]), {});
|
||||
assert.deepEqual(parser.guessOptions(["1", "", null, "abc"]), {});
|
||||
|
||||
assert.deepEqual(parser.guessOptions(["$1,000"]), {numMode: "currency", decimals: 0});
|
||||
assert.deepEqual(parser.guessOptions(["1,000%"]), {numMode: "percent"});
|
||||
assert.deepEqual(parser.guessOptions(["1,000"]), {numMode: "decimal"});
|
||||
assert.deepEqual(parser.guessOptions(["1E2"]), {numMode: "scientific"});
|
||||
|
||||
// Choose the most common mode when there are several candidates
|
||||
assert.deepEqual(parser.guessOptions(["$1", "$2", "3%"]), {numMode: "currency", decimals: 0});
|
||||
assert.deepEqual(parser.guessOptions(["$1", "2%", "3%"]), {numMode: "percent"});
|
||||
|
||||
assert.deepEqual(parser.guessOptions(["(2)"]), {numSign: 'parens'});
|
||||
assert.deepEqual(parser.guessOptions(["(2)", "3"]), {numSign: 'parens'});
|
||||
// If we see a negative number not surrounded by parens, assume that other parens mean something else
|
||||
assert.deepEqual(parser.guessOptions(["(2)", "-3"]), {});
|
||||
assert.deepEqual(parser.guessOptions(["($2)"]), {numSign: 'parens', numMode: "currency", decimals: 0});
|
||||
|
||||
// Guess 'decimal' (i.e. with thousands separators) even if most numbers don't have separators
|
||||
assert.deepEqual(parser.guessOptions(["1", "10", "100", "1,000"]), {numMode: "decimal"});
|
||||
|
||||
// For USD, currencies are formatted with minimum 2 decimal places by default,
|
||||
// so if the data doesn't have that many decimals we have to explicitly specify the number of decimals, default 0.
|
||||
// The number of digits for other currencies is defaultNumDecimalsCurrency, tested a bit further down.
|
||||
assert.deepEqual(parser.guessOptions(["$1"]), {numMode: "currency", decimals: 0});
|
||||
assert.deepEqual(parser.guessOptions(["$1.2"]), {numMode: "currency", decimals: 0});
|
||||
assert.deepEqual(parser.guessOptions(["$1.23"]), {numMode: "currency"});
|
||||
assert.deepEqual(parser.guessOptions(["$1.234"]), {numMode: "currency"});
|
||||
|
||||
// Otherwise decimal places are guessed based on trailing zeroes
|
||||
assert.deepEqual(parser.guessOptions(["$1.0"]), {numMode: "currency", decimals: 1});
|
||||
assert.deepEqual(parser.guessOptions(["$1.00"]), {numMode: "currency", decimals: 2});
|
||||
assert.deepEqual(parser.guessOptions(["$1.000"]), {numMode: "currency", decimals: 3});
|
||||
|
||||
assert.deepEqual(parser.guessOptions(["1E2"]), {numMode: "scientific"});
|
||||
assert.deepEqual(parser.guessOptions(["1.3E2"]), {numMode: "scientific"});
|
||||
assert.deepEqual(parser.guessOptions(["1.34E2"]), {numMode: "scientific"});
|
||||
assert.deepEqual(parser.guessOptions(["1.0E2"]), {numMode: "scientific", decimals: 1});
|
||||
assert.deepEqual(parser.guessOptions(["1.30E2"]), {numMode: "scientific", decimals: 2});
|
||||
|
||||
assert.equal(parser.defaultNumDecimalsCurrency, 2);
|
||||
parser = new NumberParse("en", "TND");
|
||||
assert.equal(parser.defaultNumDecimalsCurrency, 3);
|
||||
parser = new NumberParse("en", "ZMK");
|
||||
assert.equal(parser.defaultNumDecimalsCurrency, 0);
|
||||
});
|
||||
|
||||
// Nice mixture of numbers of different sizes and containing all digits
|
||||
const numbers = [
|
||||
..._.range(1, 12),
|
||||
..._.range(3, 20).map(n => Math.pow(3, n)),
|
||||
..._.range(10).map(n => Math.pow(10, -n) * 1234560798),
|
||||
];
|
||||
numbers.push(...numbers.map(n => -n));
|
||||
numbers.push(...numbers.map(n => 1 / n));
|
||||
numbers.push(0); // added at the end because of the division just before
|
||||
|
||||
// Formatter to compare numbers that only differ because of floating point precision errors
|
||||
const basicFormatter = Intl.NumberFormat("en", {
|
||||
maximumSignificantDigits: 15,
|
||||
useGrouping: false,
|
||||
});
|
||||
|
||||
// All values supported by parseNumMode
|
||||
const numModes: Array<NumMode | undefined> = ['currency', 'decimal', 'percent', 'scientific', undefined];
|
||||
|
||||
// Generate a test suite for every supported locale
|
||||
for (const locale of locales) {
|
||||
describe(`with ${locale.code} locale (${locale.name})`, function() {
|
||||
const currency = getCurrency(locale.code);
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new NumberParse(locale.code, currency);
|
||||
});
|
||||
|
||||
it("has sensible parser attributes", function() {
|
||||
// These don't strictly need to have length 1, but it's nice to know
|
||||
assert.lengthOf(parser.percentageSymbol, 1);
|
||||
assert.lengthOf(parser.minusSign, 1);
|
||||
assert.lengthOf(parser.decimalSeparator, 1);
|
||||
|
||||
// These *do* need to be a single character since the regex uses `[]`.
|
||||
assert.lengthOf(parser.digitGroupSeparator, 1);
|
||||
// This is the only symbol that's allowed to be empty
|
||||
assert.include([0, 1], parser.digitGroupSeparatorCurrency.length);
|
||||
|
||||
assert.isNotEmpty(parser.exponentSeparator);
|
||||
assert.isNotEmpty(parser.currencySymbol);
|
||||
|
||||
const symbols = [
|
||||
parser.percentageSymbol,
|
||||
parser.minusSign,
|
||||
parser.decimalSeparator,
|
||||
parser.digitGroupSeparator,
|
||||
parser.exponentSeparator,
|
||||
parser.currencySymbol,
|
||||
...parser.digitsMap.keys(),
|
||||
];
|
||||
|
||||
// All the symbols must be distinct
|
||||
assert.equal(symbols.length, new Set(symbols).size);
|
||||
|
||||
// The symbols mustn't contain characters that the parser removes (e.g. spaces)
|
||||
// or they won't be replaced correctly.
|
||||
// The digit group separators are OK because they're removed anyway, and often the separator is a space.
|
||||
// Currency is OK because it gets removed before these characters.
|
||||
for (const symbol of symbols) {
|
||||
if (![
|
||||
parser.digitGroupSeparator,
|
||||
parser.digitGroupSeparatorCurrency,
|
||||
parser.currencySymbol,
|
||||
].includes(symbol)) {
|
||||
assert.equal(symbol, symbol.replace(NumberParse.removeCharsRegex, "REMOVED"));
|
||||
}
|
||||
}
|
||||
|
||||
// Decimal and digit separators have to be different.
|
||||
// We checked digitGroupSeparator already with the Set above,
|
||||
// but not digitGroupSeparatorCurrency because it can equal digitGroupSeparator.
|
||||
assert.notEqual(parser.decimalSeparator, parser.digitGroupSeparator);
|
||||
assert.notEqual(parser.decimalSeparator, parser.digitGroupSeparatorCurrency);
|
||||
|
||||
for (const key of parser.digitsMap.keys()) {
|
||||
assert.lengthOf(key, 1);
|
||||
assert.lengthOf(parser.digitsMap.get(key)!, 1);
|
||||
}
|
||||
});
|
||||
|
||||
it("can parse formatted numbers", function() {
|
||||
for (const numMode of numModes) {
|
||||
const formatter = Intl.NumberFormat(locale.code, {
|
||||
...parseNumMode(numMode, currency),
|
||||
maximumFractionDigits: 15,
|
||||
maximumSignificantDigits: 15,
|
||||
});
|
||||
for (const num of numbers) {
|
||||
const fnum = formatter.format(num);
|
||||
const formattedNumbers = [fnum];
|
||||
|
||||
if (num > 0 && fnum[0] === "0") {
|
||||
// E.g. test that '.5' is parsed as '0.5'
|
||||
formattedNumbers.push(fnum.substring(1));
|
||||
}
|
||||
|
||||
if (num < 0) {
|
||||
formattedNumbers.push(`(${formatter.format(-num)})`);
|
||||
}
|
||||
|
||||
for (const formatted of formattedNumbers) {
|
||||
const parsed = parser.parse(formatted)?.result;
|
||||
|
||||
// Fast check, particularly to avoid formatting the numbers
|
||||
// Makes the tests about 1.5s/30% faster.
|
||||
if (parsed === num) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
assert.exists(parsed);
|
||||
assert.equal(
|
||||
basicFormatter.format(parsed!),
|
||||
basicFormatter.format(num),
|
||||
);
|
||||
} catch (e) {
|
||||
// Handy information for understanding failures
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log({
|
||||
num, formatted, parsed, numMode, parser,
|
||||
parts: formatter.formatToParts(num),
|
||||
formattedChars: [...formatted].map(char => ({
|
||||
char,
|
||||
// To see invisible characters, e.g. RTL/LTR marks
|
||||
codePoint: char.codePointAt(0),
|
||||
codePointHex: char.codePointAt(0)!.toString(16),
|
||||
})),
|
||||
formatterOptions: formatter.resolvedOptions(),
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
33
test/common/PluginInstance.ts
Normal file
33
test/common/PluginInstance.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {LocalPlugin} from 'app/common/plugin';
|
||||
import * as clientUtil from 'test/client/clientUtil';
|
||||
import * as sinon from 'sinon';
|
||||
import {assert} from 'chai';
|
||||
|
||||
import * as browserGlobals from 'app/client/lib/browserGlobals';
|
||||
const G: any = browserGlobals.get('$');
|
||||
|
||||
import {PluginInstance} from 'app/common/PluginInstance';
|
||||
|
||||
describe("PluginInstance", function() {
|
||||
clientUtil.setTmpMochaGlobals();
|
||||
it("can manages render target", function() {
|
||||
const plugin = new PluginInstance({manifest: {contributions: {}}} as LocalPlugin, {});
|
||||
assert.throws(() => plugin.getRenderTarget(2), /Unknown render target.*/);
|
||||
assert.doesNotThrow(() => plugin.getRenderTarget("fullscreen"));
|
||||
const renderTarget1 = sinon.spy();
|
||||
const renderTarget2 = sinon.spy();
|
||||
|
||||
const el1 = G.$('<h1>el1</h1>');
|
||||
const el2 = G.$('<h1>el2</h1>');
|
||||
|
||||
const handle1 = plugin.addRenderTarget(renderTarget1);
|
||||
plugin.getRenderTarget(handle1)(el1, {});
|
||||
sinon.assert.calledWith(renderTarget1, el1, {});
|
||||
plugin.removeRenderTarget(handle1);
|
||||
assert.throw(() => plugin.getRenderTarget(handle1));
|
||||
|
||||
const handle2 = plugin.addRenderTarget(renderTarget2);
|
||||
plugin.getRenderTarget(handle2)(el2 as HTMLElement, {});
|
||||
sinon.assert.calledWith(renderTarget2, el2, {});
|
||||
});
|
||||
});
|
||||
94
test/common/RecentItems.js
Normal file
94
test/common/RecentItems.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/* global describe, it */
|
||||
|
||||
var assert = require('chai').assert;
|
||||
var RecentItems = require('app/common/RecentItems');
|
||||
|
||||
describe('RecentItems', function() {
|
||||
let simpleList = ['foo', 'bar', 'baz'];
|
||||
|
||||
let objList = [
|
||||
{ name: 'foo', path: '/foo' },
|
||||
{ name: 'bar', path: '/bar' },
|
||||
{ name: 'baz', path: '/baz' },
|
||||
];
|
||||
|
||||
describe("listItems", function() {
|
||||
it("should return a valid list", function() {
|
||||
let recentItems = new RecentItems({
|
||||
intialItems: simpleList
|
||||
});
|
||||
assert.deepEqual(recentItems.listItems(), ['foo', 'bar', 'baz']);
|
||||
});
|
||||
|
||||
it("should return a valid list given a keyFunc", function() {
|
||||
let recentItems = new RecentItems({
|
||||
intialItems: objList,
|
||||
keyFunc: item => item.path
|
||||
});
|
||||
assert.deepEqual(recentItems.listItems(), [
|
||||
{ name: 'foo', path: '/foo' },
|
||||
{ name: 'bar', path: '/bar' },
|
||||
{ name: 'baz', path: '/baz' },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should produce a list of objects with unique keys", function() {
|
||||
let recentItems = new RecentItems({
|
||||
intialItems: [
|
||||
{ name: 'foo', path: '/foo' },
|
||||
{ name: 'bar', path: '/bar' },
|
||||
{ name: 'foo', path: '/foo' },
|
||||
{ name: 'baz', path: '/baz' },
|
||||
{ name: 'foobar', path: '/foo' },
|
||||
],
|
||||
keyFunc: item => item.path
|
||||
});
|
||||
assert.deepEqual(recentItems.listItems(), [
|
||||
{ name: 'bar', path: '/bar' },
|
||||
{ name: 'baz', path: '/baz' },
|
||||
{ name: 'foobar', path: '/foo' }
|
||||
]);
|
||||
let recentItems2 = new RecentItems({
|
||||
intialItems: simpleList,
|
||||
});
|
||||
assert.deepEqual(recentItems2.listItems(), ['foo', 'bar', 'baz']);
|
||||
for(let i = 0; i < 30; i++) {
|
||||
recentItems2.addItems(simpleList);
|
||||
}
|
||||
assert.deepEqual(recentItems2.listItems(), ['foo', 'bar', 'baz']);
|
||||
});
|
||||
|
||||
it("should produce a list with the correct max length", function() {
|
||||
let recentItems = new RecentItems({
|
||||
intialItems: objList,
|
||||
maxCount: 2,
|
||||
keyFunc: item => item.path
|
||||
});
|
||||
assert.deepEqual(recentItems.listItems(), [
|
||||
{ name: 'bar', path: '/bar' },
|
||||
{ name: 'baz', path: '/baz' }
|
||||
]);
|
||||
recentItems.addItem({ name: 'foo', path: '/foo' });
|
||||
assert.deepEqual(recentItems.listItems(), [
|
||||
{ name: 'baz', path: '/baz' },
|
||||
{ name: 'foo', path: '/foo' }
|
||||
]);
|
||||
recentItems.addItem({name: 'BAZ', path: '/baz'});
|
||||
assert.deepEqual(recentItems.listItems(), [
|
||||
{ name: 'foo', path: '/foo' },
|
||||
{ name: 'BAZ', path: '/baz' }
|
||||
]);
|
||||
let recentItems2 = new RecentItems({
|
||||
intialItems: simpleList,
|
||||
maxCount: 10
|
||||
});
|
||||
let alphabet = "abcdefghijklmnopqrstuvwxyz".split("");
|
||||
recentItems2.addItems(alphabet);
|
||||
assert.deepEqual(recentItems2.listItems(), 'qrstuvwxyz'.split(""));
|
||||
recentItems2.addItem('a');
|
||||
assert.deepEqual(recentItems2.listItems(), 'rstuvwxyza'.split(""));
|
||||
recentItems2.addItem('r');
|
||||
assert.deepEqual(recentItems2.listItems(), 'stuvwxyzar'.split(""));
|
||||
});
|
||||
});
|
||||
});
|
||||
170
test/common/RefCountMap.ts
Normal file
170
test/common/RefCountMap.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import {delay} from 'app/common/delay';
|
||||
import {RefCountMap} from 'app/common/RefCountMap';
|
||||
import {assert} from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
function assertResetSingleCall(spy: sinon.SinonSpy, context: any, ...args: any[]): void {
|
||||
sinon.assert.calledOnce(spy);
|
||||
sinon.assert.calledOn(spy, context);
|
||||
sinon.assert.calledWithExactly(spy, ...args);
|
||||
spy.resetHistory();
|
||||
}
|
||||
|
||||
describe("RefCountMap", function() {
|
||||
it("should dispose items when ref-count returns to 0", function() {
|
||||
const create = sinon.stub().callsFake((key) => key.toUpperCase());
|
||||
const dispose = sinon.spy();
|
||||
const m = new RefCountMap<string, string>({create, dispose, gracePeriodMs: 0});
|
||||
|
||||
const subFoo1 = m.use("foo");
|
||||
assert.strictEqual(subFoo1.get(), "FOO");
|
||||
assertResetSingleCall(create, null, "foo");
|
||||
|
||||
const subBar1 = m.use("bar");
|
||||
assert.strictEqual(subBar1.get(), "BAR");
|
||||
assertResetSingleCall(create, null, "bar");
|
||||
|
||||
const subFoo2 = m.use("foo");
|
||||
assert.strictEqual(subFoo2.get(), "FOO");
|
||||
sinon.assert.notCalled(create);
|
||||
|
||||
// Now dispose one by one.
|
||||
subFoo1.dispose();
|
||||
sinon.assert.notCalled(dispose);
|
||||
subBar1.dispose();
|
||||
assertResetSingleCall(dispose, null, "bar", "BAR");
|
||||
|
||||
// An extra subscription increases refCount, so subFoo2.dispose will not yet dispose it.
|
||||
const subFoo3 = m.use("foo");
|
||||
assert.strictEqual(subFoo3.get(), "FOO");
|
||||
sinon.assert.notCalled(create);
|
||||
|
||||
subFoo2.dispose();
|
||||
sinon.assert.notCalled(dispose);
|
||||
subFoo3.dispose();
|
||||
assertResetSingleCall(dispose, null, "foo", "FOO");
|
||||
});
|
||||
|
||||
it("should respect the grace period", async function() {
|
||||
const create = sinon.stub().callsFake((key) => key.toUpperCase());
|
||||
const dispose = sinon.spy();
|
||||
const m = new RefCountMap<string, string>({create, dispose, gracePeriodMs: 60});
|
||||
|
||||
const subFoo1 = m.use("foo");
|
||||
assert.strictEqual(subFoo1.get(), "FOO");
|
||||
assertResetSingleCall(create, null, "foo");
|
||||
|
||||
const subBar1 = m.use("bar");
|
||||
assert.strictEqual(subBar1.get(), "BAR");
|
||||
assertResetSingleCall(create, null, "bar");
|
||||
|
||||
// Disposal is not immediate, we have some time.
|
||||
subFoo1.dispose();
|
||||
subBar1.dispose();
|
||||
sinon.assert.notCalled(dispose);
|
||||
|
||||
// Wait a bit and add more usage to one of the keys.
|
||||
await delay(30);
|
||||
|
||||
const subFoo2 = m.use("foo");
|
||||
assert.strictEqual(subFoo2.get(), "FOO");
|
||||
sinon.assert.notCalled(create);
|
||||
|
||||
// Grace period hasn't expired yet, so dispose isn't called yet.
|
||||
sinon.assert.notCalled(dispose);
|
||||
|
||||
// Now wait for the grace period to end.
|
||||
await delay(40);
|
||||
|
||||
// Ensure that bar's disposal has run now, but not foo's.
|
||||
assertResetSingleCall(dispose, null, "bar", "BAR");
|
||||
|
||||
// Dispose the second usage, and wait for the full grace period.
|
||||
subFoo2.dispose();
|
||||
await delay(70);
|
||||
assertResetSingleCall(dispose, null, "foo", "FOO");
|
||||
});
|
||||
|
||||
it("should dispose immediately on clear", async function() {
|
||||
const create = sinon.stub().callsFake((key) => key.toUpperCase());
|
||||
const dispose = sinon.spy();
|
||||
const m = new RefCountMap<string, string>({create, dispose, gracePeriodMs: 0});
|
||||
const subFoo1 = m.use("foo");
|
||||
const subBar1 = m.use("bar");
|
||||
const subFoo2 = m.use("foo");
|
||||
m.dispose();
|
||||
|
||||
assert.equal(dispose.callCount, 2);
|
||||
assert.deepEqual(dispose.args, [["foo", "FOO"], ["bar", "BAR"]]);
|
||||
dispose.resetHistory();
|
||||
|
||||
// Should be a no-op to dispose subscriptions after RefCountMap is disposed.
|
||||
subFoo1.dispose();
|
||||
subFoo2.dispose();
|
||||
subBar1.dispose();
|
||||
sinon.assert.notCalled(dispose);
|
||||
|
||||
// It should not be a matter of gracePeriod, but make sure by waiting a bit.
|
||||
await delay(30);
|
||||
sinon.assert.notCalled(dispose);
|
||||
});
|
||||
|
||||
it("should be safe to purge a key", async function() {
|
||||
const create = sinon.stub().callsFake((key) => key.toUpperCase());
|
||||
const dispose = sinon.spy();
|
||||
const m = new RefCountMap<string, string>({create, dispose, gracePeriodMs: 0});
|
||||
const subFoo1 = m.use("foo");
|
||||
const subBar1 = m.use("bar");
|
||||
const subFoo2 = m.use("foo");
|
||||
|
||||
m.purgeKey("foo");
|
||||
assertResetSingleCall(dispose, null, "foo", "FOO");
|
||||
m.purgeKey("bar");
|
||||
assertResetSingleCall(dispose, null, "bar", "BAR");
|
||||
|
||||
// The tricky case is when a new "foo" key is created after the purge.
|
||||
const subFooNew1 = m.use("foo");
|
||||
const subBarNew1 = m.use("bar");
|
||||
|
||||
// Should be a no-op to dispose purged subscriptions.
|
||||
subFoo1.dispose();
|
||||
subFoo2.dispose();
|
||||
sinon.assert.notCalled(dispose);
|
||||
|
||||
// A new subscription with the same key should get disposed though.
|
||||
subFooNew1.dispose();
|
||||
assertResetSingleCall(dispose, null, "foo", "FOO");
|
||||
subBarNew1.dispose();
|
||||
assertResetSingleCall(dispose, null, "bar", "BAR");
|
||||
|
||||
// Still a no-op to dispose old purged subscriptions.
|
||||
subBar1.dispose();
|
||||
sinon.assert.notCalled(dispose);
|
||||
|
||||
// Ensure there are no scheduled disposals due to some other bug.
|
||||
await delay(30);
|
||||
sinon.assert.notCalled(dispose);
|
||||
});
|
||||
|
||||
it("should not dispose a re-created key on timeout after purge", async function() {
|
||||
const create = sinon.stub().callsFake((key) => key.toUpperCase());
|
||||
const dispose = sinon.spy();
|
||||
const m = new RefCountMap<string, string>({create, dispose, gracePeriodMs: 60});
|
||||
|
||||
const subFoo1 = m.use("foo");
|
||||
subFoo1.dispose(); // This schedules a disposal in 20ms
|
||||
m.purgeKey("foo"); // This should purge immediately AND unset the scheduled disposal
|
||||
assertResetSingleCall(dispose, null, "foo", "FOO");
|
||||
|
||||
await delay(20);
|
||||
const subFoo2 = m.use("foo"); // Should not be affected by the scheduled disposal.
|
||||
await delay(100); // "foo" stays beyond grace period, since it's being used.
|
||||
sinon.assert.notCalled(dispose);
|
||||
|
||||
subFoo2.dispose(); // Once disposed, it stays for grace period
|
||||
await delay(20);
|
||||
sinon.assert.notCalled(dispose);
|
||||
await delay(100); // And gets disposed after it.
|
||||
assertResetSingleCall(dispose, null, "foo", "FOO");
|
||||
});
|
||||
});
|
||||
29
test/common/SortFunc.ts
Normal file
29
test/common/SortFunc.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {typedCompare} from 'app/common/SortFunc';
|
||||
import {assert} from 'chai';
|
||||
import {format} from 'util';
|
||||
|
||||
describe('SortFunc', function() {
|
||||
it('should be transitive for values of different types', function() {
|
||||
const values = [
|
||||
-10, 0, 2, 10.5,
|
||||
null,
|
||||
["a"], ["b"], ["b", 1], ["b", 1, 2], ["b", 1, "10"], ["c"],
|
||||
"10.5", "2", "a",
|
||||
undefined as any,
|
||||
];
|
||||
|
||||
// Check that sorting works as expected (the values above are already sorted).
|
||||
const sorted = values.slice(0);
|
||||
sorted.sort(typedCompare);
|
||||
assert.deepEqual(sorted, values);
|
||||
|
||||
// Check comparisons between each possible pair of values above.
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
assert.equal(typedCompare(values[i], values[i]), 0, `Expected ${format(values[i])} == ${format(values[i])}`);
|
||||
for (let j = i + 1; j < values.length; j++) {
|
||||
assert.equal(typedCompare(values[i], values[j]), -1, `Expected ${format(values[i])} < ${format(values[j])}`);
|
||||
assert.equal(typedCompare(values[j], values[i]), 1, `Expected ${format(values[j])} > ${format(values[i])}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
59
test/common/StringUnion.ts
Normal file
59
test/common/StringUnion.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {StringUnion} from 'app/common/StringUnion';
|
||||
import {assert} from 'chai';
|
||||
|
||||
describe('StringUnion', function() {
|
||||
// Create Dog type
|
||||
const Dog = StringUnion(
|
||||
"bulldog",
|
||||
"poodle",
|
||||
"greyhound"
|
||||
);
|
||||
type Dog = typeof Dog.type;
|
||||
|
||||
// Create Cat type
|
||||
const Cat = StringUnion(
|
||||
"siamese",
|
||||
"sphynx",
|
||||
"bengal"
|
||||
);
|
||||
type Cat = typeof Cat.type;
|
||||
|
||||
it('should provide check and guard functions', function() {
|
||||
let dog: Dog;
|
||||
let cat: Cat;
|
||||
|
||||
const greyhound = "greyhound";
|
||||
const bengal = "bengal";
|
||||
const giraffe = "giraffe";
|
||||
|
||||
// Use Dog check function.
|
||||
dog = Dog.check(greyhound);
|
||||
assert.equal(dog, greyhound);
|
||||
|
||||
assert.doesNotThrow(() => { dog = Dog.check(greyhound); });
|
||||
assert.throws(() => { dog = Dog.check(bengal); },
|
||||
`Value '"bengal"' is not assignable to type '"bulldog" | "poodle" | "greyhound"'`);
|
||||
assert.throws(() => { dog = Dog.check(giraffe); },
|
||||
`Value '"giraffe"' is not assignable to type '"bulldog" | "poodle" | "greyhound"'`);
|
||||
|
||||
// Use Cat check function.
|
||||
cat = Cat.check(bengal);
|
||||
assert.equal(cat, bengal);
|
||||
|
||||
assert.doesNotThrow(() => { cat = Cat.check(bengal); });
|
||||
assert.throws(() => { cat = Cat.check(greyhound); },
|
||||
`Value '"greyhound"' is not assignable to type '"siamese" | "sphynx" | "bengal"'`);
|
||||
assert.throws(() => { cat = Cat.check(giraffe); },
|
||||
`Value '"giraffe"' is not assignable to type '"siamese" | "sphynx" | "bengal"'`);
|
||||
|
||||
// Use Dog guard function.
|
||||
assert.isTrue(Dog.guard(greyhound));
|
||||
assert.isFalse(Dog.guard(bengal));
|
||||
assert.isFalse(Dog.guard(giraffe));
|
||||
|
||||
// Use Cat guard function.
|
||||
assert.isTrue(Cat.guard(bengal));
|
||||
assert.isFalse(Cat.guard(greyhound));
|
||||
assert.isFalse(Cat.guard(giraffe));
|
||||
});
|
||||
});
|
||||
259
test/common/TableData.ts
Normal file
259
test/common/TableData.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import {CellValue, TableDataAction} from 'app/common/DocActions';
|
||||
import {TableData} from 'app/common/TableData';
|
||||
import {assert} from 'chai';
|
||||
import {unzip, zipObject} from 'lodash';
|
||||
|
||||
|
||||
describe('TableData', function() {
|
||||
const sampleData: TableDataAction = ["TableData", "Foo", [1, 4, 5, 7], {
|
||||
city: ['New York', 'Boston', 'Boston', 'Seattle'],
|
||||
state: ['NY', 'MA', 'MA', 'WA'],
|
||||
amount: [5, 4, "NA", 2],
|
||||
bool: [true, true, false, false],
|
||||
}];
|
||||
|
||||
// Transpose the given matrix. If empty, it's considered to consist of 0 rows and
|
||||
// colArray.length columns, so that the transpose has colArray.length empty rows.
|
||||
function transpose<T>(matrix: T[][], colArray: any[]): T[][] {
|
||||
return matrix.length > 0 ? unzip(matrix) : colArray.map(c => []);
|
||||
}
|
||||
|
||||
function verifyTableData(t: TableData, colIds: string[], data: CellValue[][]): void {
|
||||
const idIndex = colIds.indexOf('id');
|
||||
assert(idIndex !== -1, "verifyTableData expects 'id' column");
|
||||
const rowIds: number[] = data.map(row => row[idIndex]) as number[];
|
||||
assert.strictEqual(t.numRecords(), data.length);
|
||||
assert.sameMembers(t.getColIds(), colIds);
|
||||
assert.deepEqual(t.getSortedRowIds(), rowIds);
|
||||
assert.sameMembers(Array.from(t.getRowIds()), rowIds);
|
||||
const transposed = transpose(data, colIds);
|
||||
|
||||
// Verify data using .getValue()
|
||||
assert.deepEqual(rowIds.map(r => colIds.map(c => t.getValue(r, c))), data);
|
||||
|
||||
// Verify data using getRowPropFunc()
|
||||
assert.deepEqual(colIds.map(c => rowIds.map(t.getRowPropFunc(c)!)), transposed);
|
||||
|
||||
// Verify data using getRecord()
|
||||
const expRecords = data.map((row, i) => zipObject(colIds, row));
|
||||
assert.deepEqual(rowIds.map(r => t.getRecord(r)) as any, expRecords);
|
||||
|
||||
// Verify data using getRecords().
|
||||
assert.sameDeepMembers(t.getRecords(), expRecords);
|
||||
|
||||
// Verify data using getColValues().
|
||||
const rawOrderedData = t.getRowIds().map(r => data[rowIds.indexOf(r)]);
|
||||
const rawOrderedTransposed = transpose(rawOrderedData, colIds);
|
||||
assert.deepEqual(colIds.map(c => t.getColValues(c)), rawOrderedTransposed);
|
||||
}
|
||||
|
||||
it('should start out empty and support loadData', function() {
|
||||
const t = new TableData('Foo', null, {city: 'Text', state: 'Text', amount: 'Numeric', bool: 'Bool'});
|
||||
assert.equal(t.tableId, 'Foo');
|
||||
assert.isFalse(t.isLoaded);
|
||||
verifyTableData(t, ["id", "city", "state", "amount", "bool"], []);
|
||||
|
||||
t.loadData(sampleData);
|
||||
assert.isTrue(t.isLoaded);
|
||||
verifyTableData(t, ["id", "city", "state", "amount", "bool"], [
|
||||
[1, 'New York', 'NY', 5, true],
|
||||
[4, 'Boston', 'MA', 4, true],
|
||||
[5, 'Boston', 'MA', "NA", false],
|
||||
[7, 'Seattle', 'WA', 2, false],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should start out with data from constructor', function() {
|
||||
const t = new TableData('Foo', sampleData, {city: 'Text', state: 'Text', amount: 'Numeric', bool: 'Bool'});
|
||||
assert.equal(t.tableId, 'Foo');
|
||||
assert.isTrue(t.isLoaded);
|
||||
verifyTableData(t, ["id", "city", "state", "amount", "bool"], [
|
||||
[1, 'New York', 'NY', 5, true],
|
||||
[4, 'Boston', 'MA', 4, true],
|
||||
[5, 'Boston', 'MA', "NA", false],
|
||||
[7, 'Seattle', 'WA', 2, false],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support filterRecords and filterRowIds', function() {
|
||||
const t = new TableData('Foo', sampleData, {city: 'Text', state: 'Text', amount: 'Numeric', bool: 'Bool'});
|
||||
assert.deepEqual(t.filterRecords({state: 'MA'}), [
|
||||
{id: 4, city: 'Boston', state: 'MA', amount: 4, bool: true},
|
||||
{id: 5, city: 'Boston', state: 'MA', amount: 'NA', bool: false}]);
|
||||
assert.deepEqual(t.filterRowIds({state: 'MA'}), [4, 5]);
|
||||
|
||||
// After removing and re-adding a record, indices change, but filter behavior should not.
|
||||
// Notice sameDeepMembers() below, rather than deepEqual(), since order is not guaranteed.
|
||||
t.dispatchAction(["RemoveRecord", "Foo", 4]);
|
||||
t.dispatchAction(["AddRecord", "Foo", 4, {city: 'BOSTON', state: 'MA'}]);
|
||||
verifyTableData(t, ["id", "city", "state", "amount", "bool"], [
|
||||
[1, 'New York', 'NY', 5, true],
|
||||
[4, 'BOSTON', 'MA', 0, false],
|
||||
[5, 'Boston', 'MA', "NA", false],
|
||||
[7, 'Seattle', 'WA', 2, false],
|
||||
]);
|
||||
assert.deepEqual(t.filterRecords({city: 'BOSTON', amount: 0.0}), [
|
||||
{id: 4, city: 'BOSTON', state: 'MA', amount: 0, bool: false}]);
|
||||
assert.deepEqual(t.filterRowIds({city: 'BOSTON', amount: 0.0}), [4]);
|
||||
assert.sameDeepMembers(t.filterRecords({state: 'MA'}), [
|
||||
{id: 4, city: 'BOSTON', state: 'MA', amount: 0, bool: false},
|
||||
{id: 5, city: 'Boston', state: 'MA', amount: 'NA', bool: false}]);
|
||||
assert.sameDeepMembers(t.filterRowIds({state: 'MA'}), [4, 5]);
|
||||
assert.deepEqual(t.filterRecords({city: 'BOSTON', state: 'NY'}), []);
|
||||
assert.deepEqual(t.filterRowIds({city: 'BOSTON', state: 'NY'}), []);
|
||||
assert.sameDeepMembers(t.filterRecords({}), [
|
||||
{id: 1, city: 'New York', state: 'NY', amount: 5, bool: true},
|
||||
{id: 4, city: 'BOSTON', state: 'MA', amount: 0, bool: false},
|
||||
{id: 5, city: 'Boston', state: 'MA', amount: 'NA', bool: false},
|
||||
{id: 7, city: 'Seattle', state: 'WA', amount: 2, bool: false},
|
||||
]);
|
||||
assert.sameDeepMembers(t.filterRowIds({}), [1, 4, 5, 7]);
|
||||
});
|
||||
|
||||
it('should support findMatchingRow', function() {
|
||||
const t = new TableData('Foo', sampleData, {city: 'Text', state: 'Text', amount: 'Numeric', bool: 'Bool'});
|
||||
assert.equal(t.findMatchingRowId({state: 'MA'}), 4);
|
||||
assert.equal(t.findMatchingRowId({state: 'MA', bool: false}), 5);
|
||||
assert.equal(t.findMatchingRowId({city: 'Boston', state: 'MA', bool: true}), 4);
|
||||
assert.equal(t.findMatchingRowId({city: 'BOSTON', state: 'NY'}), 0);
|
||||
assert.equal(t.findMatchingRowId({statex: 'MA'}), 0);
|
||||
assert.equal(t.findMatchingRowId({id: 7}), 7);
|
||||
assert.equal(t.findMatchingRowId({}), 1);
|
||||
});
|
||||
|
||||
it('should allow getRowPropFunc to be used before loadData', function() {
|
||||
// This tests a potential bug when getRowPropFunc is saved from before loadData() is called.
|
||||
const t = new TableData('Foo', null, {city: 'Text', state: 'Text', amount: 'Numeric', bool: 'Bool'});
|
||||
verifyTableData(t, ["id", "city", "state", "amount", "bool"], []);
|
||||
assert.isFalse(t.isLoaded);
|
||||
|
||||
const getters = ["id", "city", "state", "amount", "bool"].map(c => t.getRowPropFunc(c)!);
|
||||
t.loadData(sampleData);
|
||||
assert.isTrue(t.isLoaded);
|
||||
assert.deepEqual(t.getSortedRowIds().map(r => getters.map(getter => getter(r))), [
|
||||
[1, 'New York', 'NY', 5, true],
|
||||
[4, 'Boston', 'MA', 4, true],
|
||||
[5, 'Boston', 'MA', "NA", false],
|
||||
[7, 'Seattle', 'WA', 2, false],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle Add/RemoveRecord', function() {
|
||||
const t = new TableData('Foo', sampleData, {city: 'Text', state: 'Text', amount: 'Numeric', bool: 'Bool'});
|
||||
|
||||
t.dispatchAction(["RemoveRecord", "Foo", 4]);
|
||||
verifyTableData(t, ["id", "city", "state", "amount", "bool"], [
|
||||
[1, 'New York', 'NY', 5, true],
|
||||
[5, 'Boston', 'MA', "NA", false],
|
||||
[7, 'Seattle', 'WA', 2, false],
|
||||
]);
|
||||
|
||||
t.dispatchAction(["RemoveRecord", "Foo", 7]);
|
||||
verifyTableData(t, ["id", "city", "state", "amount", "bool"], [
|
||||
[1, 'New York', 'NY', 5, true],
|
||||
[5, 'Boston', 'MA', "NA", false],
|
||||
]);
|
||||
|
||||
t.dispatchAction(["AddRecord", "Foo", 4, {city: 'BOSTON', state: 'MA', amount: 4, bool: true}]);
|
||||
verifyTableData(t, ["id", "city", "state", "amount", "bool"], [
|
||||
[1, 'New York', 'NY', 5, true],
|
||||
[4, 'BOSTON', 'MA', 4, true],
|
||||
[5, 'Boston', 'MA', "NA", false],
|
||||
]);
|
||||
|
||||
t.dispatchAction(["BulkAddRecord", "Foo", [8, 9], {
|
||||
city: ['X', 'Y'], state: ['XX', 'YY'], amount: [0.1, 0.2], bool: [null, true]
|
||||
}]);
|
||||
verifyTableData(t, ["id", "city", "state", "amount", "bool"], [
|
||||
[1, 'New York', 'NY', 5, true],
|
||||
[4, 'BOSTON', 'MA', 4, true],
|
||||
[5, 'Boston', 'MA', "NA", false],
|
||||
[8, 'X', 'XX', 0.1, null],
|
||||
[9, 'Y', 'YY', 0.2, true],
|
||||
]);
|
||||
|
||||
t.dispatchAction(["BulkRemoveRecord", "Foo", [1, 4, 9]]);
|
||||
verifyTableData(t, ["id", "city", "state", "amount", "bool"], [
|
||||
[5, 'Boston', 'MA', "NA", false],
|
||||
[8, 'X', 'XX', 0.1, null],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle UpdateRecord', function() {
|
||||
const t = new TableData('Foo', sampleData, {city: 'Text', state: 'Text', amount: 'Numeric', bool: 'Bool'});
|
||||
|
||||
t.dispatchAction(["UpdateRecord", "Foo", 4, {city: 'BOSTON', amount: 0.1}]);
|
||||
verifyTableData(t, ["id", "city", "state", "amount", "bool"], [
|
||||
[1, 'New York', 'NY', 5, true],
|
||||
[4, 'BOSTON', 'MA', 0.1, true],
|
||||
[5, 'Boston', 'MA', "NA", false],
|
||||
[7, 'Seattle', 'WA', 2, false],
|
||||
]);
|
||||
|
||||
t.dispatchAction(["BulkUpdateRecord", "Foo", [1, 7], {
|
||||
city: ['X', 'Y'], state: ['XX', 'YY'], amount: [0.1, 0.2], bool: [null, true]
|
||||
}]);
|
||||
verifyTableData(t, ["id", "city", "state", "amount", "bool"], [
|
||||
[1, 'X', 'XX', 0.1, null],
|
||||
[4, 'BOSTON', 'MA', 0.1, true],
|
||||
[5, 'Boston', 'MA', "NA", false],
|
||||
[7, 'Y', 'YY', 0.2, true],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should work correctly after AddColumn', function() {
|
||||
const t = new TableData('Foo', sampleData, {city: 'Text', state: 'Text', amount: 'Numeric', bool: 'Bool'});
|
||||
|
||||
t.dispatchAction(["AddColumn", "Foo", "foo", {type: "Text", isFormula: false, formula: ""}]);
|
||||
verifyTableData(t, ["id", "city", "state", "amount", "bool", "foo"], [
|
||||
[1, 'New York', 'NY', 5, true, ""],
|
||||
[4, 'Boston', 'MA', 4, true, ""],
|
||||
[5, 'Boston', 'MA', "NA", false, ""],
|
||||
[7, 'Seattle', 'WA', 2, false, ""],
|
||||
]);
|
||||
|
||||
t.dispatchAction(["UpdateRecord", "Foo", 4, {city: 'BOSTON', foo: "hello"}]);
|
||||
verifyTableData(t, ["id", "city", "state", "amount", "bool", "foo"], [
|
||||
[1, 'New York', 'NY', 5, true, ""],
|
||||
[4, 'BOSTON', 'MA', 4, true, "hello"],
|
||||
[5, 'Boston', 'MA', "NA", false, ""],
|
||||
[7, 'Seattle', 'WA', 2, false, ""],
|
||||
]);
|
||||
t.dispatchAction(["AddRecord", "Foo", 8, { city: 'X', state: 'XX' }]);
|
||||
verifyTableData(t, ["id", "city", "state", "amount", "bool", "foo"], [
|
||||
[1, 'New York', 'NY', 5, true, ""],
|
||||
[4, 'BOSTON', 'MA', 4, true, "hello"],
|
||||
[5, 'Boston', 'MA', "NA", false, ""],
|
||||
[7, 'Seattle', 'WA', 2, false, ""],
|
||||
[8, 'X', 'XX', 0, false, ""],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should work correctly after RenameColumn', function() {
|
||||
const t = new TableData('Foo', sampleData, {city: 'Text', state: 'Text', amount: 'Numeric', bool: 'Bool'});
|
||||
|
||||
t.dispatchAction(["RenameColumn", "Foo", "city", "ciudad"]);
|
||||
verifyTableData(t, ["id", "ciudad", "state", "amount", "bool"], [
|
||||
[1, 'New York', 'NY', 5, true ],
|
||||
[4, 'Boston', 'MA', 4, true ],
|
||||
[5, 'Boston', 'MA', "NA", false ],
|
||||
[7, 'Seattle', 'WA', 2, false ],
|
||||
]);
|
||||
|
||||
t.dispatchAction(["UpdateRecord", "Foo", 4, {ciudad: 'BOSTON', state: "XX"}]);
|
||||
verifyTableData(t, ["id", "ciudad", "state", "amount", "bool"], [
|
||||
[1, 'New York', 'NY', 5, true ],
|
||||
[4, 'BOSTON', 'XX', 4, true ],
|
||||
[5, 'Boston', 'MA', "NA", false ],
|
||||
[7, 'Seattle', 'WA', 2, false ],
|
||||
]);
|
||||
t.dispatchAction(["AddRecord", "Foo", 8, { ciudad: 'X', state: 'XX' }]);
|
||||
verifyTableData(t, ["id", "ciudad", "state", "amount", "bool"], [
|
||||
[1, 'New York', 'NY', 5, true ],
|
||||
[4, 'BOSTON', 'XX', 4, true ],
|
||||
[5, 'Boston', 'MA', "NA", false ],
|
||||
[7, 'Seattle', 'WA', 2, false ],
|
||||
[8, 'X', 'XX', 0, false ],
|
||||
]);
|
||||
});
|
||||
});
|
||||
185
test/common/ValueFormatter.ts
Normal file
185
test/common/ValueFormatter.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import {DocumentSettings} from 'app/common/DocumentSettings';
|
||||
import {NumberFormatOptions} from 'app/common/NumberFormat';
|
||||
import {parseDateTime} from 'app/common/parseDate';
|
||||
|
||||
import {createFormatter, DateTimeFormatOptions} from "app/common/ValueFormatter";
|
||||
import {assert} from 'chai';
|
||||
|
||||
const defaultDocSettings = {
|
||||
locale: 'en-US'
|
||||
};
|
||||
|
||||
const dateNumber = parseDateTime("2020-10-31 12:34:56", {});
|
||||
|
||||
describe("ValueFormatter", function() {
|
||||
describe("DateFormatter", function() {
|
||||
|
||||
function check(expected: string, dateFormat?: string) {
|
||||
for (const value of [dateNumber, ["d", dateNumber], ["D", dateNumber, "UTC"]]) {
|
||||
const actual = createFormatter("Date", {dateFormat}, defaultDocSettings).formatAny(value);
|
||||
assert.equal(actual, expected, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
it("should format dates", function() {
|
||||
check("31/10/2020", "DD/MM/YYYY");
|
||||
check("10/31/2020", "MM/DD/YYYY");
|
||||
check("2020-10-31"); // ISO by default
|
||||
});
|
||||
});
|
||||
|
||||
describe("DateTimeFormatter", function() {
|
||||
function check(expected: string, options: DateTimeFormatOptions, timezone: string = "UTC") {
|
||||
for (const value of [dateNumber, ["d", dateNumber], ["D", dateNumber, timezone]]) {
|
||||
const actual = createFormatter(`DateTime:${timezone}`, options, defaultDocSettings).formatAny(value);
|
||||
assert.equal(actual, expected, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
it("should format datetimes", function() {
|
||||
check("31/10/2020 12:34:56", {dateFormat: "DD/MM/YYYY", timeFormat: "HH:mm:ss"});
|
||||
check("10/31/2020 12:34", {dateFormat: "MM/DD/YYYY", timeFormat: "HH:mm"});
|
||||
check("2020-10-31 12:34pm", {}); // default formats
|
||||
|
||||
check("31/10/2020 08:34:56", {dateFormat: "DD/MM/YYYY", timeFormat: "HH:mm:ss"}, 'America/New_York');
|
||||
check("10/31/2020 08:34", {dateFormat: "MM/DD/YYYY", timeFormat: "HH:mm"}, 'America/New_York');
|
||||
check("2020-10-31 8:34am", {}, 'America/New_York'); // default formats
|
||||
});
|
||||
});
|
||||
|
||||
describe("NumericFormatter", function() {
|
||||
function fmt(options: NumberFormatOptions, value: number, docSettings: DocumentSettings) {
|
||||
return createFormatter("Numeric", options, docSettings).formatAny(value);
|
||||
}
|
||||
|
||||
function checkDefault(options: NumberFormatOptions, value: number, expected: string) {
|
||||
assert.equal(fmt(options, value, defaultDocSettings), expected);
|
||||
}
|
||||
|
||||
it("should support plain format", function() {
|
||||
checkDefault({}, 0, '0');
|
||||
checkDefault({}, NaN, 'NaN');
|
||||
checkDefault({}, Infinity, '∞');
|
||||
checkDefault({}, -Infinity, '-∞');
|
||||
checkDefault({}, 0.67, '0.67');
|
||||
checkDefault({}, -1234.56, '-1234.56');
|
||||
checkDefault({}, -121e+25, '-1210000000000000000000000000');
|
||||
checkDefault({}, 1.015e-8, '0.0000000102'); // maxDecimals defaults to 10 here.
|
||||
});
|
||||
|
||||
it('should support min/max decimals', function() {
|
||||
checkDefault({decimals: 2, maxDecimals: 4}, 12, '12.00');
|
||||
checkDefault({decimals: 2, maxDecimals: 4}, -1.00015, '-1.0002');
|
||||
checkDefault({decimals: 2, maxDecimals: 6}, -1.00015, '-1.00015');
|
||||
checkDefault({decimals: 6, maxDecimals: 6}, -1.00015, '-1.000150');
|
||||
checkDefault({decimals: 6, maxDecimals: 0}, -1.00015, '-1.000150');
|
||||
checkDefault({decimals: 0, maxDecimals: 2}, 12.0001, '12');
|
||||
checkDefault({decimals: 0, maxDecimals: 2}, 12.001, '12');
|
||||
checkDefault({decimals: 0, maxDecimals: 2}, 12.005, '12.01');
|
||||
checkDefault({maxDecimals: 8}, 1.015e-8, '0.00000001');
|
||||
checkDefault({maxDecimals: 7}, 1.015e-8, '0');
|
||||
|
||||
// Out-of-range values get clamped.
|
||||
checkDefault({decimals:-2, maxDecimals:3}, -1.2345, "-1.235");
|
||||
checkDefault({decimals:-2, maxDecimals:-3}, -1.2345, "-1");
|
||||
});
|
||||
|
||||
it('should support thousand separators', function() {
|
||||
checkDefault({numMode: 'decimal', decimals: 4}, 1000000, '1,000,000.0000');
|
||||
checkDefault({numMode: 'decimal'}, -1234.56, '-1,234.56');
|
||||
checkDefault({numMode: 'decimal'}, -121e+25, '-1,210,000,000,000,000,000,000,000,000');
|
||||
checkDefault({numMode: 'decimal'}, 0.1234567, '0.123'); // maxDecimals defaults to 3 here
|
||||
checkDefault({numMode: 'decimal'}, 1.015e-8, '0');
|
||||
checkDefault({numMode: 'decimal', maxDecimals: 10}, 1.015e-8, '0.0000000102');
|
||||
});
|
||||
|
||||
it('should support currency mode', function() {
|
||||
// Test currency formatting with default doc settings (locale: 'en-US').
|
||||
checkDefault({numMode: 'currency'}, 1000000, '$1,000,000.00');
|
||||
checkDefault({numMode: 'currency', decimals: 4}, 1000000, '$1,000,000.0000');
|
||||
checkDefault({numMode: 'currency'}, -1234.565, '-$1,234.57');
|
||||
checkDefault({numMode: 'currency'}, -121e+25, '-$1,210,000,000,000,000,000,000,000,000.00');
|
||||
checkDefault({numMode: 'currency'}, 0.1234567, '$0.12'); // maxDecimals defaults to 2 here
|
||||
checkDefault({numMode: 'currency', maxDecimals: 0}, 12.34567, '$12.35');
|
||||
checkDefault({numMode: 'currency', decimals: 0, maxDecimals: 0}, 12.34567, '$12');
|
||||
checkDefault({numMode: 'currency'}, 1.015e-8, '$0.00');
|
||||
checkDefault({numMode: 'currency', maxDecimals: 10}, 1.015e-8, '$0.0000000102');
|
||||
checkDefault({numMode: 'currency'}, -1.015e-8, '-$0.00');
|
||||
|
||||
// Test currency formatting with custom locales.
|
||||
assert.equal(fmt({numMode: 'currency'}, 1000000, {locale: 'es-ES'}), '1.000.000,00 €');
|
||||
assert.equal(fmt({numMode: 'currency', decimals: 4}, 1000000, {locale: 'en-NZ'}), '$1,000,000.0000');
|
||||
assert.equal(fmt({numMode: 'currency'}, -1234.565, {locale: 'de-CH'}), 'CHF-1’234.57');
|
||||
assert.equal(fmt({numMode: 'currency'}, -121e+25, {locale: 'es-AR'}),
|
||||
'-$ 1.210.000.000.000.000.000.000.000.000,00');
|
||||
assert.equal(fmt({numMode: 'currency'}, 0.1234567, {locale: 'fr-BE'}), '0,12 €');
|
||||
assert.equal(fmt({numMode: 'currency', maxDecimals: 0}, 12.34567, {locale: 'en-GB'}), '£12.35');
|
||||
assert.equal(fmt({numMode: 'currency', decimals: 0, maxDecimals: 0}, 12.34567, {locale: 'en-IE'}), '€12');
|
||||
assert.equal(fmt({numMode: 'currency'}, 1.015e-8, {locale: 'en-ZA'}), 'R 0,00');
|
||||
assert.equal(fmt({numMode: 'currency', maxDecimals: 10}, 1.015e-8, {locale: 'en-CA'}), '$0.0000000102');
|
||||
assert.equal(fmt({numMode: 'currency'}, -1.015e-8, {locale: 'nl-BE'}), '€ -0,00');
|
||||
|
||||
// Test currency formatting with custom currency AND locales (e.g. column-specific currency setting).
|
||||
assert.equal(fmt({numMode: 'currency'}, 1000000, {locale: 'es-ES', currency: 'USD'}), '1.000.000,00 $');
|
||||
assert.equal(
|
||||
fmt({numMode: 'currency', decimals: 4}, 1000000, {locale: 'en-NZ', currency: 'JPY'}),
|
||||
'¥1,000,000.0000');
|
||||
assert.equal(fmt({numMode: 'currency'}, -1234.565, {locale: 'de-CH', currency: 'JMD'}), '$-1’234.57');
|
||||
assert.equal(
|
||||
fmt({numMode: 'currency'}, -121e+25, {locale: 'es-AR', currency: 'GBP'}),
|
||||
'-£ 1.210.000.000.000.000.000.000.000.000,00');
|
||||
assert.equal(fmt({numMode: 'currency'}, 0.1234567, {locale: 'fr-BE', currency: 'GBP'}), '0,12 £');
|
||||
assert.equal(fmt({numMode: 'currency', maxDecimals: 0}, 12.34567, {locale: 'en-GB', currency: 'USD'}), '$12.35');
|
||||
assert.equal(
|
||||
fmt({numMode: 'currency', decimals: 0, maxDecimals: 0}, 12.34567, {locale: 'en-IE', currency: 'SGD'}),
|
||||
'$12');
|
||||
assert.equal(fmt({numMode: 'currency'}, 1.015e-8, {locale: 'en-ZA', currency: 'HKD'}), '$0,00');
|
||||
assert.equal(
|
||||
fmt({numMode: 'currency', maxDecimals: 10}, 1.015e-8, {locale: 'en-CA', currency: 'RUB'}),
|
||||
'₽0.0000000102');
|
||||
assert.equal(fmt({numMode: 'currency'}, -1.015e-8, {locale: 'nl-BE', currency: 'USD'}), '$ -0,00');
|
||||
});
|
||||
|
||||
it('should support percentages', function() {
|
||||
checkDefault({numMode: 'percent'}, 0.5, '50%');
|
||||
checkDefault({numMode: 'percent'}, -0.15, '-15%');
|
||||
checkDefault({numMode: 'percent'}, 0.105, '11%');
|
||||
checkDefault({numMode: 'percent', maxDecimals: 5}, 0.105, '10.5%');
|
||||
checkDefault({numMode: 'percent', decimals: 5}, 0.105, '10.50000%');
|
||||
checkDefault({numMode: 'percent', maxDecimals: 2}, 1.2345, '123.45%');
|
||||
checkDefault({numMode: 'percent'}, -1234.567, '-123,457%'); // maxDecimals defaults to 0 here
|
||||
checkDefault({numMode: 'percent'}, 1.015e-8, '0%');
|
||||
checkDefault({numMode: 'percent', maxDecimals: 10}, 1.015e-8, '0.000001015%');
|
||||
});
|
||||
|
||||
it('should support parentheses for negative numbers', function() {
|
||||
checkDefault({numSign: 'parens', numMode: 'decimal'}, -1234.56, '(1,234.56)');
|
||||
checkDefault({numSign: 'parens', numMode: 'decimal'}, +1234.56, ' 1,234.56 ');
|
||||
checkDefault({numSign: 'parens', numMode: 'decimal'}, -121e+25, '(1,210,000,000,000,000,000,000,000,000)');
|
||||
checkDefault({numSign: 'parens', numMode: 'decimal'}, 0.1234567, ' 0.123 ');
|
||||
checkDefault({numSign: 'parens', numMode: 'decimal'}, 1.015e-8, ' 0 ');
|
||||
checkDefault({numSign: 'parens', numMode: 'currency'}, -1234.565, '($1,234.57)');
|
||||
checkDefault({numSign: 'parens', numMode: 'currency'}, -121e+20, '($12,100,000,000,000,000,000,000.00)');
|
||||
checkDefault({numSign: 'parens', numMode: 'currency'}, 121e+20, ' $12,100,000,000,000,000,000,000.00 ');
|
||||
checkDefault({numSign: 'parens', numMode: 'currency'}, 1.015e-8, ' $0.00 ');
|
||||
checkDefault({numSign: 'parens', numMode: 'currency'}, -1.015e-8, '($0.00)');
|
||||
checkDefault({numSign: 'parens'}, -1234.56, '(1234.56)');
|
||||
checkDefault({numSign: 'parens'}, +1234.56, ' 1234.56 ');
|
||||
checkDefault({numSign: 'parens', numMode: 'percent'}, -0.1234, '(12%)');
|
||||
checkDefault({numSign: 'parens', numMode: 'percent'}, +0.1234, ' 12% ');
|
||||
});
|
||||
|
||||
it('should support scientific mode', function() {
|
||||
checkDefault({numMode: 'scientific'}, 0.5, '5E-1');
|
||||
checkDefault({numMode: 'scientific'}, -0.15, '-1.5E-1');
|
||||
checkDefault({numMode: 'scientific'}, -1234.56, '-1.235E3');
|
||||
checkDefault({numMode: 'scientific'}, +1234.56, '1.235E3');
|
||||
checkDefault({numMode: 'scientific'}, 1.015e-8, '1.015E-8');
|
||||
checkDefault({numMode: 'scientific', maxDecimals: 10}, 1.015e-8, '1.015E-8');
|
||||
checkDefault({numMode: 'scientific', decimals: 10}, 1.015e-8, '1.0150000000E-8');
|
||||
checkDefault({numMode: 'scientific', maxDecimals: 2}, 1.015e-8, '1.02E-8');
|
||||
checkDefault({numMode: 'scientific', maxDecimals: 1}, 1.015e-8, '1E-8');
|
||||
checkDefault({numMode: 'scientific'}, -121e+25, '-1.21E27');
|
||||
});
|
||||
});
|
||||
});
|
||||
241
test/common/ValueGuesser.ts
Normal file
241
test/common/ValueGuesser.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import {arrayRepeat} from 'app/common/gutil';
|
||||
import {guessColInfo, guessColInfoForImports, GuessResult} from 'app/common/ValueGuesser';
|
||||
import {assert} from 'chai';
|
||||
|
||||
const defaultDocSettings = {
|
||||
locale: 'en-US'
|
||||
};
|
||||
|
||||
function check(values: Array<string | null>, expectedResult: GuessResult) {
|
||||
const result = guessColInfo(values, defaultDocSettings, "America/New_York");
|
||||
assert.deepEqual(result, expectedResult);
|
||||
}
|
||||
|
||||
|
||||
describe("ValueGuesser", function() {
|
||||
it("should guess booleans and numbers correctly", function() {
|
||||
check(
|
||||
["true", "false"],
|
||||
{
|
||||
values: [true, false],
|
||||
colInfo: {type: 'Bool'},
|
||||
},
|
||||
);
|
||||
|
||||
// 1 and 0 in a boolean column would be converted to true and false,
|
||||
// but they're guessed as numbers, not booleans
|
||||
check(
|
||||
["1", "0"],
|
||||
{
|
||||
values: [1, 0],
|
||||
colInfo: {type: 'Numeric'},
|
||||
},
|
||||
);
|
||||
|
||||
// Even here, guessing booleans would be sensible, but the original values would be lost
|
||||
// if the user didn't like the guess and converted boolean column was converted back to Text.
|
||||
// Also note that when we fallback to Text without any parsing, guessColInfo doesn't return any values,
|
||||
// as sending them back to the data engine would be wasteful.
|
||||
check(
|
||||
["true", "false", "1", "0"],
|
||||
{colInfo: {type: 'Text'}},
|
||||
);
|
||||
|
||||
// Now that 90% if the values are straightforward booleans, it guesses Bool
|
||||
// "0" is still not parsed by guessColInfo as it's trying to be lossless.
|
||||
// However, it will actually be converted in Python by Bool.do_convert,
|
||||
// so this is a small way information can still be lost.
|
||||
check(
|
||||
[...arrayRepeat(9, "true"), "0"],
|
||||
{
|
||||
values: [...arrayRepeat(9, true), "0"],
|
||||
colInfo: {type: 'Bool'},
|
||||
},
|
||||
);
|
||||
|
||||
// If there are blank values ("" or null) then leave them as text,
|
||||
// because the data engine would convert them to false which would lose info.
|
||||
check(
|
||||
["true", ""],
|
||||
{colInfo: {type: 'Text'}},
|
||||
);
|
||||
check(
|
||||
["false", null],
|
||||
{colInfo: {type: 'Text'}},
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle formatted numbers", function() {
|
||||
check(
|
||||
["0.0", "1.0"],
|
||||
{
|
||||
values: [0, 1],
|
||||
colInfo: {type: "Numeric", widgetOptions: {decimals: 1}},
|
||||
}
|
||||
);
|
||||
|
||||
check(
|
||||
["$1.00"],
|
||||
{
|
||||
values: [1],
|
||||
colInfo: {type: "Numeric", widgetOptions: {numMode: "currency", decimals: 2}},
|
||||
}
|
||||
);
|
||||
|
||||
check(
|
||||
["$1"],
|
||||
{
|
||||
values: [1],
|
||||
colInfo: {type: "Numeric", widgetOptions: {numMode: "currency", decimals: 0}},
|
||||
}
|
||||
);
|
||||
|
||||
// Inconsistent number of decimal places
|
||||
check(
|
||||
["$1", "$1.00"],
|
||||
{colInfo: {type: 'Text'}},
|
||||
);
|
||||
|
||||
// Inconsistent use of currency
|
||||
check(
|
||||
["1.00", "$1.00"],
|
||||
{colInfo: {type: 'Text'}},
|
||||
);
|
||||
|
||||
check(
|
||||
["500", "6000"],
|
||||
{
|
||||
values: [500, 6000],
|
||||
colInfo: {type: "Numeric"},
|
||||
}
|
||||
);
|
||||
check(
|
||||
["500", "6,000"],
|
||||
{
|
||||
values: [500, 6000],
|
||||
colInfo: {type: "Numeric", widgetOptions: {numMode: "decimal"}},
|
||||
}
|
||||
);
|
||||
// Inconsistent use of thousands separators
|
||||
check(
|
||||
["5000", "6,000"],
|
||||
{colInfo: {type: 'Text'}},
|
||||
);
|
||||
});
|
||||
|
||||
it("should guess dates and datetimes correctly", function() {
|
||||
check(
|
||||
["1970-01-21", null, ""],
|
||||
{
|
||||
// The number represents 1970-01-21 parsed to a timestamp.
|
||||
// null and "" are converted to null.
|
||||
values: [20 * 24 * 60 * 60, null, null],
|
||||
colInfo: {
|
||||
type: 'Date',
|
||||
widgetOptions: {
|
||||
dateFormat: "YYYY-MM-DD",
|
||||
timeFormat: "",
|
||||
isCustomDateFormat: false,
|
||||
isCustomTimeFormat: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
check(
|
||||
["1970-01-01 05:00:00"],
|
||||
{
|
||||
// 05:00 in the given timezone is 10:00 in UTC
|
||||
values: [10 * 60 * 60],
|
||||
colInfo: {
|
||||
// "America/New_York" is the timezone given by `check`
|
||||
type: 'DateTime:America/New_York',
|
||||
widgetOptions: {
|
||||
dateFormat: "YYYY-MM-DD",
|
||||
timeFormat: "HH:mm:ss",
|
||||
isCustomDateFormat: false,
|
||||
isCustomTimeFormat: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// A mixture of Date and DateTime cannot be guessed as either, fallback to Text
|
||||
check(
|
||||
[
|
||||
"1970-01-01",
|
||||
"1970-01-01",
|
||||
"1970-01-01",
|
||||
"1970-01-01 05:00:00",
|
||||
],
|
||||
{colInfo: {type: 'Text'}},
|
||||
);
|
||||
});
|
||||
|
||||
it("should require 90% of values to be parsed", function() {
|
||||
// 90% of the strings can be parsed to numbers, so guess Numeric.
|
||||
check(
|
||||
[...arrayRepeat(9, "12"), "foo"],
|
||||
{
|
||||
values: [...arrayRepeat(9, 12), "foo"],
|
||||
colInfo: {type: 'Numeric'},
|
||||
},
|
||||
);
|
||||
|
||||
// Less than 90% are numbers, so fallback to Text
|
||||
check(
|
||||
[...arrayRepeat(8, "12"), "foo"],
|
||||
{colInfo: {type: 'Text'}},
|
||||
);
|
||||
|
||||
// Same as the previous two checks but with a bunch of blanks
|
||||
check(
|
||||
[...arrayRepeat(9, "12"), "foo", ...arrayRepeat(90, "")],
|
||||
{
|
||||
values: [...arrayRepeat(9, 12), "foo", ...arrayRepeat(90, null)],
|
||||
colInfo: {type: 'Numeric'},
|
||||
},
|
||||
);
|
||||
check(
|
||||
[...arrayRepeat(8, "12"), "foo", ...arrayRepeat(90, "")],
|
||||
{colInfo: {type: 'Text'}},
|
||||
);
|
||||
|
||||
// Just a bunch of blanks and text, no numbers or anything
|
||||
check(
|
||||
[...arrayRepeat(100, null), "foo", "bar"],
|
||||
{colInfo: {type: 'Text'}},
|
||||
);
|
||||
});
|
||||
|
||||
describe("guessColInfoForImports", function() {
|
||||
// Prepare dummy docData; just the minimum to satisfy the code that uses it.
|
||||
const docData: any = {
|
||||
docSettings: () => defaultDocSettings,
|
||||
docInfo: () => ({timezone: 'America/New_York'}),
|
||||
};
|
||||
it("should guess empty column when all cells are empty", function() {
|
||||
assert.deepEqual(guessColInfoForImports([null, "", "", null], docData), {
|
||||
values: [null, "", "", null],
|
||||
colMetadata: {type: 'Any', isFormula: true, formula: ''}
|
||||
});
|
||||
});
|
||||
it("should do proper numeric format guessing for a mix of number/string types", function() {
|
||||
assert.deepEqual(guessColInfoForImports([-5.5, "1,234.6", null, 0], docData), {
|
||||
values: [-5.5, 1234.6, null, 0],
|
||||
colMetadata: {type: 'Numeric', widgetOptions: '{"numMode":"decimal"}'}
|
||||
});
|
||||
});
|
||||
it("should not guess empty column when values are not actually empty", function() {
|
||||
assert.deepEqual(guessColInfoForImports([null, 0, "", false], docData), {
|
||||
values: [null, 0, "", false],
|
||||
colMetadata: {type: 'Text'}
|
||||
});
|
||||
});
|
||||
it("should do no guessing for object values", function() {
|
||||
assert.deepEqual(guessColInfoForImports(["test", ['L' as any, 1]], docData), {
|
||||
values: ["test", ['L' as any, 1]]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
619
test/common/arraySplice.js
Normal file
619
test/common/arraySplice.js
Normal file
@@ -0,0 +1,619 @@
|
||||
/* global describe, it */
|
||||
|
||||
var _ = require('underscore');
|
||||
var assert = require('chai').assert;
|
||||
var gutil = require('app/common/gutil');
|
||||
var utils = require('../utils');
|
||||
|
||||
/**
|
||||
* Set env ENABLE_TIMING_TESTS=1 to run the timing tests.
|
||||
* These tests rely on mocha's reported timings to allow you to compare the performance of
|
||||
* different implementations.
|
||||
*/
|
||||
var ENABLE_TIMING_TESTS = Boolean(process.env.ENABLE_TIMING_TESTS);
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
// Following recommendations such as here:
|
||||
// http://stackoverflow.com/questions/7032550/javascript-insert-an-array-inside-another-array
|
||||
// However, this won't work for large arrToInsert because .apply has a limit on length of args.
|
||||
function spliceApplyConcat(target, start, arrToInsert) {
|
||||
target.splice.apply(target, [start, 0].concat(arrToInsert));
|
||||
return target;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
// Seems like could be faster, but disturbingly mutates the last argument.
|
||||
// However, this won't work for large arrToInsert because .apply has a limit on length of args.
|
||||
function spliceApplyUnshift(target, start, arrToInsert) {
|
||||
var spliceArgs = arrToInsert;
|
||||
spliceArgs.unshift(start, 0);
|
||||
try {
|
||||
target.splice.apply(target, spliceArgs);
|
||||
} finally {
|
||||
spliceArgs.splice(0, 2);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
// This is from the same stackoverflow answer, but builds a new array instead of mutating target.
|
||||
function nonSpliceUsingSlice(target, start, arrToInsert) {
|
||||
return target.slice(0, start).concat(arrToInsert, target.slice(start));
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
// A simple manual implementation, that performs reasonably well in all environments.
|
||||
function spliceManualWithTailCopy(target, start, arrToInsert) {
|
||||
var insLen = arrToInsert.length;
|
||||
if (insLen === 1) {
|
||||
target.splice(start, 0, arrToInsert[0]);
|
||||
} else if (insLen > 1) {
|
||||
var i, len, tail = target.slice(start);
|
||||
for (i = 0; i < insLen; i++, start++) {
|
||||
target[start] = arrToInsert[i];
|
||||
}
|
||||
for (i = 0, len = tail.length; i < len; i++, start++) {
|
||||
target[start] = tail[i];
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
function spliceCopyWithTail(helpers) {
|
||||
var copyForward = helpers.copyForward;
|
||||
return function(target, start, arrToInsert) {
|
||||
var tail = target.slice(start), insLen = arrToInsert.length;
|
||||
copyForward(target, start, arrToInsert, 0, insLen);
|
||||
copyForward(target, start + insLen, tail, 0, tail.length);
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
// This implementation avoids creating a copy of the tail, but fills in the array
|
||||
// non-contiguously.
|
||||
function spliceFwdBackCopy(helpers) {
|
||||
var copyForward = helpers.copyForward,
|
||||
copyBackward = helpers.copyBackward;
|
||||
return function(target, start, arrayToInsert) {
|
||||
var count = arrayToInsert.length;
|
||||
copyBackward(target, start + count, target, start, target.length - start);
|
||||
copyForward(target, start, arrayToInsert, 0, count);
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
// This implementation tries to be smarter by avoiding allocations, appending to the array
|
||||
// contiguously, then filling in the gap.
|
||||
function spliceAppendCopy(helpers) {
|
||||
var appendFunc = helpers.append,
|
||||
copyForward = helpers.copyForward,
|
||||
copyBackward = helpers.copyBackward;
|
||||
return function(target, start, arrToInsert) {
|
||||
var origLen = target.length;
|
||||
var tailLen = origLen - start;
|
||||
var insLen = arrToInsert.length;
|
||||
if (insLen > tailLen) {
|
||||
appendFunc(target, arrToInsert, tailLen, insLen - tailLen);
|
||||
appendFunc(target, target, start, tailLen);
|
||||
copyForward(target, start, arrToInsert, 0, tailLen);
|
||||
} else {
|
||||
appendFunc(target, target, origLen - insLen, insLen);
|
||||
copyBackward(target, start + insLen, target, start, tailLen - insLen);
|
||||
copyForward(target, start, arrToInsert, 0, insLen);
|
||||
}
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
// This implementation only appends, but requires splicing out the tail from the original.
|
||||
// It is consistently slower on Node.
|
||||
function spliceAppendOnly(helpers) {
|
||||
var appendFunc = helpers.append;
|
||||
return function(target, start, arrToInsert) {
|
||||
var tail = target.splice(start, target.length);
|
||||
appendFunc(target, arrToInsert, 0, arrToInsert.length);
|
||||
appendFunc(target, tail, 0, tail.length);
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// COPY-FORWARD FUNCTIONS
|
||||
//----------------------------------------------------------------------
|
||||
var copyForward = {
|
||||
gutil: gutil.arrayCopyForward,
|
||||
|
||||
copyForward1: function(toArray, toStart, fromArray, fromStart, count) {
|
||||
for (var end = toStart + count; toStart < end; ++toStart, ++fromStart) {
|
||||
toArray[toStart] = fromArray[fromStart];
|
||||
}
|
||||
},
|
||||
|
||||
copyForward8: function(toArray, toStart, fromArray, fromStart, count) {
|
||||
var end = toStart + count;
|
||||
for (var xend = end - 7; toStart < xend; fromStart += 8, toStart += 8) {
|
||||
toArray[toStart] = fromArray[fromStart];
|
||||
toArray[toStart+1] = fromArray[fromStart+1];
|
||||
toArray[toStart+2] = fromArray[fromStart+2];
|
||||
toArray[toStart+3] = fromArray[fromStart+3];
|
||||
toArray[toStart+4] = fromArray[fromStart+4];
|
||||
toArray[toStart+5] = fromArray[fromStart+5];
|
||||
toArray[toStart+6] = fromArray[fromStart+6];
|
||||
toArray[toStart+7] = fromArray[fromStart+7];
|
||||
}
|
||||
for (; toStart < end; ++fromStart, ++toStart) {
|
||||
toArray[toStart] = fromArray[fromStart];
|
||||
}
|
||||
},
|
||||
|
||||
copyForward64: function(toArray, toStart, fromArray, fromStart, count) {
|
||||
var end = toStart + count;
|
||||
for (var xend = end - 63; toStart < xend; fromStart += 64, toStart += 64) {
|
||||
toArray[toStart]=fromArray[fromStart]; toArray[toStart+1]=fromArray[fromStart+1];
|
||||
toArray[toStart+2]=fromArray[fromStart+2]; toArray[toStart+3]=fromArray[fromStart+3];
|
||||
toArray[toStart+4]=fromArray[fromStart+4]; toArray[toStart+5]=fromArray[fromStart+5];
|
||||
toArray[toStart+6]=fromArray[fromStart+6]; toArray[toStart+7]=fromArray[fromStart+7];
|
||||
toArray[toStart+8]=fromArray[fromStart+8]; toArray[toStart+9]=fromArray[fromStart+9];
|
||||
toArray[toStart+10]=fromArray[fromStart+10]; toArray[toStart+11]=fromArray[fromStart+11];
|
||||
toArray[toStart+12]=fromArray[fromStart+12]; toArray[toStart+13]=fromArray[fromStart+13];
|
||||
toArray[toStart+14]=fromArray[fromStart+14]; toArray[toStart+15]=fromArray[fromStart+15];
|
||||
toArray[toStart+16]=fromArray[fromStart+16]; toArray[toStart+17]=fromArray[fromStart+17];
|
||||
toArray[toStart+18]=fromArray[fromStart+18]; toArray[toStart+19]=fromArray[fromStart+19];
|
||||
toArray[toStart+20]=fromArray[fromStart+20]; toArray[toStart+21]=fromArray[fromStart+21];
|
||||
toArray[toStart+22]=fromArray[fromStart+22]; toArray[toStart+23]=fromArray[fromStart+23];
|
||||
toArray[toStart+24]=fromArray[fromStart+24]; toArray[toStart+25]=fromArray[fromStart+25];
|
||||
toArray[toStart+26]=fromArray[fromStart+26]; toArray[toStart+27]=fromArray[fromStart+27];
|
||||
toArray[toStart+28]=fromArray[fromStart+28]; toArray[toStart+29]=fromArray[fromStart+29];
|
||||
toArray[toStart+30]=fromArray[fromStart+30]; toArray[toStart+31]=fromArray[fromStart+31];
|
||||
toArray[toStart+32]=fromArray[fromStart+32]; toArray[toStart+33]=fromArray[fromStart+33];
|
||||
toArray[toStart+34]=fromArray[fromStart+34]; toArray[toStart+35]=fromArray[fromStart+35];
|
||||
toArray[toStart+36]=fromArray[fromStart+36]; toArray[toStart+37]=fromArray[fromStart+37];
|
||||
toArray[toStart+38]=fromArray[fromStart+38]; toArray[toStart+39]=fromArray[fromStart+39];
|
||||
toArray[toStart+40]=fromArray[fromStart+40]; toArray[toStart+41]=fromArray[fromStart+41];
|
||||
toArray[toStart+42]=fromArray[fromStart+42]; toArray[toStart+43]=fromArray[fromStart+43];
|
||||
toArray[toStart+44]=fromArray[fromStart+44]; toArray[toStart+45]=fromArray[fromStart+45];
|
||||
toArray[toStart+46]=fromArray[fromStart+46]; toArray[toStart+47]=fromArray[fromStart+47];
|
||||
toArray[toStart+48]=fromArray[fromStart+48]; toArray[toStart+49]=fromArray[fromStart+49];
|
||||
toArray[toStart+50]=fromArray[fromStart+50]; toArray[toStart+51]=fromArray[fromStart+51];
|
||||
toArray[toStart+52]=fromArray[fromStart+52]; toArray[toStart+53]=fromArray[fromStart+53];
|
||||
toArray[toStart+54]=fromArray[fromStart+54]; toArray[toStart+55]=fromArray[fromStart+55];
|
||||
toArray[toStart+56]=fromArray[fromStart+56]; toArray[toStart+57]=fromArray[fromStart+57];
|
||||
toArray[toStart+58]=fromArray[fromStart+58]; toArray[toStart+59]=fromArray[fromStart+59];
|
||||
toArray[toStart+60]=fromArray[fromStart+60]; toArray[toStart+61]=fromArray[fromStart+61];
|
||||
toArray[toStart+62]=fromArray[fromStart+62]; toArray[toStart+63]=fromArray[fromStart+63];
|
||||
}
|
||||
for (; toStart < end; ++fromStart, ++toStart) {
|
||||
toArray[toStart] = fromArray[fromStart];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// COPY-BACKWARD FUNCTIONS
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
var copyBackward = {
|
||||
gutil: gutil.arrayCopyBackward,
|
||||
|
||||
copyBackward1: function(toArray, toStart, fromArray, fromStart, count) {
|
||||
for (var i = toStart + count - 1, j = fromStart + count - 1; i >= toStart; --i, --j) {
|
||||
toArray[i] = fromArray[j];
|
||||
}
|
||||
},
|
||||
|
||||
copyBackward8: function(toArray, toStart, fromArray, fromStart, count) {
|
||||
var i = toStart + count - 1, j = fromStart + count - 1;
|
||||
for (var xStart = toStart + 7; i >= xStart; i -= 8, j -= 8) {
|
||||
toArray[i] = fromArray[j];
|
||||
toArray[i-1] = fromArray[j-1];
|
||||
toArray[i-2] = fromArray[j-2];
|
||||
toArray[i-3] = fromArray[j-3];
|
||||
toArray[i-4] = fromArray[j-4];
|
||||
toArray[i-5] = fromArray[j-5];
|
||||
toArray[i-6] = fromArray[j-6];
|
||||
toArray[i-7] = fromArray[j-7];
|
||||
}
|
||||
for ( ; i >= toStart; --i, --j) {
|
||||
toArray[i] = fromArray[j];
|
||||
}
|
||||
},
|
||||
|
||||
copyBackward64: function(toArray, toStart, fromArray, fromStart, count) {
|
||||
var i = toStart + count - 1, j = fromStart + count - 1;
|
||||
for (var xStart = toStart + 63; i >= xStart; i -= 64, j -= 64) {
|
||||
toArray[i]=fromArray[j]; toArray[i-1]=fromArray[j-1];
|
||||
toArray[i-2]=fromArray[j-2]; toArray[i-3]=fromArray[j-3];
|
||||
toArray[i-4]=fromArray[j-4]; toArray[i-5]=fromArray[j-5];
|
||||
toArray[i-6]=fromArray[j-6]; toArray[i-7]=fromArray[j-7];
|
||||
toArray[i-8]=fromArray[j-8]; toArray[i-9]=fromArray[j-9];
|
||||
toArray[i-10]=fromArray[j-10]; toArray[i-11]=fromArray[j-11];
|
||||
toArray[i-12]=fromArray[j-12]; toArray[i-13]=fromArray[j-13];
|
||||
toArray[i-14]=fromArray[j-14]; toArray[i-15]=fromArray[j-15];
|
||||
toArray[i-16]=fromArray[j-16]; toArray[i-17]=fromArray[j-17];
|
||||
toArray[i-18]=fromArray[j-18]; toArray[i-19]=fromArray[j-19];
|
||||
toArray[i-20]=fromArray[j-20]; toArray[i-21]=fromArray[j-21];
|
||||
toArray[i-22]=fromArray[j-22]; toArray[i-23]=fromArray[j-23];
|
||||
toArray[i-24]=fromArray[j-24]; toArray[i-25]=fromArray[j-25];
|
||||
toArray[i-26]=fromArray[j-26]; toArray[i-27]=fromArray[j-27];
|
||||
toArray[i-28]=fromArray[j-28]; toArray[i-29]=fromArray[j-29];
|
||||
toArray[i-30]=fromArray[j-30]; toArray[i-31]=fromArray[j-31];
|
||||
toArray[i-32]=fromArray[j-32]; toArray[i-33]=fromArray[j-33];
|
||||
toArray[i-34]=fromArray[j-34]; toArray[i-35]=fromArray[j-35];
|
||||
toArray[i-36]=fromArray[j-36]; toArray[i-37]=fromArray[j-37];
|
||||
toArray[i-38]=fromArray[j-38]; toArray[i-39]=fromArray[j-39];
|
||||
toArray[i-40]=fromArray[j-40]; toArray[i-41]=fromArray[j-41];
|
||||
toArray[i-42]=fromArray[j-42]; toArray[i-43]=fromArray[j-43];
|
||||
toArray[i-44]=fromArray[j-44]; toArray[i-45]=fromArray[j-45];
|
||||
toArray[i-46]=fromArray[j-46]; toArray[i-47]=fromArray[j-47];
|
||||
toArray[i-48]=fromArray[j-48]; toArray[i-49]=fromArray[j-49];
|
||||
toArray[i-50]=fromArray[j-50]; toArray[i-51]=fromArray[j-51];
|
||||
toArray[i-52]=fromArray[j-52]; toArray[i-53]=fromArray[j-53];
|
||||
toArray[i-54]=fromArray[j-54]; toArray[i-55]=fromArray[j-55];
|
||||
toArray[i-56]=fromArray[j-56]; toArray[i-57]=fromArray[j-57];
|
||||
toArray[i-58]=fromArray[j-58]; toArray[i-59]=fromArray[j-59];
|
||||
toArray[i-60]=fromArray[j-60]; toArray[i-61]=fromArray[j-61];
|
||||
toArray[i-62]=fromArray[j-62]; toArray[i-63]=fromArray[j-63];
|
||||
}
|
||||
for ( ; i >= toStart; --i, --j) {
|
||||
toArray[i] = fromArray[j];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// APPEND FUNCTIONS.
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
var append = {
|
||||
gutil: gutil.arrayAppend,
|
||||
|
||||
append1: function(toArray, fromArray, fromStart, count) {
|
||||
var end = fromStart + count;
|
||||
for (var i = fromStart; i < end; i++) {
|
||||
toArray.push(fromArray[i]);
|
||||
}
|
||||
},
|
||||
|
||||
appendCopy1: function(toArray, fromArray, fromStart, count) {
|
||||
if (count === 1) {
|
||||
toArray.push(fromArray[fromStart]);
|
||||
} else if (count > 1) {
|
||||
var len = toArray.length;
|
||||
toArray.length = len + count;
|
||||
copyForward.copyForward1(toArray, len, fromArray, fromStart, count);
|
||||
}
|
||||
},
|
||||
|
||||
append8: function(toArray, fromArray, fromStart, count) {
|
||||
var end = fromStart + count;
|
||||
for (var xend = end - 7; fromStart < xend; fromStart += 8) {
|
||||
toArray.push(
|
||||
fromArray[fromStart],
|
||||
fromArray[fromStart + 1],
|
||||
fromArray[fromStart + 2],
|
||||
fromArray[fromStart + 3],
|
||||
fromArray[fromStart + 4],
|
||||
fromArray[fromStart + 5],
|
||||
fromArray[fromStart + 6],
|
||||
fromArray[fromStart + 7]);
|
||||
}
|
||||
for ( ; fromStart < end; ++fromStart) {
|
||||
toArray.push(fromArray[fromStart]);
|
||||
}
|
||||
},
|
||||
|
||||
append64: function(toArray, fromArray, fromStart, count) {
|
||||
var end = fromStart + count;
|
||||
for (var xend = end - 63; fromStart < xend; fromStart += 64) {
|
||||
toArray.push(
|
||||
fromArray[fromStart], fromArray[fromStart + 1],
|
||||
fromArray[fromStart + 2], fromArray[fromStart + 3],
|
||||
fromArray[fromStart + 4], fromArray[fromStart + 5],
|
||||
fromArray[fromStart + 6], fromArray[fromStart + 7],
|
||||
fromArray[fromStart + 8], fromArray[fromStart + 9],
|
||||
fromArray[fromStart + 10], fromArray[fromStart + 11],
|
||||
fromArray[fromStart + 12], fromArray[fromStart + 13],
|
||||
fromArray[fromStart + 14], fromArray[fromStart + 15],
|
||||
fromArray[fromStart + 16], fromArray[fromStart + 17],
|
||||
fromArray[fromStart + 18], fromArray[fromStart + 19],
|
||||
fromArray[fromStart + 20], fromArray[fromStart + 21],
|
||||
fromArray[fromStart + 22], fromArray[fromStart + 23],
|
||||
fromArray[fromStart + 24], fromArray[fromStart + 25],
|
||||
fromArray[fromStart + 26], fromArray[fromStart + 27],
|
||||
fromArray[fromStart + 28], fromArray[fromStart + 29],
|
||||
fromArray[fromStart + 30], fromArray[fromStart + 31],
|
||||
fromArray[fromStart + 32], fromArray[fromStart + 33],
|
||||
fromArray[fromStart + 34], fromArray[fromStart + 35],
|
||||
fromArray[fromStart + 36], fromArray[fromStart + 37],
|
||||
fromArray[fromStart + 38], fromArray[fromStart + 39],
|
||||
fromArray[fromStart + 40], fromArray[fromStart + 41],
|
||||
fromArray[fromStart + 42], fromArray[fromStart + 43],
|
||||
fromArray[fromStart + 44], fromArray[fromStart + 45],
|
||||
fromArray[fromStart + 46], fromArray[fromStart + 47],
|
||||
fromArray[fromStart + 48], fromArray[fromStart + 49],
|
||||
fromArray[fromStart + 50], fromArray[fromStart + 51],
|
||||
fromArray[fromStart + 52], fromArray[fromStart + 53],
|
||||
fromArray[fromStart + 54], fromArray[fromStart + 55],
|
||||
fromArray[fromStart + 56], fromArray[fromStart + 57],
|
||||
fromArray[fromStart + 58], fromArray[fromStart + 59],
|
||||
fromArray[fromStart + 60], fromArray[fromStart + 61],
|
||||
fromArray[fromStart + 62], fromArray[fromStart + 63]
|
||||
);
|
||||
}
|
||||
for ( ; fromStart < end; ++fromStart) {
|
||||
toArray.push(fromArray[fromStart]);
|
||||
}
|
||||
},
|
||||
|
||||
appendSlice64: function(toArray, fromArray, fromStart, count) {
|
||||
var end = fromStart + count;
|
||||
for ( ; fromStart < end; fromStart += 64) {
|
||||
Array.prototype.push.apply(toArray, fromArray.slice(fromStart, Math.min(fromStart + 64, end)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
var helpers1 = {
|
||||
copyForward: copyForward.copyForward1,
|
||||
copyBackward: copyBackward.copyBackward1,
|
||||
append: append.append1,
|
||||
};
|
||||
|
||||
var helpers8 = {
|
||||
copyForward: copyForward.copyForward8,
|
||||
copyBackward: copyBackward.copyBackward8,
|
||||
append: append.append8,
|
||||
};
|
||||
|
||||
var helpers64 = {
|
||||
copyForward: copyForward.copyForward64,
|
||||
copyBackward: copyBackward.copyBackward64,
|
||||
append: append.append64,
|
||||
};
|
||||
|
||||
var allArraySpliceFuncs = {
|
||||
spliceApplyConcat: spliceApplyConcat,
|
||||
spliceApplyUnshift: spliceApplyUnshift,
|
||||
nonSpliceUsingSlice: nonSpliceUsingSlice,
|
||||
|
||||
spliceGutil: gutil.arraySplice,
|
||||
spliceManualWithTailCopy: spliceManualWithTailCopy,
|
||||
|
||||
spliceCopyWithTail1: spliceCopyWithTail(helpers1),
|
||||
spliceCopyWithTail8: spliceCopyWithTail(helpers8),
|
||||
spliceCopyWithTail64: spliceCopyWithTail(helpers64),
|
||||
|
||||
spliceFwdBackCopy1: spliceFwdBackCopy(helpers1),
|
||||
spliceFwdBackCopy8: spliceFwdBackCopy(helpers8),
|
||||
spliceFwdBackCopy64: spliceFwdBackCopy(helpers64),
|
||||
|
||||
spliceAppendCopy1: spliceAppendCopy(helpers1),
|
||||
spliceAppendCopy8: spliceAppendCopy(helpers8),
|
||||
spliceAppendCopy64: spliceAppendCopy(helpers64),
|
||||
|
||||
spliceAppendOnly1: spliceAppendOnly(helpers1),
|
||||
spliceAppendOnly8: spliceAppendOnly(helpers8),
|
||||
spliceAppendOnly64: spliceAppendOnly(helpers64),
|
||||
};
|
||||
|
||||
var timedArraySpliceFuncs = {
|
||||
// The following two naive implementations cannot cope with large arrays, and raise
|
||||
// "RangeError: Maximum call stack size exceeded".
|
||||
|
||||
//spliceApplyConcat: spliceApplyConcat,
|
||||
//spliceApplyUnshift: spliceApplyUnshift,
|
||||
|
||||
// This isn't a real splice, it doesn't modify the array.
|
||||
//nonSpliceUsingSlice: nonSpliceUsingSlice,
|
||||
|
||||
// The implementations commented out below are the slower ones.
|
||||
spliceGutil: gutil.arraySplice,
|
||||
spliceManualWithTailCopy: spliceManualWithTailCopy,
|
||||
|
||||
spliceCopyWithTail1: spliceCopyWithTail(helpers1),
|
||||
//spliceCopyWithTail8: spliceCopyWithTail(helpers8),
|
||||
//spliceCopyWithTail64: spliceCopyWithTail(helpers64),
|
||||
|
||||
//spliceFwdBackCopy1: spliceFwdBackCopy(helpers1),
|
||||
//spliceFwdBackCopy8: spliceFwdBackCopy(helpers8),
|
||||
//spliceFwdBackCopy64: spliceFwdBackCopy(helpers64),
|
||||
|
||||
spliceAppendCopy1: spliceAppendCopy(helpers1),
|
||||
spliceAppendCopy8: spliceAppendCopy(helpers8),
|
||||
spliceAppendCopy64: spliceAppendCopy(helpers64),
|
||||
|
||||
//spliceAppendOnly1: spliceAppendOnly(helpers1),
|
||||
//spliceAppendOnly8: spliceAppendOnly(helpers8),
|
||||
//spliceAppendOnly64: spliceAppendOnly(helpers64),
|
||||
};
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
describe("array copy functions", function() {
|
||||
it("copyForward should copy correctly", function() {
|
||||
_.each(copyForward, function(copyFunc, name) {
|
||||
var data = _.range(10000);
|
||||
copyFunc(data, 0, data, 1, 9999);
|
||||
copyFunc(data, 0, data, 1, 9999);
|
||||
assert.equal(data[0], 2);
|
||||
assert.equal(data[1], 3);
|
||||
assert.equal(data[9996], 9998);
|
||||
assert.equal(data[9997], 9999);
|
||||
assert.equal(data[9998], 9999);
|
||||
assert.equal(data[9999], 9999);
|
||||
});
|
||||
});
|
||||
|
||||
it("copyBackward should copy correctly", function() {
|
||||
_.each(copyBackward, function(copyFunc, name) {
|
||||
var data = _.range(10000);
|
||||
copyFunc(data, 1, data, 0, 9999);
|
||||
copyFunc(data, 1, data, 0, 9999);
|
||||
assert.equal(data[0], 0);
|
||||
assert.equal(data[1], 0);
|
||||
assert.equal(data[2], 0);
|
||||
assert.equal(data[3], 1);
|
||||
assert.equal(data[9998], 9996);
|
||||
assert.equal(data[9999], 9997);
|
||||
});
|
||||
});
|
||||
|
||||
it("arrayAppend should append correctly", function() {
|
||||
_.each(append, function(appendFunc, name) {
|
||||
var out = [];
|
||||
var data = _.range(20000);
|
||||
appendFunc(out, data, 100, 1);
|
||||
appendFunc(out, data, 100, 1000);
|
||||
appendFunc(out, data, 100, 10000);
|
||||
assert.deepEqual(out.slice(0, 4), [100, 100, 101, 102]);
|
||||
assert.deepEqual(out.slice(1000, 1004), [1099, 100, 101, 102]);
|
||||
assert.deepEqual(out.slice(11000), [10099]);
|
||||
});
|
||||
});
|
||||
|
||||
// See ENABLE_TIMING_TESTS flag on top of this file.
|
||||
if (ENABLE_TIMING_TESTS) {
|
||||
describe("timing", function() {
|
||||
var a1m = _.range(1000000);
|
||||
describe("copyForward", function() {
|
||||
var reps = 40;
|
||||
_.each(copyForward, function(copyFunc, name) {
|
||||
var b1m = a1m.slice(0);
|
||||
it(name, function() {
|
||||
utils.repeat(reps, copyFunc, b1m, 0, b1m, 1, 999999);
|
||||
|
||||
// Make sure it actually worked. These checks shouldn't affect timings much.
|
||||
assert.deepEqual(b1m.slice(0, 10), _.range(reps, reps + 10));
|
||||
assert.equal(b1m[999999-reps-1], 999998);
|
||||
assert.equal(b1m[999999-reps], 999999);
|
||||
assert.deepEqual(b1m.slice(1000000-reps), _.times(reps, _.constant(999999)));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("copyBackward", function() {
|
||||
var reps = 40;
|
||||
_.each(copyBackward, function(copyFunc, name) {
|
||||
var b1m = a1m.slice(0);
|
||||
it(name, function() {
|
||||
utils.repeat(reps, copyFunc, b1m, 1, b1m, 0, 999999);
|
||||
|
||||
// Make sure it actually worked. These checks shouldn't affect timings much.
|
||||
assert.deepEqual(b1m.slice(0, reps), _.times(reps, _.constant(0)));
|
||||
assert.equal(b1m[reps], 0);
|
||||
assert.equal(b1m[reps + 1], 1);
|
||||
assert.deepEqual(b1m.slice(999990), _.range(999990-reps, 1000000-reps));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("append", function() {
|
||||
var data = _.range(1000000);
|
||||
function chunkedAppend(appendFunc, data, chunk) {
|
||||
var out = [];
|
||||
var count = data.length / chunk;
|
||||
for (var i = 0; i < count; i++) {
|
||||
appendFunc(out, data, i * chunk, chunk);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
_.each(append, function(appendFunc, name) {
|
||||
it(name, function() {
|
||||
var out1 = chunkedAppend(appendFunc, data, 1);
|
||||
var out2 = chunkedAppend(appendFunc, data, 1000);
|
||||
var out3 = chunkedAppend(appendFunc, data, 1000000);
|
||||
|
||||
// Make sure it actually worked. Keep the checks short to avoid affecting timings.
|
||||
assert.deepEqual(out1.slice(0, 10), data.slice(0, 10));
|
||||
assert.deepEqual(out1.slice(data.length - 10), data.slice(data.length - 10));
|
||||
assert.deepEqual(out2.slice(0, 10), data.slice(0, 10));
|
||||
assert.deepEqual(out2.slice(data.length - 10), data.slice(data.length - 10));
|
||||
assert.deepEqual(out3.slice(0, 10), data.slice(0, 10));
|
||||
assert.deepEqual(out3.slice(data.length - 10), data.slice(data.length - 10));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('arraySplice', function() {
|
||||
|
||||
// Make sure all our functions produce the same results as spliceApplyConcat for simple cases.
|
||||
var refSpliceFunc = spliceApplyConcat;
|
||||
|
||||
it("all candidate functions should be correct for simpler cases", function() {
|
||||
_.each(allArraySpliceFuncs, function(spliceFunc, name) {
|
||||
var a10 = _.range(10), a100 = _.range(100);
|
||||
function checkSpliceFunc(target, start, arrToInsert) {
|
||||
assert.deepEqual(spliceFunc(target.slice(0), start, arrToInsert),
|
||||
refSpliceFunc(target.slice(0), start, arrToInsert),
|
||||
"splice function incorrect for " + name);
|
||||
}
|
||||
|
||||
checkSpliceFunc(a10, 5, a100);
|
||||
checkSpliceFunc(a100, 50, a10);
|
||||
checkSpliceFunc(a100, 90, a10);
|
||||
checkSpliceFunc(a100, 0, a10);
|
||||
checkSpliceFunc(a100, 100, a10);
|
||||
checkSpliceFunc(a10, 0, a100);
|
||||
checkSpliceFunc(a10, 10, a100);
|
||||
checkSpliceFunc(a10, 1, a10);
|
||||
checkSpliceFunc(a10, 5, a10);
|
||||
checkSpliceFunc(a10, 5, []);
|
||||
assert.deepEqual(spliceFunc(a10.slice(0), 5, a10),
|
||||
[0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 5, 6, 7, 8, 9]);
|
||||
});
|
||||
});
|
||||
|
||||
// See ENABLE_TIMING_TESTS flag on top of this file.
|
||||
if (ENABLE_TIMING_TESTS) {
|
||||
describe("timing", function() {
|
||||
var a1 = _.range(1);
|
||||
var a1k = _.range(1000);
|
||||
var a1m = _.range(1000000);
|
||||
|
||||
describe("insert-one", function() {
|
||||
_.each(timedArraySpliceFuncs, function(spliceFunc, name) {
|
||||
var b1m = a1m.slice(0);
|
||||
it(name, function() {
|
||||
utils.repeat(40, spliceFunc, b1m, 500000, a1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("insert-1k", function() {
|
||||
_.each(timedArraySpliceFuncs, function(spliceFunc, name) {
|
||||
var b1m = a1m.slice(0);
|
||||
it(name, function() {
|
||||
utils.repeat(40, spliceFunc, b1m, 500000, a1k);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("insert-1m", function() {
|
||||
_.each(timedArraySpliceFuncs, function(spliceFunc, name) {
|
||||
var b1m = a1m.slice(0);
|
||||
it(name, function() {
|
||||
utils.repeat(4, spliceFunc, b1m, 500000, a1m);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
49
test/common/csvFormat.ts
Normal file
49
test/common/csvFormat.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as csvFormat from 'app/common/csvFormat';
|
||||
import {assert} from 'chai';
|
||||
|
||||
describe('csvFormat', function() {
|
||||
it('should encode/decode csv values correctly', function() {
|
||||
function verify(plain: string, encoded: string) {
|
||||
assert.equal(csvFormat.csvEncodeCell(plain), encoded);
|
||||
assert.equal(csvFormat.csvDecodeCell(encoded), plain);
|
||||
}
|
||||
verify("hello world", "hello world");
|
||||
verify(`Commas,, galore, `, `"Commas,, galore, "`);
|
||||
verify(`"Quote" 'me,', ""please!""`, `"""Quote"" 'me,', """"please!"""""`);
|
||||
verify(` sing"le `, `" sing""le "`);
|
||||
verify(``, ``);
|
||||
verify(`""`, `""""""`);
|
||||
verify(`\t\n'\`\\`, `"\t\n'\`\\"`);
|
||||
// The exact interpretation of invalid encodings isn't too important, but should include most
|
||||
// of the value and not throw exceptions.
|
||||
assert.equal(csvFormat.csvDecodeCell(`invalid"e\ncoding `), `invalid"e\ncoding`);
|
||||
assert.equal(csvFormat.csvDecodeCell(`"invalid"e`), `invalid"e`);
|
||||
});
|
||||
|
||||
it('should encode/decode csv rows correctly', function() {
|
||||
function verify(plain: string[], encoded: string, prettier: boolean) {
|
||||
assert.equal(csvFormat.csvEncodeRow(plain, {prettier}), encoded);
|
||||
assert.deepEqual(csvFormat.csvDecodeRow(encoded), plain);
|
||||
}
|
||||
verify(["hello", "world"], "hello,world", false);
|
||||
verify(["hello", "world"], "hello, world", true);
|
||||
verify(["hello ", " world"], `"hello "," world"`, false);
|
||||
verify(["hello ", " world"], `"hello ", " world"`, true);
|
||||
verify([' '], `" "`, false);
|
||||
verify(['', ''], `,`, false);
|
||||
verify(['', ' ', ''], `, " ", `, true);
|
||||
verify([
|
||||
"Commas,, galore, ",
|
||||
`"Quote" 'me,', ""please!""`,
|
||||
` sing"le `,
|
||||
' ',
|
||||
'',
|
||||
], `"Commas,, galore, ","""Quote"" 'me,', """"please!"""""," sing""le "," ",`, false);
|
||||
verify(['Medium', 'Very high', `with, comma*=~!|more`, `asdf\nsdf`],
|
||||
`Medium, Very high, "with, comma*=~!|more", "asdf\nsdf"`, true);
|
||||
// The exact interpretation of invalid encodings isn't too important, but should include most
|
||||
// of the value and not throw exceptions.
|
||||
assert.deepEqual(csvFormat.csvDecodeRow(`invalid"e\ncoding,","`),
|
||||
['invalid"e\ncoding,', '']);
|
||||
});
|
||||
});
|
||||
15
test/common/getTableTitle.ts
Normal file
15
test/common/getTableTitle.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import {getTableTitle} from 'app/common/ActiveDocAPI';
|
||||
import {assert} from 'chai';
|
||||
|
||||
describe('getTableTitle', function() {
|
||||
it('should construct correct table titles', async function() {
|
||||
function check(groupByColLabels: string[] | null, expected: string) {
|
||||
assert.equal(getTableTitle({title: "My Table", groupByColLabels, colIds: []}), expected);
|
||||
}
|
||||
|
||||
check(null, "My Table");
|
||||
check([], "My Table [Totals]");
|
||||
check(["A"], "My Table [by A]");
|
||||
check(["A", "B"], "My Table [by A, B]");
|
||||
});
|
||||
});
|
||||
23
test/common/gristUrls.ts
Normal file
23
test/common/gristUrls.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {parseFirstUrlPart} from 'app/common/gristUrls';
|
||||
import {assert} from 'chai';
|
||||
|
||||
describe('gristUrls', function() {
|
||||
|
||||
describe('parseFirstUrlPart', function() {
|
||||
it('should strip out matching tag', function() {
|
||||
assert.deepEqual(parseFirstUrlPart('o', '/o/foo/bar?x#y'), {value: 'foo', path: '/bar?x#y'});
|
||||
assert.deepEqual(parseFirstUrlPart('o', '/o/foo?x#y'), {value: 'foo', path: '/?x#y'});
|
||||
assert.deepEqual(parseFirstUrlPart('o', '/o/foo#y'), {value: 'foo', path: '/#y'});
|
||||
assert.deepEqual(parseFirstUrlPart('o', '/o/foo'), {value: 'foo', path: '/'});
|
||||
});
|
||||
|
||||
it('should pass unchanged non-matching path or tag', function() {
|
||||
assert.deepEqual(parseFirstUrlPart('xxx', '/o/foo/bar?x#y'), {path: '/o/foo/bar?x#y'});
|
||||
assert.deepEqual(parseFirstUrlPart('o', '/O/foo/bar?x#y'), {path: '/O/foo/bar?x#y'});
|
||||
assert.deepEqual(parseFirstUrlPart('o', '/bar?x#y'), {path: '/bar?x#y'});
|
||||
assert.deepEqual(parseFirstUrlPart('o', '/o/?x#y'), {path: '/o/?x#y'});
|
||||
assert.deepEqual(parseFirstUrlPart('o', '/#y'), {path: '/#y'});
|
||||
assert.deepEqual(parseFirstUrlPart('o', ''), {path: ''});
|
||||
});
|
||||
});
|
||||
});
|
||||
264
test/common/gutil.js
Normal file
264
test/common/gutil.js
Normal file
@@ -0,0 +1,264 @@
|
||||
/* global describe, it */
|
||||
|
||||
var assert = require('chai').assert;
|
||||
var gutil = require('app/common/gutil');
|
||||
var _ = require('underscore');
|
||||
|
||||
describe('gutil', function() {
|
||||
|
||||
describe("mapToObject", function() {
|
||||
it("should produce an object with all keys", function() {
|
||||
assert.deepEqual(gutil.mapToObject(["foo", "bar", "baz"], function(value, i) {
|
||||
return [value.toUpperCase(), i];
|
||||
}), {
|
||||
"foo": ["FOO", 0],
|
||||
"bar": ["BAR", 1],
|
||||
"baz": ["BAZ", 2]
|
||||
});
|
||||
|
||||
assert.deepEqual(gutil.mapToObject(["foo", "bar", "baz"], function() {}), {
|
||||
"foo": undefined,
|
||||
"bar": undefined,
|
||||
"baz": undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("should work on an empty array", function() {
|
||||
var countCalls = 0;
|
||||
assert.deepEqual(gutil.mapToObject([], function() { countCalls++; }), {});
|
||||
assert.equal(countCalls, 0);
|
||||
});
|
||||
|
||||
it("should override values for duplicate keys", function() {
|
||||
assert.deepEqual(gutil.mapToObject(["foo", "bar", "foo"], function(val, i) { return i; }),
|
||||
{ "foo": 2, "bar": 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiCompareFunc', function() {
|
||||
var firstName = {
|
||||
0: 'John',
|
||||
1: 'John',
|
||||
2: 'John',
|
||||
3: 'John',
|
||||
4: 'Johnson',
|
||||
5: 'Johnson',
|
||||
};
|
||||
var lastName = {
|
||||
0: 'Smith',
|
||||
1: 'Smith',
|
||||
2: 'Smith',
|
||||
3: 'Smithy',
|
||||
4: 'Smithy',
|
||||
5: 'Smith',
|
||||
};
|
||||
var age = {
|
||||
0: 20,
|
||||
1: 30,
|
||||
2: 21,
|
||||
3: 31,
|
||||
4: 40,
|
||||
5: 50,
|
||||
};
|
||||
|
||||
it('should do single comparisons', function() {
|
||||
var sort1 = [_.propertyOf(firstName)];
|
||||
var compareA = gutil.multiCompareFunc(sort1, [gutil.nativeCompare], [1]);
|
||||
var compareD = gutil.multiCompareFunc(sort1, [gutil.nativeCompare], [-1]);
|
||||
assert.equal(compareA(0, 1), 0); // John == John
|
||||
assert.equal(compareD(0, 1), 0);
|
||||
assert.isBelow(compareA(0, 4), 0); // John < Johnson if ascending
|
||||
assert.isAbove(compareA(4, 0), 0);
|
||||
assert.isAbove(compareD(0, 4), 0); // John > Johnson if descending
|
||||
assert.isBelow(compareD(4, 0), 0);
|
||||
});
|
||||
|
||||
it('should do multiple comparisons', function() {
|
||||
var sort2 = [_.propertyOf(firstName), _.propertyOf(lastName)];
|
||||
var sort3 = [_.propertyOf(firstName), _.propertyOf(lastName), _.propertyOf(age)];
|
||||
var compare2 = gutil.multiCompareFunc(sort2, [gutil.nativeCompare, gutil.nativeCompare], [1, 1]);
|
||||
var compare3 = gutil.multiCompareFunc(sort3,
|
||||
[gutil.nativeCompare, gutil.nativeCompare, gutil.nativeCompare], [1, 1, -1]);
|
||||
|
||||
assert.equal(compare2(0, 1), 0); // John Smith, 20 = John Smith, 30
|
||||
assert.equal(compare2(1, 2), 0); // John Smith, 30 = John Smith, 21
|
||||
assert.isBelow(compare2(0, 3), 0); // John Smith < John Smithy
|
||||
assert.isBelow(compare2(0, 4), 0); // John Smith < Johnson Smithy
|
||||
assert.isBelow(compare2(0, 5), 0); // John Smith < Johnson Smith
|
||||
|
||||
assert.isAbove(compare3(0, 1), 0); // John Smith, 20 > John Smith, 30 (age descending)
|
||||
assert.isBelow(compare3(1, 2), 0); // John Smith, 30 < John Smith, 21
|
||||
assert.isBelow(compare3(0, 3), 0); // John Smith, 20 < John Smithy, 31
|
||||
assert.isBelow(compare3(0, 4), 0); // John Smith, 20 < Johnson Smithy, 40
|
||||
assert.isBelow(compare3(3, 4), 0); // John Smithy, 20 < Johnson Smithy, 40
|
||||
assert.isAbove(compare3(4, 5), 0); // Johnson Smithy > Johnson Smith
|
||||
});
|
||||
});
|
||||
|
||||
describe("deepExtend", function() {
|
||||
var sample = {
|
||||
a: 1,
|
||||
b: "hello",
|
||||
c: [1, 2, 3],
|
||||
d: { e: 1, f: 2 }
|
||||
};
|
||||
it("should copy recursively", function() {
|
||||
assert.deepEqual(gutil.deepExtend({}, {}), {});
|
||||
assert.deepEqual(gutil.deepExtend({}, sample), sample);
|
||||
assert.deepEqual(gutil.deepExtend({}, sample, {}), sample);
|
||||
assert.deepEqual(gutil.deepExtend({}, sample, sample), sample);
|
||||
assert.deepEqual(gutil.deepExtend({}, sample, {a: 2}).a, 2);
|
||||
assert.deepEqual(gutil.deepExtend({}, sample, {d: {g: 3}}).d, {e:1, f:2, g:3});
|
||||
assert.deepEqual(gutil.deepExtend({c: [4, 5, 6, 7], d: {g: 3}}, sample).d, {e:1, f:2, g:3});
|
||||
assert.deepEqual(gutil.deepExtend({c: [4, 5, 6, 7], d: {g: 3}}, sample).c, [1, 2, 3, 7]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("maxsplit", function() {
|
||||
it("should respect maxNumSplits parameter", function() {
|
||||
assert.deepEqual(gutil.maxsplit("foo bar baz", " ", 0), ["foo bar baz"]);
|
||||
assert.deepEqual(gutil.maxsplit("foo bar baz", " ", 1), ["foo", "bar baz"]);
|
||||
assert.deepEqual(gutil.maxsplit("foo bar baz", " ", 2), ["foo", "bar", "baz"]);
|
||||
assert.deepEqual(gutil.maxsplit("foo bar baz", " ", 3), ["foo", "bar", "baz"]);
|
||||
assert.deepEqual(gutil.maxsplit("foo<x>bar<x>baz", "<x>", 1), ["foo", "bar<x>baz"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("arrayInsertBefore", function() {
|
||||
it("should insert before the given nextValue", function() {
|
||||
var array = ["foo", "bar", "baz"];
|
||||
gutil.arrayInsertBefore(array, "asdf", "foo");
|
||||
assert.deepEqual(array, ["asdf", "foo", "bar", "baz"]);
|
||||
gutil.arrayInsertBefore(array, "hello", "baz");
|
||||
assert.deepEqual(array, ["asdf", "foo", "bar", "hello", "baz"]);
|
||||
gutil.arrayInsertBefore(array, "zoo", "unknown");
|
||||
assert.deepEqual(array, ["asdf", "foo", "bar", "hello", "baz", "zoo"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("popFromMap", function() {
|
||||
it("should return the value for the popped key", function() {
|
||||
var map = new Map([["foo", 1], ["bar", 2], ["baz", 3]]);
|
||||
assert.equal(gutil.popFromMap(map, "bar"), 2);
|
||||
assert.deepEqual(Array.from(map), [["foo", 1], ["baz", 3]]);
|
||||
assert.strictEqual(gutil.popFromMap(map, "unknown"), undefined);
|
||||
assert.deepEqual(Array.from(map), [["foo", 1], ["baz", 3]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isSubset", function() {
|
||||
it("should determine the subset relationship for Sets", function() {
|
||||
let sEmpty = new Set(),
|
||||
sFoo = new Set([1]),
|
||||
sBar = new Set([2, 3]),
|
||||
sBaz = new Set([1, 2, 3]);
|
||||
|
||||
assert.isTrue(gutil.isSubset(sEmpty, sFoo));
|
||||
assert.isFalse(gutil.isSubset(sFoo, sEmpty));
|
||||
|
||||
assert.isTrue(gutil.isSubset(sFoo, sBaz));
|
||||
assert.isFalse(gutil.isSubset(sFoo, sBar));
|
||||
|
||||
assert.isTrue(gutil.isSubset(sBar, sBaz));
|
||||
assert.isTrue(gutil.isSubset(sBar, sBar));
|
||||
|
||||
assert.isTrue(gutil.isSubset(sBaz, sBaz));
|
||||
assert.isFalse(gutil.isSubset(sBaz, sBar));
|
||||
});
|
||||
});
|
||||
|
||||
describe("growMatrix", function() {
|
||||
it("should grow the matrix to the desired size", function() {
|
||||
let matrix = [["a", 1], ["b", 2], ["c", 3]];
|
||||
assert.deepEqual(gutil.growMatrix(matrix, 4, 4),
|
||||
[["a", 1, "a", 1],
|
||||
["b", 2, "b", 2],
|
||||
["c", 3, "c", 3],
|
||||
["a", 1, "a", 1]]);
|
||||
assert.deepEqual(gutil.growMatrix(matrix, 3, 4),
|
||||
[["a", 1, "a", 1],
|
||||
["b", 2, "b", 2],
|
||||
["c", 3, "c", 3]]);
|
||||
assert.deepEqual(gutil.growMatrix(matrix, 6, 2),
|
||||
[["a", 1],
|
||||
["b", 2],
|
||||
["c", 3],
|
||||
["a", 1],
|
||||
["b", 2],
|
||||
["c", 3]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sortedScan", function() {
|
||||
it("should callback on the correct items for simple arrays", function() {
|
||||
const a = [1, 2, 4, 5, 7, 8, 9, 10, 11, 15, 17];
|
||||
const b = [2, 3, 4, 5, 9, 11, 19];
|
||||
|
||||
// Run the scan function, allowing it to populate callArgs.
|
||||
let callArgs = [];
|
||||
gutil.sortedScan(a, b, (ai, bi) => { callArgs.push([ai, bi]); });
|
||||
|
||||
assert.deepEqual(callArgs,
|
||||
[[1, null], [2, 2], [null, 3], [4, 4],
|
||||
[5, 5], [7, null], [8, null], [9, 9],
|
||||
[10, null], [11, 11], [15, null], [17, null],
|
||||
[null, 19]]);
|
||||
});
|
||||
|
||||
it("should callback on the correct items for object arrays", function() {
|
||||
const a = [{ id: 1, fruit: 'apple' },
|
||||
{ id: 2, fruit: 'banana' },
|
||||
{ id: 4, fruit: 'orange' },
|
||||
{ id: 5, fruit: 'peach' },
|
||||
{ id: 6, fruit: 'plum' }];
|
||||
const b = [{ id: 2, fruit: 'apple' },
|
||||
{ id: 3, fruit: 'avocado' },
|
||||
{ id: 4, fruit: 'peach' },
|
||||
{ id: 6, fruit: 'pear' },
|
||||
{ id: 9, fruit: 'plum' },
|
||||
{ id: 10, fruit: 'raspberry' }];
|
||||
|
||||
// Run the scan function.
|
||||
let fruitArgs = [];
|
||||
gutil.sortedScan(a, b, (ai, bi) => {
|
||||
fruitArgs.push([ai ? ai.fruit : '', bi ? bi.fruit : '']);
|
||||
}, item => item.id);
|
||||
|
||||
assert.deepEqual(fruitArgs,
|
||||
[['apple', ''], ['banana', 'apple'], ['', 'avocado'],
|
||||
['orange', 'peach'], ['peach', ''], ['plum', 'pear'],
|
||||
['', 'plum'], ['', 'raspberry']]);
|
||||
|
||||
// Run the scan function again, using fruit as the key.
|
||||
let idArgs = [];
|
||||
gutil.sortedScan(a, b, (ai, bi) => {
|
||||
idArgs.push([ai ? ai.id : 0, bi ? bi.id : 0]);
|
||||
}, item => item.fruit);
|
||||
|
||||
assert.deepEqual(idArgs,
|
||||
[[1, 2], [0, 3], [2, 0], [4, 0],
|
||||
[5, 4], [0, 6], [6, 9], [0, 10]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isEmail", function() {
|
||||
it("should distinguish valid and invalid emails", function() {
|
||||
// Reference: https://blogs.msdn.microsoft.com/testing123/2009/02/06/email-address-test-cases/
|
||||
assert.isTrue(gutil.isEmail('email@domain.com'));
|
||||
assert.isTrue(gutil.isEmail('e-mail_123@domain.com'));
|
||||
assert.isTrue(gutil.isEmail('email@subdomain.do-main.com'));
|
||||
assert.isTrue(gutil.isEmail('firstname+lastname@domain.com'));
|
||||
assert.isTrue(gutil.isEmail('email@domain.co.jp'));
|
||||
|
||||
assert.isFalse(gutil.isEmail('plainaddress'));
|
||||
assert.isFalse(gutil.isEmail('@domain.com'));
|
||||
assert.isFalse(gutil.isEmail('email@domain@domain.com'));
|
||||
assert.isFalse(gutil.isEmail('.email@domain.com'));
|
||||
assert.isFalse(gutil.isEmail('email.@domain.com'));
|
||||
assert.isFalse(gutil.isEmail('email..email@domain.com'));
|
||||
assert.isFalse(gutil.isEmail('あいうえお@domain.com'));
|
||||
assert.isFalse(gutil.isEmail('email@domain'));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
157
test/common/gutil2.ts
Normal file
157
test/common/gutil2.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import {delay} from 'app/common/delay';
|
||||
import * as gutil from 'app/common/gutil';
|
||||
import {assert} from 'chai';
|
||||
import {Observable} from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
describe('gutil2', function() {
|
||||
describe('waitObs', function() {
|
||||
it('should resolve promise when predicate matches', async function() {
|
||||
const obs: ko.Observable<number|null> = ko.observable<number|null>(null);
|
||||
const promise1 = gutil.waitObs(obs, (val) => Boolean(val));
|
||||
const promise2 = gutil.waitObs(obs, (val) => (val === null));
|
||||
const promise3 = gutil.waitObs(obs, (val) => (val! > 20));
|
||||
const spy1 = sinon.spy(), spy2 = sinon.spy(), spy3 = sinon.spy();
|
||||
const done = Promise.all([
|
||||
promise1.then((val) => { spy1(); assert.strictEqual(val, 17); }),
|
||||
promise2.then((val) => { spy2(); assert.strictEqual(val, null); }),
|
||||
promise3.then((val) => { spy3(); assert.strictEqual(val, 30); }),
|
||||
]);
|
||||
|
||||
await delay(1);
|
||||
obs(17);
|
||||
await delay(1);
|
||||
obs(30);
|
||||
await delay(1);
|
||||
|
||||
await done;
|
||||
sinon.assert.callOrder(spy2, spy1, spy3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('waitGrainObs', function() {
|
||||
it('should resolve promise when predicate matches', async function() {
|
||||
const obs = Observable.create<number|null>(null, null);
|
||||
const promise1 = gutil.waitGrainObs(obs, (val) => Boolean(val));
|
||||
const promise2 = gutil.waitGrainObs(obs, (val) => (val === null));
|
||||
const promise3 = gutil.waitGrainObs(obs, (val) => (val! > 20));
|
||||
const spy1 = sinon.spy(), spy2 = sinon.spy(), spy3 = sinon.spy();
|
||||
const done = Promise.all([
|
||||
promise1.then((val) => { spy1(); assert.strictEqual(val, 17); }),
|
||||
promise2.then((val) => { spy2(); assert.strictEqual(val, null); }),
|
||||
promise3.then((val) => { spy3(); assert.strictEqual(val, 30); }),
|
||||
]);
|
||||
|
||||
await delay(1);
|
||||
obs.set(17);
|
||||
await delay(1);
|
||||
obs.set(30);
|
||||
await delay(1);
|
||||
|
||||
await done;
|
||||
sinon.assert.callOrder(spy2, spy1, spy3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PromiseChain', function() {
|
||||
it('should resolve promises in order', async function() {
|
||||
const chain = new gutil.PromiseChain();
|
||||
|
||||
const spy1 = sinon.spy(), spy2 = sinon.spy(), spy3 = sinon.spy();
|
||||
const done = Promise.all([
|
||||
chain.add(() => delay(30).then(spy1).then(() => 1)),
|
||||
chain.add(() => delay(20).then(spy2).then(() => 2)),
|
||||
chain.add(() => delay(10).then(spy3).then(() => 3)),
|
||||
]);
|
||||
assert.deepEqual(await done, [1, 2, 3]);
|
||||
sinon.assert.callOrder(spy1, spy2, spy3);
|
||||
});
|
||||
|
||||
it('should skip pending callbacks, but not new callbacks, on error', async function() {
|
||||
const chain = new gutil.PromiseChain();
|
||||
|
||||
const spy1 = sinon.spy(), spy2 = sinon.spy(), spy3 = sinon.spy();
|
||||
let res1: any, res2: any, res3: any;
|
||||
await assert.isRejected(Promise.all([
|
||||
res1 = chain.add(() => delay(30).then(spy1).then(() => { throw new Error('Err1'); })),
|
||||
res2 = chain.add(() => delay(20).then(spy2)),
|
||||
res3 = chain.add(() => delay(10).then(spy3)),
|
||||
]), /Err1/);
|
||||
|
||||
// Check that already-scheduled callbacks did not get called.
|
||||
sinon.assert.calledOnce(spy1);
|
||||
sinon.assert.notCalled(spy2);
|
||||
sinon.assert.notCalled(spy3);
|
||||
spy1.resetHistory();
|
||||
|
||||
// Ensure skipped add() calls return a rejection.
|
||||
await assert.isRejected(res1, /^Err1/);
|
||||
await assert.isRejected(res2, /^Skipped due to an earlier error/);
|
||||
await assert.isRejected(res3, /^Skipped due to an earlier error/);
|
||||
|
||||
// New promises do get scheduled.
|
||||
await assert.isRejected(Promise.all([
|
||||
res1 = chain.add(() => delay(1).then(spy1).then(() => 17)),
|
||||
res2 = chain.add(() => delay(1).then(spy2).then(() => { throw new Error('Err2'); })),
|
||||
res3 = chain.add(() => delay(1).then(spy3)),
|
||||
]), /Err2/);
|
||||
sinon.assert.callOrder(spy1, spy2);
|
||||
sinon.assert.notCalled(spy3);
|
||||
|
||||
// Check the return values of add() calls.
|
||||
assert.strictEqual(await res1, 17);
|
||||
await assert.isRejected(res2, /^Err2/);
|
||||
await assert.isRejected(res3, /^Skipped due to an earlier error/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isLongerThan", function() {
|
||||
it('should work correctly', async function() {
|
||||
assert.equal(await gutil.isLongerThan(delay(200), 100), true);
|
||||
assert.equal(await gutil.isLongerThan(delay(10), 100), false);
|
||||
|
||||
// A promise that throws before the timeout, causes the returned promise to resolve to false.
|
||||
const errorObj = {};
|
||||
let promise = delay(10).then(() => { throw errorObj; });
|
||||
assert.equal(await gutil.isLongerThan(promise, 100), false);
|
||||
await assert.isRejected(promise);
|
||||
|
||||
// A promise that throws after the timeout, causes the returned promise to resolve to true.
|
||||
promise = delay(200).then(() => { throw errorObj; });
|
||||
assert.equal(await gutil.isLongerThan(promise, 100), true);
|
||||
await assert.isRejected(promise);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidHex", function() {
|
||||
it('should work correctly', async function() {
|
||||
assert.equal(gutil.isValidHex('#FF00FF'), true);
|
||||
assert.equal(gutil.isValidHex('#FF00FFF'), false);
|
||||
assert.equal(gutil.isValidHex('#FF0'), false);
|
||||
assert.equal(gutil.isValidHex('#FF00'), false);
|
||||
assert.equal(gutil.isValidHex('FF00FF'), false);
|
||||
assert.equal(gutil.isValidHex('#FF00FG'), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pruneArray", function() {
|
||||
function check<T>(arr: T[], indexes: number[], expect: T[]) {
|
||||
gutil.pruneArray(arr, indexes);
|
||||
assert.deepEqual(arr, expect);
|
||||
}
|
||||
it('should remove correct elements', function() {
|
||||
check(['a', 'b', 'c'], [], ['a', 'b', 'c']);
|
||||
check(['a', 'b', 'c'], [0], ['b', 'c']);
|
||||
check(['a', 'b', 'c'], [1], ['a', 'c']);
|
||||
check(['a', 'b', 'c'], [2], ['a', 'b']);
|
||||
check(['a', 'b', 'c'], [0, 1], ['c']);
|
||||
check(['a', 'b', 'c'], [0, 2], ['b']);
|
||||
check(['a', 'b', 'c'], [1, 2], ['a']);
|
||||
check(['a', 'b', 'c'], [0, 1, 2], []);
|
||||
check([], [], []);
|
||||
check(['a'], [], ['a']);
|
||||
check(['a'], [0], []);
|
||||
});
|
||||
});
|
||||
});
|
||||
169
test/common/marshal.js
Normal file
169
test/common/marshal.js
Normal file
@@ -0,0 +1,169 @@
|
||||
/* global describe, it */
|
||||
|
||||
var assert = require('chai').assert;
|
||||
var marshal = require('app/common/marshal');
|
||||
var MemBuffer = require('app/common/MemBuffer');
|
||||
|
||||
|
||||
describe("marshal", function() {
|
||||
function binStringToArray(binaryString) {
|
||||
var a = new Uint8Array(binaryString.length);
|
||||
for (var i = 0; i < binaryString.length; i++) {
|
||||
a[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
function arrayToBinString(array) {
|
||||
return String.fromCharCode.apply(String, array);
|
||||
}
|
||||
var samples = [
|
||||
[null, 'N'],
|
||||
[1, 'i\x01\x00\x00\x00'],
|
||||
[1000000, 'i@B\x0f\x00'],
|
||||
[-123456, 'i\xc0\x1d\xfe\xff'],
|
||||
[1.23, 'g\xae\x47\xe1\x7a\x14\xae\xf3\x3f', 2],
|
||||
[-625e-4, 'g\x00\x00\x00\x00\x00\x00\xb0\xbf', 2],
|
||||
[12.34, 'f\x0512.34', 0],
|
||||
[6.02e23, 'f\x086.02e+23', 0],
|
||||
[true, 'T'],
|
||||
[false, 'F'],
|
||||
[MemBuffer.stringToArray('Hello world'), 's\x0b\x00\x00\x00Hello world'],
|
||||
['Résumé', 's\x08\x00\x00\x00R\xc3\xa9sum\xc3\xa9'],
|
||||
[[1, 2, 3],
|
||||
'[\x03\x00\x00\x00i\x01\x00\x00\x00i\x02\x00\x00\x00i\x03\x00\x00\x00'],
|
||||
[{'This': 4, 'is': 0, 'a': MemBuffer.stringToArray('test')},
|
||||
'{s\x04\x00\x00\x00Thisi\x04\x00\x00\x00s\x01\x00\x00\x00as\x04\x00\x00\x00tests\x02\x00\x00\x00isi\x00\x00\x00\x000'],
|
||||
];
|
||||
|
||||
describe('basic data structures', function() {
|
||||
it("should serialize correctly", function() {
|
||||
var m0 = new marshal.Marshaller({ stringToBuffer: true, version: 0 });
|
||||
var m2 = new marshal.Marshaller({ stringToBuffer: true, version: 2 });
|
||||
for (var i = 0; i < samples.length; i++) {
|
||||
var value = samples[i][0];
|
||||
var expected = binStringToArray(samples[i][1]);
|
||||
var version = samples[i].length === 3 ? samples[i][2] : 0;
|
||||
var currentMarshaller = version >= 2 ? m2 : m0;
|
||||
currentMarshaller.marshal(value);
|
||||
var marshalled = currentMarshaller.dump();
|
||||
assert.deepEqual(marshalled, expected,
|
||||
"Wrong serialization of " + JSON.stringify(value) +
|
||||
"\n actual: " + escape(arrayToBinString(marshalled)) + "\n" +
|
||||
"\n expected: " + escape(arrayToBinString(expected)));
|
||||
}
|
||||
});
|
||||
|
||||
it("should deserialize correctly", function() {
|
||||
var m = new marshal.Unmarshaller();
|
||||
var values = [];
|
||||
m.on('value', function(val) { values.push(val); });
|
||||
|
||||
for (var i = 0; i < samples.length; i++) {
|
||||
values.length = 0;
|
||||
var expected = samples[i][0];
|
||||
m.push(binStringToArray(samples[i][1]));
|
||||
assert.strictEqual(values.length, 1);
|
||||
var value = values[0];
|
||||
if (typeof expected === 'string') {
|
||||
// This tests marshals JS strings to Python strings, but unmarshalls to Uint8Arrays. So
|
||||
// when the source is a string, we need to tweak the returned value for comparison.
|
||||
value = MemBuffer.arrayToString(value);
|
||||
}
|
||||
assert.deepEqual(value, expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("should support stringToBuffer and bufferToString", function() {
|
||||
var mY = new marshal.Marshaller({ stringToBuffer: true });
|
||||
var mN = new marshal.Marshaller({ stringToBuffer: false });
|
||||
var uY = new marshal.Unmarshaller({ bufferToString: true });
|
||||
var uN = new marshal.Unmarshaller({ bufferToString: false });
|
||||
var helloBuf = MemBuffer.stringToArray("hello");
|
||||
function passThrough(m, u, value) {
|
||||
var ret = null;
|
||||
u.on('value', function(v) { ret = v; });
|
||||
m.marshal(value);
|
||||
u.push(m.dump());
|
||||
return ret;
|
||||
}
|
||||
// No conversion, no change.
|
||||
assert.deepEqual(passThrough(mN, uN, "hello"), "hello");
|
||||
assert.deepEqual(passThrough(mN, uN, helloBuf), helloBuf);
|
||||
|
||||
// If convert to strings on the way back, then see all strings.
|
||||
assert.deepEqual(passThrough(mN, uY, "hello"), "hello");
|
||||
assert.deepEqual(passThrough(mN, uY, helloBuf), "hello");
|
||||
|
||||
// If convert to buffers on the way forward, and no conversion back, then see all buffers.
|
||||
assert.deepEqual(passThrough(mY, uN, "hello"), helloBuf);
|
||||
assert.deepEqual(passThrough(mY, uN, helloBuf), helloBuf);
|
||||
|
||||
// If convert to buffers on the way forward, and to strings back, then see all strings.
|
||||
assert.deepEqual(passThrough(mY, uY, "hello"), "hello");
|
||||
assert.deepEqual(passThrough(mY, uY, helloBuf), "hello");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
function mkbuf(arg) { return new Uint8Array(arg); }
|
||||
|
||||
function dumps(codeStr, value) {
|
||||
var m = new marshal.Marshaller();
|
||||
m.marshal(marshal.wrap(codeStr, value));
|
||||
return m.dump();
|
||||
}
|
||||
|
||||
describe('int64', function() {
|
||||
it("should serialize 32-bit values correctly", function() {
|
||||
assert.deepEqual(dumps('INT64', 0x7FFFFFFF), mkbuf([73, 255, 255, 255, 127, 0, 0, 0, 0]));
|
||||
assert.deepEqual(dumps('INT64', -0x80000000), mkbuf([73, 0, 0, 0, 128, 255, 255, 255, 255]));
|
||||
|
||||
// TODO: larger values fail now, but of course it's better to fix, and change this test.
|
||||
assert.throws(function() { dumps('INT64', 0x7FFFFFFF+1); }, /int64/);
|
||||
assert.throws(function() { dumps('INT64', -0x80000000-1); }, /int64/);
|
||||
});
|
||||
|
||||
it("should deserialize 32-bit values correctly", function() {
|
||||
assert.strictEqual(marshal.loads([73, 255, 255, 255, 127, 0, 0, 0, 0]), 0x7FFFFFFF);
|
||||
assert.strictEqual(marshal.loads([73, 0, 0, 0, 128, 255, 255, 255, 255]), -0x80000000);
|
||||
|
||||
// Can be verified in Python with: marshal.loads("".join(chr(r) for r in [73, 255, ...]))
|
||||
assert.strictEqual(marshal.loads([73, 255, 255, 255, 127, 255, 255, 255, 255]), -0x80000001);
|
||||
assert.strictEqual(marshal.loads([73, 0, 0, 0, 128, 0, 0, 0, 0]), 0x80000000);
|
||||
|
||||
// Be sure to test with low and high 32-bit words being positive or negative. Note that
|
||||
// integers that are too large to be safely represented are currently returned as strings.
|
||||
assert.strictEqual(marshal.loads([73, 1, 2, 3, 190, 4, 5, 6, 200]), '-4033530898337824255');
|
||||
assert.strictEqual(marshal.loads([73, 1, 2, 3, 190, 4, 5, 6, 20]), '1442846248544698881');
|
||||
assert.strictEqual(marshal.loads([73, 1, 2, 3, 90, 4, 5, 6, 200]), '-4033530900015545855');
|
||||
assert.strictEqual(marshal.loads([73, 1, 2, 3, 90, 4, 5, 6, 20]), '1442846246866977281');
|
||||
});
|
||||
});
|
||||
|
||||
describe('interned strings', function() {
|
||||
it("should parse interned strings correctly", function() {
|
||||
var testData = '{t\x03\x00\x00\x00aaat\x03\x00\x00\x00bbbR\x01\x00\x00\x00R\x00\x00\x00\x000';
|
||||
assert.deepEqual(marshal.loads(binStringToArray(testData)),
|
||||
{ 'aaa': MemBuffer.stringToArray('bbb'),
|
||||
'bbb': MemBuffer.stringToArray('aaa')
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('longs', function() {
|
||||
// This is generated as [991**i for i in xrange(10)] + [-678**i for i in xrange(10)].
|
||||
// Note how overly large values currently get stringified.
|
||||
const sampleData = [1, 991, 982081, 973242271, 964483090561, 955802742745951,
|
||||
'947200518061237441', '938675713398686304031', '930227631978098127294721',
|
||||
'921855583290295244149068511',
|
||||
-1, -678, -459684, -311665752, -211309379856, -143267759542368, '-97135540969725504',
|
||||
'-65857896777473891712', '-44651654015127298580736', '-30273821422256308437739008'];
|
||||
|
||||
const serialized = "[\x14\x00\x00\x00i\x01\x00\x00\x00i\xdf\x03\x00\x00iA\xfc\x0e\x00i\x9f\x7f\x02:I\x81\x08\xac\x8f\xe0\x00\x00\x00I_\xeb\xf4*Le\x03\x00I\xc1$\x1bJ\xda!%\rl\x05\x00\x00\x00\x1fG&>\x130\xf0\x15.\x03l\x06\x00\x00\x00\x01Q@\x17n\x1b\x84m\xbbO\x18\x00l\x06\x00\x00\x00\xdf\x123\x03\x86/\xd0r4(Q_i\xff\xff\xff\xffiZ\xfd\xff\xffi\\\xfc\xf8\xffi\xa8[l\xedI\xf0\xbe\xfa\xcc\xce\xff\xff\xffI\xa0\xaf\x15\xe0\xb2}\xff\xffI\xc0!oy\xbd\xe7\xa6\xfel\xfb\xff\xff\xff\x80\x1dYG\xc1\x00\xb2\x0f9\x00l\xfa\xff\xff\xff\x00!Rv\x9f\x00p\x11I\x17\x01\x00l\xfa\xff\xff\xff\x00f\xda]\x8c'\xa3.\xb2+!\x03";
|
||||
|
||||
it("should deserialize arbitrarily long integers correctly", function() {
|
||||
assert.deepEqual(marshal.loads(binStringToArray(serialized)), sampleData);
|
||||
});
|
||||
});
|
||||
});
|
||||
448
test/common/parseDate.ts
Normal file
448
test/common/parseDate.ts
Normal file
@@ -0,0 +1,448 @@
|
||||
/* global describe, it */
|
||||
import {guessDateFormat, guessDateFormats, parseDate, parseDateStrict, parseDateTime} from 'app/common/parseDate';
|
||||
import {assert} from 'chai';
|
||||
import * as moment from 'moment-timezone';
|
||||
|
||||
const today = new Date();
|
||||
const year = today.getUTCFullYear();
|
||||
const month = String(today.getUTCMonth() + 1).padStart(2, '0');
|
||||
|
||||
/**
|
||||
* Assert that parseDate and parseDateStrict parse `input` correctly,
|
||||
* returning a date that looks like expectedDateStr in ISO format.
|
||||
* parseDate should always produce a parsed date from `input`.
|
||||
* parseDateStrict should return at most one date, i.e. the formats it tries shouldn't allow ambiguity.
|
||||
*
|
||||
* fallback=true indicates the date cannot be parsed strictly with the given format
|
||||
* so parseDate has to fallback to another format and parseDateStrict gives no results.
|
||||
*
|
||||
* Otherwise, parseDateStrict should return a result
|
||||
* unless no dateFormat is given in which case it may or may not.
|
||||
*/
|
||||
function testParse(dateFormat: string|null, input: string, expectedDateStr: string, fallback: boolean = false) {
|
||||
assertDateEqual(parseDate(input, dateFormat ? {dateFormat} : {}), expectedDateStr);
|
||||
|
||||
const strict = new Set<number>();
|
||||
parseDateStrict(input, dateFormat, strict);
|
||||
assert.include([0, 1], strict.size);
|
||||
|
||||
// fallback=true indicates the date cannot be parsed strictly with the given format
|
||||
// so it has to fallback to another format.
|
||||
if (fallback) {
|
||||
assert.isEmpty(strict);
|
||||
} else if (dateFormat) {
|
||||
assert.equal(strict.size, 1);
|
||||
}
|
||||
|
||||
if (strict.size) {
|
||||
const strictParsed = [...strict][0];
|
||||
assertDateEqual(strictParsed, expectedDateStr);
|
||||
assertDateEqual(parseDateTime(input, dateFormat ? {dateFormat} : {})!, expectedDateStr);
|
||||
}
|
||||
}
|
||||
|
||||
function assertDateEqual(parsed: number|null, expectedDateStr: string) {
|
||||
const formatted = parsed === null ? null : new Date(parsed * 1000).toISOString().slice(0, 10);
|
||||
assert.equal(formatted, expectedDateStr);
|
||||
}
|
||||
|
||||
function testTimeParse(input: string, expectedUTCTimeStr: string | null, timezone?: string) {
|
||||
const parsed1 = parseDateTime('1993-04-02T' + input,
|
||||
{timeFormat: 'Z', timezone, dateFormat: 'YYYY-MM-DD'}) || null;
|
||||
const parsed2 = parseDate('1993-04-02', {time: input, timeFormat: 'UNUSED', timezone});
|
||||
for (const parsed of [parsed1, parsed2]) {
|
||||
if (expectedUTCTimeStr === null) {
|
||||
assert.isNull(parsed);
|
||||
return;
|
||||
}
|
||||
const output = new Date(parsed! * 1000).toISOString().slice(11, 19);
|
||||
assert.equal(output, expectedUTCTimeStr, `testTimeParse(${input}, ${timezone})`);
|
||||
}
|
||||
}
|
||||
|
||||
function testDateTimeParse(
|
||||
date: string, time: string, expectedUTCTimeStr: string | null, timezone: string, dateFormat?: string
|
||||
) {
|
||||
const parsed1 = parseDateTime(date + ' ' + time,
|
||||
{timeFormat: 'Z', timezone, dateFormat: dateFormat || 'YYYY-MM-DD'}) || null;
|
||||
|
||||
// This is for testing the combination of date and time which is important when daylight savings is involved
|
||||
const parsed2 = parseDate(date, {time, timeFormat: 'UNUSED', timezone, dateFormat});
|
||||
|
||||
for (const parsed of [parsed1, parsed2]) {
|
||||
if (expectedUTCTimeStr === null) {
|
||||
assert.isNull(parsed);
|
||||
return;
|
||||
}
|
||||
const output = new Date(parsed! * 1000).toISOString().slice(0, 19).replace("T", " ");
|
||||
assert.equal(output, expectedUTCTimeStr);
|
||||
}
|
||||
}
|
||||
|
||||
function testDateTimeStringParse(
|
||||
dateTime: string, expectedUTCTimeStr: string | null, dateFormat: string, timezone?: string,
|
||||
) {
|
||||
const parsed = parseDateTime(dateTime, {timezone, dateFormat});
|
||||
|
||||
if (expectedUTCTimeStr === null) {
|
||||
assert.isUndefined(parsed);
|
||||
return;
|
||||
}
|
||||
const output = new Date(parsed! * 1000).toISOString().slice(0, 19).replace("T", " ");
|
||||
assert.equal(output, expectedUTCTimeStr);
|
||||
}
|
||||
|
||||
describe('parseDate', function() {
|
||||
this.timeout(5000);
|
||||
|
||||
it('should allow parsing common date formats', function() {
|
||||
testParse(null, 'November 18th, 1994', '1994-11-18');
|
||||
testParse(null, 'nov 18 1994', '1994-11-18');
|
||||
testParse(null, '11-18-94', '1994-11-18');
|
||||
testParse(null, '11-18-1994', '1994-11-18');
|
||||
testParse(null, '1994-11-18', '1994-11-18');
|
||||
testParse(null, 'November 18, 1994', '1994-11-18');
|
||||
testParse('DD/MM/YY', '18/11/94', '1994-11-18');
|
||||
// fallback format is used because 18 is not a valid month
|
||||
testParse('MM/DD/YY', '18/11/94', '1994-11-18', true);
|
||||
|
||||
testParse(null, '18/11/94', '1994-11-18');
|
||||
testParse(null, '12/11/94', '1994-12-11');
|
||||
testParse('DD/MM/YY', '12/11/94', '1994-11-12');
|
||||
testParse('MM/DD/YY', '11/12/94', '1994-11-12');
|
||||
|
||||
testParse(null, '25', `${year}-${month}-25`);
|
||||
testParse(null, '10', `${year}-${month}-10`);
|
||||
testParse('DD/MM/YY', '10', `${year}-${month}-10`);
|
||||
testParse('DD/MM/YY', '3/4', `${year}-04-03`);
|
||||
// Separators in the format should not affect the parsing (for better or worse).
|
||||
testParse('YY-DD/MM', '3/4', `${year}-04-03`);
|
||||
testParse('YY/DD-MM', '3/4', `${year}-04-03`);
|
||||
testParse('MM/DD/YY', '3/4', `${year}-03-04`);
|
||||
testParse('YY/MM/DD', '3/4', `${year}-03-04`);
|
||||
testParse(null, '3/4', `${year}-03-04`);
|
||||
|
||||
// Single number gets parse according to the most specific item in the format string.
|
||||
testParse('DD', '10', `${year}-${month}-10`);
|
||||
testParse('DD/MM', '10', `${year}-${month}-10`);
|
||||
testParse('MM', '10', `${year}-10-01`);
|
||||
testParse('MM/YY', '10', `${year}-10-01`);
|
||||
testParse('MMM', '10', `${year}-10-01`);
|
||||
testParse('YY', '10', `2010-01-01`);
|
||||
testParse('YYYY', '10', `2010-01-01`);
|
||||
|
||||
testParse('YY', '05', `2005-01-01`);
|
||||
testParse('YY', '5', `${year}-05-01`, true); // Not a valid year, so falls back to "M" format
|
||||
testParse('YYYY', '1910', `1910-01-01`);
|
||||
testParse('YY', '3/4', `${year}-03-04`, true); // Falls back to another format
|
||||
testParse('DD/MM', '3/4', `${year}-04-03`);
|
||||
testParse('MM/YY', '3/04', `2004-03-01`);
|
||||
testParse('MM/YY', '3/4', `${year}-03-04`, true); // Not a valid year, so falls back to "M/D" format
|
||||
|
||||
testParse(null, '4/2/93', '1993-04-02');
|
||||
testParse(null, '04-02-1993', '1993-04-02');
|
||||
testParse(null, '4-02-93', '1993-04-02');
|
||||
testParse(null, 'April 2nd, 1993', '1993-04-02');
|
||||
|
||||
testParse('DD MMM YY', '15-Jan 99', '1999-01-15');
|
||||
testParse('DD MMM YYYY', '15-Jan 1999', '1999-01-15');
|
||||
testParse('DD MMM', '15-Jan 1999', '1999-01-15');
|
||||
|
||||
testParse('MMMM Do, YYYY', 'April 2nd, 1993', '1993-04-02');
|
||||
testParse('MMM Do YYYY', 'Apr 2nd 1993', `1993-04-02`);
|
||||
testParse('Do MMMM YYYY', '2nd April 1993', `1993-04-02`);
|
||||
testParse('Do MMM YYYY', '2nd Apr 1993', `1993-04-02`);
|
||||
testParse('MMMM D, YYYY', 'April 2, 1993', '1993-04-02');
|
||||
testParse('MMM D YYYY', 'Apr 2 1993', `1993-04-02`);
|
||||
testParse('D MMMM YYYY', '2 April 1993', `1993-04-02`);
|
||||
testParse('D MMM YYYY', '2 Apr 1993', `1993-04-02`);
|
||||
testParse('MMMM Do, ', 'April 2nd, 1993', '1993-04-02');
|
||||
testParse('MMM Do ', 'Apr 2nd 1993', `1993-04-02`);
|
||||
testParse('Do MMMM ', '2nd April 1993', `1993-04-02`);
|
||||
testParse('Do MMM ', '2nd Apr 1993', `1993-04-02`);
|
||||
testParse('MMMM D, ', 'April 2, 1993', '1993-04-02');
|
||||
testParse('MMM D ', 'Apr 2 1993', `1993-04-02`);
|
||||
testParse('D MMMM ', '2 April 1993', `1993-04-02`);
|
||||
testParse('D MMM ', '2 Apr 1993', `1993-04-02`);
|
||||
testParse('MMMM Do, ', 'April 2nd', `${year}-04-02`);
|
||||
testParse('MMM Do ', 'Apr 2nd', `${year}-04-02`);
|
||||
testParse('Do MMMM ', '2nd April', `${year}-04-02`);
|
||||
testParse('Do MMM ', '2nd Apr', `${year}-04-02`);
|
||||
testParse('MMMM D, ', 'April 2', `${year}-04-02`);
|
||||
testParse('MMM D ', 'Apr 2', `${year}-04-02`);
|
||||
testParse('D MMMM ', '2 April', `${year}-04-02`);
|
||||
testParse('D MMM ', '2 Apr', `${year}-04-02`);
|
||||
|
||||
// Test the combination of Do and YY, which was buggy at one point.
|
||||
testParse('MMMM Do, YY', 'April 2nd, 93', '1993-04-02');
|
||||
testParse('MMM Do, YY', 'Apr 2nd, 93', '1993-04-02');
|
||||
testParse('Do MMMM YY', '2nd April 93', `1993-04-02`);
|
||||
testParse('Do MMM YY', '2nd Apr 93', `1993-04-02`);
|
||||
|
||||
testParse(' D MMM ', ' 2 Apr ', `${year}-04-02`);
|
||||
testParse('D MMM', ' 2 Apr ', `${year}-04-02`);
|
||||
testParse(' D MMM ', '2 Apr', `${year}-04-02`);
|
||||
|
||||
testParse(null, ' 11-18-94 ', '1994-11-18');
|
||||
testParse(' DD MM YY', '18/11/94', '1994-11-18');
|
||||
});
|
||||
|
||||
it('should allow parsing common date-time formats', function() {
|
||||
// These are the test cases from before.
|
||||
testTimeParse('22:18:04', '22:18:04');
|
||||
testTimeParse('8pm', '20:00:00');
|
||||
testTimeParse('22:18:04', '22:18:04', 'UTC');
|
||||
testTimeParse('22:18:04', '03:18:04', 'America/New_York');
|
||||
testTimeParse('22:18:04', '06:18:04', 'America/Los_Angeles');
|
||||
testTimeParse('22:18:04', '13:18:04', 'Japan');
|
||||
|
||||
// Weird time formats are no longer parsed
|
||||
// testTimeParse('HH-mm', '1-15', '01:15:00');
|
||||
// testTimeParse('ss mm HH', '4 23 3', '03:23:04');
|
||||
|
||||
// The current behavior parses any standard-like format (with HH:MM:SS components in the usual
|
||||
// order) regardless of the format requested.
|
||||
|
||||
// Test a few variations of spelling AM/PM.
|
||||
for (const [am, pm] of [['A', ' p'], [' am', 'pM'], ['AM', ' PM']]) {
|
||||
testTimeParse('1', '01:00:00');
|
||||
testTimeParse('1' + am, '01:00:00');
|
||||
testTimeParse('1' + pm, '13:00:00');
|
||||
testTimeParse('22', '22:00:00');
|
||||
testTimeParse('22' + am, '22:00:00'); // Best guess for 22am/22pm is 22:00.
|
||||
testTimeParse('22' + pm, '22:00:00');
|
||||
testTimeParse('0', '00:00:00');
|
||||
testTimeParse('0' + am, '00:00:00');
|
||||
testTimeParse('0' + pm, '00:00:00');
|
||||
testTimeParse('12', '12:00:00'); // 12:00 is more likely 12pm than 12am
|
||||
testTimeParse('12' + am, '00:00:00');
|
||||
testTimeParse('12' + pm, '12:00:00');
|
||||
testTimeParse('9:8', '09:08:00');
|
||||
testTimeParse('9:8' + am, '09:08:00');
|
||||
testTimeParse('9:8' + pm, '21:08:00');
|
||||
testTimeParse('09:08', '09:08:00');
|
||||
testTimeParse('09:08' + am, '09:08:00');
|
||||
testTimeParse('09:08' + pm, '21:08:00');
|
||||
testTimeParse('21:59', '21:59:00');
|
||||
testTimeParse('21:59' + am, '21:59:00');
|
||||
testTimeParse('21:59' + pm, '21:59:00');
|
||||
testTimeParse('10:18:04', '10:18:04');
|
||||
testTimeParse('10:18:04' + am, '10:18:04');
|
||||
testTimeParse('10:18:04' + pm, '22:18:04');
|
||||
testTimeParse('22:18:04', '22:18:04');
|
||||
testTimeParse('22:18:04' + am, '22:18:04');
|
||||
testTimeParse('22:18:04' + pm, '22:18:04');
|
||||
testTimeParse('12:18:04', '12:18:04');
|
||||
testTimeParse('12:18:04' + am, '00:18:04');
|
||||
testTimeParse('12:18:04' + pm, '12:18:04');
|
||||
testTimeParse('908', '09:08:00');
|
||||
testTimeParse('0910', '09:10:00');
|
||||
testTimeParse('2112', '21:12:00');
|
||||
}
|
||||
|
||||
// Tests with time zones.
|
||||
testTimeParse('09:08', '09:08:00', 'UTC');
|
||||
testTimeParse('09:08', '14:08:00', 'America/New_York');
|
||||
testTimeParse('09:08', '00:08:00', 'Japan');
|
||||
testTimeParse('09:08 Z', '09:08:00');
|
||||
testTimeParse('09:08z', '09:08:00');
|
||||
testTimeParse('09:08 UT', '09:08:00');
|
||||
testTimeParse('09:08 UTC', '09:08:00');
|
||||
testTimeParse('09:08-05', '14:08:00');
|
||||
testTimeParse('09:08-5', '14:08:00');
|
||||
testTimeParse('09:08-0500', '14:08:00');
|
||||
testTimeParse('09:08-05:00', '14:08:00');
|
||||
testTimeParse('09:08-500', '14:08:00');
|
||||
testTimeParse('09:08-5:00', '14:08:00');
|
||||
testTimeParse('09:08+05', '04:08:00');
|
||||
testTimeParse('09:08+5', '04:08:00');
|
||||
testTimeParse('09:08+0500', '04:08:00');
|
||||
testTimeParse('09:08+5:00', '04:08:00');
|
||||
testTimeParse('09:08+05:00', '04:08:00');
|
||||
});
|
||||
|
||||
it('should handle timezone abbreviations', function() {
|
||||
// New York can be abbreviated as EDT or EST depending on the time of year for daylight savings.
|
||||
// We ignore the abbreviation so it's parsed the same whichever is used.
|
||||
// However the parsed UTC time depends on the date.
|
||||
testDateTimeParse('2020-02-02', '09:45 edt', '2020-02-02 14:45:00', 'America/New_York');
|
||||
testDateTimeParse('2020-10-10', '09:45 edt', '2020-10-10 13:45:00', 'America/New_York');
|
||||
testDateTimeParse('2020-02-02', '09:45 est', '2020-02-02 14:45:00', 'America/New_York');
|
||||
testDateTimeParse('2020-10-10', '09:45 est', '2020-10-10 13:45:00', 'America/New_York');
|
||||
// Spaces and case shouldn't matter.
|
||||
testDateTimeParse('2020-10-10', '09:45 EST', '2020-10-10 13:45:00', 'America/New_York');
|
||||
testDateTimeParse('2020-10-10', '09:45EST', '2020-10-10 13:45:00', 'America/New_York');
|
||||
testDateTimeParse('2020-10-10', '09:45EDT', '2020-10-10 13:45:00', 'America/New_York');
|
||||
|
||||
// Testing that AEDT is rejected in the New York timezone even though it ends with EDT which is valid.
|
||||
testTimeParse('09:45:00 aedt', null, 'America/New_York');
|
||||
testTimeParse('09:45:00AEDT', null, 'America/New_York');
|
||||
testTimeParse('09:45:00 aedt', '23:45:00', 'Australia/ACT');
|
||||
testTimeParse('09:45:00AEDT', '23:45:00', 'Australia/ACT');
|
||||
|
||||
// Testing multiple abbreviations of US/Pacific
|
||||
testDateTimeParse('2020-02-02', '09:45 PST', null, 'America/New_York');
|
||||
testDateTimeParse('2020-02-02', '09:45 PST', '2020-02-02 17:45:00', 'US/Pacific');
|
||||
testDateTimeParse('2020-10-10', '09:45 PST', '2020-10-10 16:45:00', 'US/Pacific');
|
||||
testDateTimeParse('2020-02-02', '09:45 PDT', '2020-02-02 17:45:00', 'US/Pacific');
|
||||
testDateTimeParse('2020-10-10', '09:45 PDT', '2020-10-10 16:45:00', 'US/Pacific');
|
||||
// PWT and PPT are some obscure abbreviations apparently used at some time and thus supported by moment
|
||||
testDateTimeParse('2020-10-10', '09:45 PWT', '2020-10-10 16:45:00', 'US/Pacific');
|
||||
testDateTimeParse('2020-10-10', '09:45 PPT', '2020-10-10 16:45:00', 'US/Pacific');
|
||||
// POT is not valid
|
||||
testDateTimeParse('2020-10-10', '09:45 POT', null, 'US/Pacific');
|
||||
|
||||
// Both these timezones have CST and CDT, but not COT.
|
||||
// The timezones are far apart so the parsed UTC times are too.
|
||||
testTimeParse('09:45 CST', '01:45:00', 'Asia/Shanghai');
|
||||
testTimeParse('09:45 CDT', '01:45:00', 'Asia/Shanghai');
|
||||
testTimeParse('09:45 CST', '15:45:00', 'Canada/Central');
|
||||
testTimeParse('09:45 CDT', '15:45:00', 'Canada/Central');
|
||||
testTimeParse('09:45 COT', null, 'Asia/Shanghai');
|
||||
testTimeParse('09:45 COT', null, 'Canada/Central');
|
||||
});
|
||||
|
||||
it('should parse datetime strings', function() {
|
||||
for (const separator of [' ', 'T']) {
|
||||
for (let tz of ['Z', 'UTC', '+00:00', '-00', '']) {
|
||||
for (const tzSeparator of ['', ' ']) {
|
||||
tz = tzSeparator + tz;
|
||||
|
||||
let expected = '2020-03-04 12:34:56';
|
||||
testDateTimeStringParse(
|
||||
` 2020-03-04${separator}12:34:56${tz} `, expected, 'YYYY-MM-DD'
|
||||
);
|
||||
testDateTimeStringParse(
|
||||
` 03-04-2020${separator}12:34:56${tz} `, expected, 'MM/DD/YYYY'
|
||||
);
|
||||
testDateTimeStringParse(
|
||||
` 04-03-20${separator}12:34:56${tz} `, expected, 'DD-MM-YY'
|
||||
);
|
||||
testDateTimeStringParse(
|
||||
` 2020-03-04${separator}12:34:56${tz} `, expected, '',
|
||||
);
|
||||
expected = '2020-03-04 12:34:00';
|
||||
testDateTimeStringParse(
|
||||
` 04-03-20${separator}12:34${tz} `, expected, 'DD-MM-YY'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle datetimes as formatted by moment', function() {
|
||||
for (const date of ['2020-02-03', '2020-06-07', '2020-10-11']) { // different months for daylight savings
|
||||
const dateTime = date + ' 12:34:56';
|
||||
const utcMoment = moment.tz(dateTime, 'UTC');
|
||||
for (const dateFormat of ['DD/MM/YY', 'MM/DD/YY']) {
|
||||
for (const tzFormat of ['z', 'Z']) { // abbreviation (z) vs +/-HH:MM (Z)
|
||||
assert.isTrue(utcMoment.isValid());
|
||||
for (const tzName of moment.tz.names()) {
|
||||
const tzMoment = moment.tz(utcMoment, tzName);
|
||||
const formattedTime = tzMoment.format('HH:mm:ss ' + tzFormat);
|
||||
const formattedDate = tzMoment.format(dateFormat);
|
||||
testDateTimeParse(formattedDate, formattedTime, dateTime, tzName, dateFormat);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should be flexible in parsing the preferred format', function() {
|
||||
for (const format of ['DD-MM-YYYY', 'DD-MM-YY', 'DD-MMM-YYYY', 'DD-MMM-YY']) {
|
||||
testParse(format, '1/2/21', '2021-02-01');
|
||||
testParse(format, '01/02/2021', '2021-02-01');
|
||||
testParse(format, '1-02-21', '2021-02-01');
|
||||
}
|
||||
|
||||
for (const format of ['MM-DD-YYYY', 'MM-DD-YY', 'MMM-DD-YYYY', 'MMM-DD-YY']) {
|
||||
testParse(format, '1/2/21', '2021-01-02');
|
||||
testParse(format, '01/02/2021', '2021-01-02');
|
||||
testParse(format, '1-02-21', '2021-01-02');
|
||||
}
|
||||
|
||||
for (const format of ['YY-MM-DD', 'YYYY-MM-DD', 'YY-MMM-DD', 'YYYY-MMM-DD']) {
|
||||
testParse(format, '01/2/3', '2001-02-03');
|
||||
testParse(format, '2001/02/03', '2001-02-03');
|
||||
testParse(format, '01-02-03', '2001-02-03');
|
||||
testParse(format, '10/11', `${year}-10-11`);
|
||||
testParse(format, '2/3', `${year}-02-03`);
|
||||
testParse(format, '12', `${year}-${month}-12`);
|
||||
}
|
||||
|
||||
testParse('DD MMM YYYY', '1 FEB 2021', '2021-02-01');
|
||||
testParse('DD MMM YYYY', '1-feb-21', '2021-02-01');
|
||||
testParse('DD MMM YYYY', '1/2/21', '2021-02-01');
|
||||
testParse('DD MMM YYYY', '01/02/2021', '2021-02-01');
|
||||
testParse('DD MMM YYYY', '1-02-21', '2021-02-01');
|
||||
testParse('DD MMM YYYY', '1 2', `${year}-02-01`);
|
||||
testParse('DD MMM YYYY', '1 feb', `${year}-02-01`);
|
||||
|
||||
testParse('DD MMM', '1 FEB 2021', '2021-02-01');
|
||||
testParse('DD MMM', '1-feb-2021', '2021-02-01');
|
||||
testParse('DD MMM', '1/2/2021', '2021-02-01');
|
||||
testParse('DD MMM', '01/02/2021', '2021-02-01');
|
||||
testParse('DD MMM', '1-02-2021', '2021-02-01');
|
||||
testParse('DD MMM', '1 2 2021', `2021-02-01`);
|
||||
testParse('DD MMM', '1 feb 2021', `2021-02-01`);
|
||||
});
|
||||
|
||||
it('should support underscores as separators', async function() {
|
||||
testParse('DD_MM_YY', '3/4', `${year}-04-03`);
|
||||
testParse('DD_MM_YY', '3_4', `${year}-04-03`);
|
||||
testParse('DD_MM_YY', '3_4_98', `1998-04-03`);
|
||||
testParse('DD/MM/YY', '3_4_98', `1998-04-03`);
|
||||
});
|
||||
|
||||
it('should interpret two-digit years as bootstrap datepicker does', function() {
|
||||
const yy = year % 100;
|
||||
// These checks are expected to work as long as today's year is between 2021 and 2088.
|
||||
testParse('MM-DD-YY', `1/2/${yy}`, `20${yy}-01-02`);
|
||||
testParse('MM-DD-YY', `1/2/${yy + 9}`, `20${yy + 9}-01-02`);
|
||||
testParse('MM-DD-YY', `1/2/${yy + 11}`, `19${yy + 11}-01-02`);
|
||||
// These should work until 2045 (after that 55 would be interpreted as 2055).
|
||||
testParse('MM-DD-YY', `1/2/00`, `2000-01-02`);
|
||||
testParse('MM-DD-YY', `1/2/08`, `2008-01-02`);
|
||||
testParse('MM-DD-YY', `1/2/20`, `2020-01-02`);
|
||||
testParse('MM-DD-YY', `1/2/30`, `2030-01-02`);
|
||||
testParse('MM-DD-YY', `1/2/55`, `1955-01-02`);
|
||||
testParse('MM-DD-YY', `1/2/79`, `1979-01-02`);
|
||||
testParse('MM-DD-YY', `1/2/98`, `1998-01-02`);
|
||||
});
|
||||
|
||||
describe('guessDateFormat', function() {
|
||||
it('should guess date formats', function() {
|
||||
// guessDateFormats with an *s* shows all the equally likely guesses.
|
||||
// It's only directly used in tests, just to reveal the inner workings.
|
||||
// guessDateFormat picks one of those formats which is actually used in type conversion etc.
|
||||
|
||||
// ISO YYYY-MM-DD is king
|
||||
assert.deepEqual(guessDateFormats(["2020-01-02"]), ["YYYY-MM-DD"]);
|
||||
assert.deepEqual(guessDateFormat(["2020-01-02"]), "YYYY-MM-DD");
|
||||
|
||||
// Some ambiguous dates
|
||||
assert.deepEqual(guessDateFormats(["01/01/2020"]), ["DD/MM/YYYY", "MM/DD/YYYY"]);
|
||||
assert.deepEqual(guessDateFormats(["01/02/03"]), ['DD/MM/YY', 'MM/DD/YY', 'YY/MM/DD']);
|
||||
assert.deepEqual(guessDateFormats(["01-01-2020"]), ["DD-MM-YYYY", "MM-DD-YYYY"]);
|
||||
assert.deepEqual(guessDateFormats(["01-02-03"]), ['DD-MM-YY', 'MM-DD-YY', 'YY-MM-DD']);
|
||||
assert.deepEqual(guessDateFormat(["01/01/2020"]), "MM/DD/YYYY");
|
||||
assert.deepEqual(guessDateFormat(["01/02/03"]), 'YY/MM/DD');
|
||||
assert.deepEqual(guessDateFormat(["01-01-2020"]), "MM-DD-YYYY");
|
||||
assert.deepEqual(guessDateFormat(["01-02-03"]), 'YY-MM-DD');
|
||||
|
||||
// Ambiguous date with only two parts
|
||||
assert.deepEqual(guessDateFormats(["01/02"]), ["DD/MM", "MM/DD", "YY/MM"]);
|
||||
assert.deepEqual(guessDateFormat(["01/02"]), "YY/MM");
|
||||
|
||||
// First date is ambiguous, second date makes the guess unambiguous.
|
||||
assert.deepEqual(guessDateFormats(["01/01/2020", "20/01/2020"]), ["DD/MM/YYYY"]);
|
||||
assert.deepEqual(guessDateFormats(["01/01/2020", "01/20/2020"]), ["MM/DD/YYYY"]);
|
||||
assert.deepEqual(guessDateFormat(["01/01/2020", "20/01/2020"]), "DD/MM/YYYY");
|
||||
assert.deepEqual(guessDateFormat(["01/01/2020", "01/20/2020"]), "MM/DD/YYYY");
|
||||
|
||||
// Not a date at all, guess YYYY-MM-DD as the default.
|
||||
assert.deepEqual(guessDateFormats(["foo bar"]), null);
|
||||
assert.deepEqual(guessDateFormat(["foo bar"]), "YYYY-MM-DD");
|
||||
});
|
||||
});
|
||||
});
|
||||
220
test/common/promises.js
Normal file
220
test/common/promises.js
Normal file
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* Do some timing of promises, as well as of nextTick and setTimeout, so that we have an idea of
|
||||
* how long different things take.
|
||||
*
|
||||
* To see actual timings, comment out the console.log inside the `log` function below.
|
||||
*/
|
||||
|
||||
|
||||
/* global describe, it, before */
|
||||
|
||||
var assert = require('chai').assert;
|
||||
|
||||
var bluebird = require('bluebird');
|
||||
|
||||
// Disable longStackTraces, which seem to be enabled in the browser by default.
|
||||
bluebird.config({ longStackTraces: false });
|
||||
|
||||
function log(message) {
|
||||
//console.log(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Measurement helpers. Usage:
|
||||
* var start = startTimer();
|
||||
* ...
|
||||
* var usec = usecElapsed(start); // Returns microseconds.
|
||||
*/
|
||||
var startTimer, usecElapsed;
|
||||
if (typeof process !== 'undefined' && typeof process.hrtime !== 'undefined') {
|
||||
startTimer = function() {
|
||||
return process.hrtime();
|
||||
};
|
||||
usecElapsed = function(start) {
|
||||
var elapsed = process.hrtime(start);
|
||||
return elapsed[0] * 1000000 + elapsed[1] / 1000;
|
||||
};
|
||||
} else {
|
||||
startTimer = function() {
|
||||
return Date.now();
|
||||
};
|
||||
usecElapsed = function(start) {
|
||||
var elapsedMs = (Date.now() - start);
|
||||
return elapsedMs * 1000;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to run timing tests. Adds a test case to run the given function, possibly multiple
|
||||
* times, and check the timing value that it returns.
|
||||
*
|
||||
* Example:
|
||||
* describe("myClass", function() {
|
||||
* timeIt("myFunc", { reps: 3, expectedUs: 100, fudgeFactor: 4}, myFunc);
|
||||
* });
|
||||
* Produces:
|
||||
* myFunc should take ~100us (up to x4) [got 123us]: 316ms
|
||||
* Notes:
|
||||
* - The number at the end isn't very meaningful (includes repetitions and measurements).
|
||||
* - Fudge factors should be pretty large, since tests often take shorter or longer depending
|
||||
* on platform, system load, etc.
|
||||
*
|
||||
* @param {Number} options.reps - Run the test this many times and check the min value.
|
||||
* @param {Number} options.expectedUs - Expected number of microseconds to receive from func.
|
||||
* @param {Number} options.fudgeFactor - It's fine if the test takes this factor longer or shorter.
|
||||
* @param {Number} options.noLowerBound - don't test for being too fast.
|
||||
* @param {Function} func - Will call func(reportUs), where reportUs is a function that should be
|
||||
* called with the test measurement when func is done.
|
||||
* @return {Function} Function that takes a `done` callback and calls it when all is done.
|
||||
*/
|
||||
function timeIt(name, options, func) {
|
||||
var reps = options.reps || 1;
|
||||
var fudgeFactor = options.fudgeFactor || 1;
|
||||
var expectedUs = options.expectedUs;
|
||||
var noLowerBound = options.noLowerBound;
|
||||
var test = it(name + " should take ~" + expectedUs + "us (up to x" + fudgeFactor + ")",
|
||||
function(done) {
|
||||
var n = 0;
|
||||
var minTimeUs = Infinity;
|
||||
function iteration(timeUs) {
|
||||
try {
|
||||
minTimeUs = Math.min(minTimeUs, timeUs);
|
||||
if (n++ < reps) {
|
||||
func(next);
|
||||
return;
|
||||
}
|
||||
log("Ran test " + n + " times, min time " + minTimeUs);
|
||||
assert(minTimeUs <= expectedUs * fudgeFactor,
|
||||
"Time of " + minTimeUs + "us is longer than expected (" + expectedUs + ") " +
|
||||
"by more than fudge factor of " + fudgeFactor);
|
||||
if (!noLowerBound) {
|
||||
assert(minTimeUs >= expectedUs / fudgeFactor,
|
||||
"Time of " + minTimeUs + "us is shorter than expected (" + expectedUs + ") " +
|
||||
"by more than fudge factor of " + fudgeFactor);
|
||||
}
|
||||
tackOnMeasuredTime(test, minTimeUs);
|
||||
done();
|
||||
} catch (err) {
|
||||
tackOnMeasuredTime(test, minTimeUs);
|
||||
done(err);
|
||||
}
|
||||
}
|
||||
function next(timeUs) {
|
||||
setTimeout(iteration, 0, timeUs);
|
||||
}
|
||||
next(Infinity);
|
||||
});
|
||||
}
|
||||
|
||||
function tackOnMeasuredTime(test, timeUs) {
|
||||
// Output the measured time as 123.1, or 0.0005 when small
|
||||
var str = timeUs > 10 ? timeUs.toFixed(0) : timeUs.toPrecision(2);
|
||||
test.title = test.title.replace(/( \[got [^]]*us\])?$/, " [got " + str + "us]");
|
||||
}
|
||||
|
||||
describe("promises", function() {
|
||||
// These are normally skipped. They are not really tests of our code, but timings to help
|
||||
// understand how long different things take. Because of global state affecting tests (e.g.
|
||||
// longStackTraces setting, async_hooks affecting timings), it doesn't work well to run these as
|
||||
// part of the full test suite. Instead, they can be run manually using
|
||||
//
|
||||
// ENABLE_TIMING_TESTS=1 bin/mocha test/common/promises.ts
|
||||
//
|
||||
// (Note that things in mocha.opts, such as report-why-tests-hang, affect them and may need to
|
||||
// be commented out to see accurate timings.)
|
||||
//
|
||||
before(function() {
|
||||
if (!process.env.ENABLE_TIMING_TESTS) {
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
function test(arg) {
|
||||
return arg + 2;
|
||||
}
|
||||
|
||||
timeIt("simple calls", { reps: 3, expectedUs: 0.005, fudgeFactor: 10, noLowerBound: true },
|
||||
function(reportUs) {
|
||||
var iterations = 10000000;
|
||||
var start = startTimer();
|
||||
var value = 0;
|
||||
for (var i = 0; i < iterations; i++) {
|
||||
value = test(value);
|
||||
}
|
||||
var us = usecElapsed(start) / iterations;
|
||||
assert.equal(value, iterations * 2);
|
||||
log("Direct calls took " + us + " us / iteration");
|
||||
reportUs(us);
|
||||
});
|
||||
|
||||
function testPromiseLib(promiseLib, libName, setupFunc, timingOptions) {
|
||||
var iterations = timingOptions.iters;
|
||||
timeIt(libName + " chain", timingOptions, function(reportUs) {
|
||||
setupFunc();
|
||||
var start = startTimer();
|
||||
var chain = promiseLib.resolve(0);
|
||||
for (var i = 0; i < iterations; i++) {
|
||||
chain = chain.then(test);
|
||||
}
|
||||
var chainDone = false;
|
||||
chain.then(function(value) {
|
||||
var us = usecElapsed(start) / iterations;
|
||||
chainDone = true;
|
||||
assert.equal(value, iterations * 2);
|
||||
log(libName + " promise chain took " + us + " us / iteration");
|
||||
reportUs(us);
|
||||
});
|
||||
assert.equal(chainDone, false);
|
||||
});
|
||||
}
|
||||
|
||||
// Measure bluebird with and without longStackSupport. If switching promise libraries, we could
|
||||
// add similar timings here to compare performance. E.g. Q is nearly two orders of magnitude
|
||||
// slower than bluebird.
|
||||
var isNode = Boolean(process.version);
|
||||
|
||||
testPromiseLib(bluebird, 'bluebird (no long traces)',
|
||||
// Sadly, no way to turn off bluebird.longStackTraces, so just do this test first.
|
||||
function() {
|
||||
assert.isFalse(bluebird.hasLongStackTraces(), "longStackTraces should be off");
|
||||
},
|
||||
{ iters: 20000, reps: 3, expectedUs: isNode ? 0.3 : 1, fudgeFactor: 8});
|
||||
|
||||
// TODO: with bluebird 3, we can no longer switch between having and not having longStackTraces.
|
||||
// We'd have to measure it in two different test runs. For now, can run this test with
|
||||
// BLUEBIRD_DEBUG=1 environment variable.
|
||||
//testPromiseLib(bluebird, 'bluebird (with long traces)',
|
||||
// function() { bluebird.longStackTraces(); },
|
||||
// { iters: 20000, reps: 3, expectedUs: isNode ? 0.3 : 1, fudgeFactor: 8});
|
||||
|
||||
|
||||
function testRepeater(repeaterFunc, name, timingOptions) {
|
||||
var iterations = timingOptions.iters;
|
||||
timeIt("timing of " + name, timingOptions, function(reportUs) {
|
||||
var count = 0;
|
||||
function step() {
|
||||
if (count < iterations) {
|
||||
repeaterFunc(step);
|
||||
count++;
|
||||
} else {
|
||||
var us = usecElapsed(start) / iterations;
|
||||
assert.equal(count, iterations);
|
||||
log(name + " took " + us + " us / iteration (" + iterations + " iterations)");
|
||||
reportUs(us);
|
||||
}
|
||||
}
|
||||
var start = startTimer();
|
||||
step();
|
||||
});
|
||||
}
|
||||
|
||||
if (process.maxTickDepth) {
|
||||
// Probably running under Node
|
||||
testRepeater(process.nextTick, "process.nextTick",
|
||||
{ iters: process.maxTickDepth*9/10, reps: 20, expectedUs: 0.1, fudgeFactor: 4 });
|
||||
}
|
||||
if (typeof setImmediate !== 'undefined') {
|
||||
testRepeater(setImmediate, "setImmediate",
|
||||
{ iters: 100, reps: 10, expectedUs: 2.0, fudgeFactor: 4 });
|
||||
}
|
||||
});
|
||||
53
test/common/roles.ts
Normal file
53
test/common/roles.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as roles from 'app/common/roles';
|
||||
import {assert} from 'chai';
|
||||
|
||||
describe('roles', function() {
|
||||
describe('getStrongestRole', function() {
|
||||
it('should return the strongest role', function() {
|
||||
assert.equal(roles.getStrongestRole(roles.OWNER, roles.EDITOR), roles.OWNER);
|
||||
assert.equal(roles.getStrongestRole(roles.OWNER, roles.VIEWER, null), roles.OWNER);
|
||||
assert.equal(roles.getStrongestRole(roles.EDITOR, roles.VIEWER), roles.EDITOR);
|
||||
assert.equal(roles.getStrongestRole(roles.VIEWER), roles.VIEWER);
|
||||
assert.equal(roles.getStrongestRole(roles.VIEWER, roles.GUEST), roles.VIEWER);
|
||||
assert.equal(roles.getStrongestRole(roles.OWNER, roles.GUEST), roles.OWNER);
|
||||
assert.equal(roles.getStrongestRole(null, roles.GUEST), roles.GUEST);
|
||||
assert.equal(roles.getStrongestRole(null, roles.EDITOR), roles.EDITOR);
|
||||
assert.equal(roles.getStrongestRole(roles.EDITOR, roles.EDITOR, roles.EDITOR), roles.EDITOR);
|
||||
assert.equal(roles.getStrongestRole(roles.EDITOR, roles.OWNER, roles.EDITOR), roles.OWNER);
|
||||
assert.equal(roles.getStrongestRole(null, null, roles.EDITOR, roles.VIEWER, roles.EDITOR), roles.EDITOR);
|
||||
assert.equal(roles.getStrongestRole(null, null, null), null);
|
||||
|
||||
assert.throws(() => roles.getStrongestRole(undefined as any, roles.EDITOR), /Invalid role undefined/);
|
||||
assert.throws(() => roles.getStrongestRole(undefined as any, null), /Invalid role undefined/);
|
||||
assert.throws(() => roles.getStrongestRole(undefined as any, undefined), /Invalid role undefined/);
|
||||
assert.throws(() => roles.getStrongestRole('XXX' as any, roles.EDITOR), /Invalid role XXX/);
|
||||
assert.throws(() => roles.getStrongestRole('XXX' as any, null), /Invalid role XXX/);
|
||||
assert.throws(() => roles.getStrongestRole('XXX' as any, 'YYY'), /Invalid role XXX/);
|
||||
assert.throws(() => roles.getStrongestRole(), /No roles given/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWeakestRole', function() {
|
||||
it('should return the weakest role', function() {
|
||||
assert.equal(roles.getWeakestRole(roles.OWNER, roles.EDITOR), roles.EDITOR);
|
||||
assert.equal(roles.getWeakestRole(roles.OWNER, roles.VIEWER, null), null);
|
||||
assert.equal(roles.getWeakestRole(roles.EDITOR, roles.VIEWER), roles.VIEWER);
|
||||
assert.equal(roles.getWeakestRole(roles.VIEWER), roles.VIEWER);
|
||||
assert.equal(roles.getWeakestRole(roles.VIEWER, roles.GUEST), roles.GUEST);
|
||||
assert.equal(roles.getWeakestRole(roles.OWNER, roles.GUEST), roles.GUEST);
|
||||
assert.equal(roles.getWeakestRole(null, roles.EDITOR), null);
|
||||
assert.equal(roles.getWeakestRole(roles.EDITOR, roles.EDITOR, roles.EDITOR), roles.EDITOR);
|
||||
assert.equal(roles.getWeakestRole(roles.EDITOR, roles.OWNER, roles.EDITOR), roles.EDITOR);
|
||||
assert.equal(roles.getWeakestRole(null, null, roles.EDITOR, roles.VIEWER, roles.EDITOR), null);
|
||||
assert.equal(roles.getWeakestRole(roles.OWNER, roles.OWNER), roles.OWNER);
|
||||
|
||||
assert.throws(() => roles.getWeakestRole(undefined as any, roles.EDITOR), /Invalid role undefined/);
|
||||
assert.throws(() => roles.getWeakestRole(undefined as any, null), /Invalid role undefined/);
|
||||
assert.throws(() => roles.getWeakestRole(undefined as any, undefined), /Invalid role undefined/);
|
||||
assert.throws(() => roles.getWeakestRole('XXX' as any, roles.EDITOR), /Invalid role XXX/);
|
||||
assert.throws(() => roles.getWeakestRole('XXX' as any, null), /Invalid role XXX/);
|
||||
assert.throws(() => roles.getWeakestRole('XXX' as any, 'YYY'), /Invalid role XXX/);
|
||||
assert.throws(() => roles.getWeakestRole(), /No roles given/);
|
||||
});
|
||||
});
|
||||
});
|
||||
121
test/common/serializeTiming.js
Normal file
121
test/common/serializeTiming.js
Normal file
@@ -0,0 +1,121 @@
|
||||
/* global describe, it, before, after */
|
||||
|
||||
var _ = require('underscore');
|
||||
var assert = require('assert');
|
||||
var Chance = require('chance');
|
||||
var utils = require('../utils');
|
||||
var marshal = require('app/common/marshal');
|
||||
|
||||
/**
|
||||
* This test measures the complete encoding/decoding time of several ways to serialize an array of
|
||||
* data. This is intended both to choose a good serialization format, and to optimize its
|
||||
* implementation. This test is supposed to work both in Node and in browsers.
|
||||
*/
|
||||
describe("Serialization", function() {
|
||||
|
||||
function marshalV0(data) {
|
||||
var m = new marshal.Marshaller({stringToBuffer: true, version: 0});
|
||||
m.marshal(data);
|
||||
return m.dump();
|
||||
}
|
||||
|
||||
function marshalV2(data) {
|
||||
var m = new marshal.Marshaller({stringToBuffer: true, version: 2});
|
||||
m.marshal(data);
|
||||
return m.dump();
|
||||
}
|
||||
|
||||
function unmarshal(buffer) {
|
||||
var m = new marshal.Unmarshaller({bufferToString: true});
|
||||
var value;
|
||||
m.on('value', function(v) { value = v; });
|
||||
m.push(buffer);
|
||||
m.removeAllListeners();
|
||||
return value;
|
||||
}
|
||||
|
||||
var encoders = {
|
||||
"marshal_v0": {enc: marshalV0, dec: unmarshal},
|
||||
"marshal_v2": {enc: marshalV2, dec: unmarshal},
|
||||
"json": {enc: JSON.stringify, dec: JSON.parse},
|
||||
};
|
||||
|
||||
describe("correctness", function() {
|
||||
var data;
|
||||
before(function() {
|
||||
// Generate an array of random data using the Chance module
|
||||
var chance = new Chance(1274323391); // seed is arbitrary
|
||||
data = {
|
||||
'floats1k': chance.n(chance.floating, 1000),
|
||||
'strings1k': chance.n(chance.string, 1000),
|
||||
};
|
||||
});
|
||||
|
||||
_.each(encoders, function(encoder, name) {
|
||||
it(name, function() {
|
||||
assert.deepEqual(encoder.dec(encoder.enc(data.floats1k)), data.floats1k);
|
||||
assert.deepEqual(encoder.dec(encoder.enc(data.strings1k)), data.strings1k);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
utils.timing.describe("timings", function() {
|
||||
var data, encoded = {}, results = {};
|
||||
before(function() {
|
||||
this.timeout(10000);
|
||||
// Generate an array of random data using the Chance module
|
||||
var chance = new Chance(1274323391); // seed is arbitrary
|
||||
data = {
|
||||
'floats100k': chance.n(chance.floating, 100000),
|
||||
'strings100k': chance.n(chance.string, 100000),
|
||||
};
|
||||
// And prepare an encoded version for each encoder so that we can time decoding.
|
||||
_.each(data, function(values, key) {
|
||||
_.each(encoders, function(encoder, name) {
|
||||
encoded[key + ":" + name] = encoder.enc(values);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function test_encode(name, key, expectedMs) {
|
||||
utils.timing.it(expectedMs, "encodes " + key + " with " + name, function() {
|
||||
utils.repeat(5, encoders[name].enc, data[key]);
|
||||
});
|
||||
}
|
||||
|
||||
function test_decode(name, key, expectedMs) {
|
||||
utils.timing.it(expectedMs, "decodes " + key + " with " + name, function() {
|
||||
var ret = utils.repeat(5, encoders[name].dec, encoded[key + ":" + name]);
|
||||
results[key + ":" + name] = ret;
|
||||
});
|
||||
}
|
||||
|
||||
after(function() {
|
||||
// Verify the results of decoding tests outside the timed test case.
|
||||
_.each(results, function(result, keyName) {
|
||||
var key = keyName.split(":")[0];
|
||||
assert.deepEqual(result, data[key], "wrong result decoding " + keyName);
|
||||
});
|
||||
});
|
||||
|
||||
// Note that these tests take quite a bit longer when running ALL tests than when running them
|
||||
// separately, so the expected times are artificially inflated below to let them pass. This
|
||||
// may be because memory allocation is slower due to memory fragmentation. Just running gc()
|
||||
// before the tests doesn't remove the discrepancy.
|
||||
// Also note that the expected time needs to be high enough for both node and browser.
|
||||
test_encode('marshal_v0', 'floats100k', 1600);
|
||||
test_decode('marshal_v0', 'floats100k', 600);
|
||||
test_encode('marshal_v0', 'strings100k', 1000);
|
||||
test_decode('marshal_v0', 'strings100k', 800);
|
||||
|
||||
test_encode('marshal_v2', 'floats100k', 160);
|
||||
test_decode('marshal_v2', 'floats100k', 160);
|
||||
test_encode('marshal_v2', 'strings100k', 1000);
|
||||
test_decode('marshal_v2', 'strings100k', 800);
|
||||
|
||||
test_encode('json', 'floats100k', 120);
|
||||
test_decode('json', 'floats100k', 120);
|
||||
test_encode('json', 'strings100k', 80);
|
||||
test_decode('json', 'strings100k', 80);
|
||||
});
|
||||
});
|
||||
168
test/common/sortTiming.js
Normal file
168
test/common/sortTiming.js
Normal file
@@ -0,0 +1,168 @@
|
||||
/* global describe, it */
|
||||
|
||||
var assert = require('assert');
|
||||
var gutil = require('app/common/gutil');
|
||||
var _ = require('underscore');
|
||||
var utils = require('../utils');
|
||||
|
||||
// Uncomment to see logs
|
||||
function log(messages) {
|
||||
//console.log.apply(console, messages);
|
||||
}
|
||||
/**
|
||||
* Compares performance of underscore.sortedIndex and gutil.sortedIndex on ranges of the
|
||||
* given array.
|
||||
* @param {array} arr - array to call sortedIndex on
|
||||
* @param {function} keyFunc - a sort key function used to sort the array
|
||||
* @param {function} cmp - a compare function used to sort the array
|
||||
* @param {object} object - object of settings for utils.time
|
||||
* @param {string} msg - helpful message to display with time results
|
||||
**/
|
||||
function benchmarkSortedIndex(arr, keyFunc, cmp, options, msg) {
|
||||
var t1, t2;
|
||||
var currArray = [], currSearchElems = [];
|
||||
var sortedArr = _.sortBy(arr, keyFunc);
|
||||
var compareFunc = gutil.multiCompareFunc([keyFunc], [cmp], [true]);
|
||||
|
||||
function testUnderscore(arr, searchElems) {
|
||||
searchElems.forEach(function(i) { _.sortedIndex(arr, i, keyFunc); });
|
||||
}
|
||||
function testGutil(arr, searchElems) {
|
||||
searchElems.forEach(function(i) { gutil.sortedIndex(arr, i, compareFunc); });
|
||||
}
|
||||
|
||||
// TODO: Write a library function that does this for loop stuff b/c its largely the same
|
||||
// across the 3 benchmark functions. This is kind of messy to abstract b/c of issues
|
||||
// with array sorting side effects and function context.
|
||||
for(var p = 1; 2 * currArray.length <= arr.length; p++) {
|
||||
log(['==========================================================']);
|
||||
currArray = sortedArr.slice(0, Math.pow(2, p));
|
||||
currSearchElems = arr.slice(0, Math.pow(2, p));
|
||||
log(['Calling sortedIndex', currArray.length, 'times averaged over', options.iters,
|
||||
'iterations |', msg]);
|
||||
t1 = utils.time(testUnderscore, null, [currArray, currSearchElems], options);
|
||||
t2 = utils.time(testGutil, null, [currArray, currSearchElems], options);
|
||||
log(["Underscore.sortedIndex:", t1, 'ms.', 'Avg time per call:', t1/currArray.length]);
|
||||
log(["gutil.sortedIndex :", t2, 'ms.', 'Avg time per call:', t2/currArray.length]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares performance of sorting using 1-key, 2-key, ... (keys.length)-key comparison
|
||||
* functions on ranges of the given array.
|
||||
* @param {array} arr - array to sort
|
||||
* @param {function array} keys - array of sort key functions
|
||||
* @param {function array} cmps - array of compare functions parallel to keys
|
||||
* @param {boolean array} asc - array of booleans denoting asc/descending. This is largely
|
||||
irrelevant to performance
|
||||
* @param {object} object - object of settings for utils.time
|
||||
* @param {string} msg - helpful message to display with time results
|
||||
**/
|
||||
function benchmarkMultiCompareSort(arr, keys, cmps, asc, options, msg) {
|
||||
var elapsed;
|
||||
var compareFuncs = [], currArray = [];
|
||||
for(var l = 0; l < keys.length; l++) {
|
||||
compareFuncs.push(gutil.multiCompareFunc(keys.slice(0, l+1), cmps.slice(0, l+1), asc.slice(0, l+1)));
|
||||
}
|
||||
|
||||
for(var p = 1; 2 * currArray.length <= arr.length; p++) {
|
||||
currArray = arr.slice(0, Math.pow(2, p));
|
||||
log(['==========================================================']);
|
||||
log(['Sorting', currArray.length, 'elements averaged over', options.iters,
|
||||
'iterations |', msg]);
|
||||
for(var i = 0; i < compareFuncs.length; i++) {
|
||||
elapsed = utils.time(Array.prototype.sort, currArray, [compareFuncs[i]], options);
|
||||
log([(i+1) + "-key compare sort took: ", elapsed, 'ms']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares performance of Array.sort, Array.sort with a gutilMultiCompareFunc(on 1-key), and
|
||||
* Underscore's sort function on ranges of the given array.
|
||||
* @param {array} arr - array to sort
|
||||
* @param {function} compareKey - compare function to use for sorting
|
||||
* @param {function} keyFunc - key function used to construct a compare function for sorting with
|
||||
Array.sort
|
||||
* @param {object} object - object of settings for utils.time
|
||||
* @param {string} msg - helpful message to display with time results
|
||||
**/
|
||||
function benchmarkNormalSort(arr, compareFunc, keyFunc, options, msg) {
|
||||
var t1, t2, t3;
|
||||
var currArray = [];
|
||||
var gutilCompare = gutil.multiCompareFunc([keyFunc], [compareFunc], [true]);
|
||||
|
||||
for (var p = 1; 2 * currArray.length <= arr.length; p++) {
|
||||
log(['==========================================================']);
|
||||
currArray = arr.slice(0, Math.pow(2, p));
|
||||
log(['Sorting', currArray.length, 'elements averaged over', options.iters,
|
||||
'iterations |', msg]);
|
||||
t1 = utils.time(Array.prototype.sort, currArray, [compareFunc], options);
|
||||
t2 = utils.time(Array.prototype.sort, currArray, [gutilCompare], options);
|
||||
t3 = utils.time(_.sortBy, null, [currArray, keyFunc], options);
|
||||
log(['Array.sort with compare func :', t1]);
|
||||
log(['Array.sort with constructed multicompare func:', t2]);
|
||||
log(['Underscore sort :', t3]);
|
||||
}
|
||||
}
|
||||
|
||||
describe('Performance tests', function() {
|
||||
var maxPower = 10; // tweak as needed
|
||||
var options = {'iters': 10, 'avg': true};
|
||||
var timeout = 5000000; // arbitrary
|
||||
var length = Math.pow(2, maxPower);
|
||||
|
||||
// sample data to do our sorting on. generating these random lists can take a while...
|
||||
var nums = utils.genItems('floating', length, {min:0, max:length});
|
||||
var people = utils.genPeople(length);
|
||||
var strings = utils.genItems('string', length, {length:10});
|
||||
|
||||
describe('Benchmark test for gutil.sortedIndex', function() {
|
||||
it('should be close to underscore.sortedIndex\'s performance', function() {
|
||||
this.timeout(timeout);
|
||||
benchmarkSortedIndex(nums, _.identity, gutil.nativeCompare, options,
|
||||
'Sorted index benchmark on numbers');
|
||||
benchmarkSortedIndex(strings, _.identity, gutil.nativeCompare, options,
|
||||
'Sorted index benchmark on strings');
|
||||
assert(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Benchmarks for various sorting', function() {
|
||||
var peopleKeys = [_.property('last'), _.property('first'), _.property('age'),
|
||||
_.property('year'), _.property('month'), _.property('day')];
|
||||
var cmp1 = [gutil.nativeCompare, gutil.nativeCompare, gutil.nativeCompare, gutil.nativeCompare,
|
||||
gutil.nativeCompare, gutil.nativeCompare];
|
||||
var stringKeys = [_.identity, function (x) { return x.length; },
|
||||
function (x) { return x[0]; } ];
|
||||
var cmp2 = [gutil.nativeCompare, gutil.nativeCompare, gutil.nativeCompare];
|
||||
var numKeys = [_.identity, utils.mod(2), utils.mod(3), utils.mod(5)];
|
||||
var cmp3 = numKeys.map(function() { return gutil.nativeCompare; });
|
||||
var asc = [1, 1, -1, 1, 1]; // bools for ascending/descending in multicompare
|
||||
|
||||
it('should be close to _.sortBy with only 1 compare key', function() {
|
||||
this.timeout(timeout);
|
||||
benchmarkNormalSort(strings, gutil.nativeCompare, _.identity, options,
|
||||
'Regular sort test on string array');
|
||||
benchmarkNormalSort(people, function(a, b) { return a.age - b.age; }, _.property('age'),
|
||||
options, 'Regular sort test on people array using age as sort key');
|
||||
benchmarkNormalSort(nums, gutil.nativeCompare, _.identity, options,
|
||||
'Regular sort test on number array');
|
||||
assert(true);
|
||||
});
|
||||
|
||||
it('should have consistent performance when no tie breakers are needed', function() {
|
||||
this.timeout(timeout);
|
||||
benchmarkMultiCompareSort(strings, stringKeys, cmp2, asc, options, 'Consistency test on string array');
|
||||
benchmarkMultiCompareSort(nums, numKeys, cmp3, asc, options, 'Consistency test on number array');
|
||||
assert(true);
|
||||
});
|
||||
|
||||
it('should scale linearly in the number of compare keys used', function() {
|
||||
this.timeout(timeout);
|
||||
benchmarkMultiCompareSort(people, peopleKeys, cmp1, asc, options, 'Linear scaling test on people array');
|
||||
assert(true);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
23
test/common/timeFormat.js
Normal file
23
test/common/timeFormat.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/* global describe, it */
|
||||
|
||||
var assert = require('assert');
|
||||
var {timeFormat} = require('app/common/timeFormat');
|
||||
|
||||
describe('timeFormat', function() {
|
||||
|
||||
var date = new Date(2014, 3, 4, 22, 28, 16, 123);
|
||||
|
||||
it("should format date", function() {
|
||||
assert.equal(timeFormat("Y", date), "20140404");
|
||||
assert.equal(timeFormat("D", date), "2014-04-04");
|
||||
});
|
||||
|
||||
it("should format time", function() {
|
||||
assert.equal(timeFormat("T", date), "22:28:16");
|
||||
assert.equal(timeFormat("T + M", date), "22:28:16 + 123");
|
||||
});
|
||||
|
||||
it("should format date and time", function() {
|
||||
assert.equal(timeFormat("A", date), "2014-04-04 22:28:16.123");
|
||||
});
|
||||
});
|
||||
97
test/common/tsvFormat.ts
Normal file
97
test/common/tsvFormat.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import {assert} from 'chai';
|
||||
|
||||
import {tsvDecode, tsvEncode} from 'app/common/tsvFormat';
|
||||
|
||||
const sampleData = [
|
||||
['plain value', 'plain value'],
|
||||
['quotes "inside" hello', 'quotes "inside" hello'],
|
||||
['"half" quotes', '"half" quotes'],
|
||||
['half "quotes"', 'half "quotes"'],
|
||||
['"full quotes"', '"full quotes"'],
|
||||
['"extra" "quotes"', '"extra" "quotes"'],
|
||||
['"has" ""double"" quotes"', '"has" ""double"" quotes"'],
|
||||
['"more ""double""', '"more ""double""'],
|
||||
['tab\tinside', 'tab\tinside'],
|
||||
['\ttab first', '\ttab first'],
|
||||
['tab last\t', 'tab last\t'],
|
||||
[' space first', ' space first'],
|
||||
['space last ', 'space last '],
|
||||
['\nnewline first', '\nnewline first'],
|
||||
['newline last\n', 'newline last\n'],
|
||||
['newline\ninside', 'newline\ninside'],
|
||||
['"tab\tinside quotes outside"', '"tab\tinside quotes outside"'],
|
||||
['"tab"\tbetween "quoted"', '"tab"\tbetween "quoted"'],
|
||||
['"newline\ninside quotes outside"', '"newline\ninside quotes outside"'],
|
||||
['"newline"\nbetween "quoted"', '"newline"\nbetween "quoted"'],
|
||||
['"', '"'],
|
||||
['""', '""'],
|
||||
// A few special characters on their own that should work correctly.
|
||||
['', ' ', '\t', '\n', "'", "\\"],
|
||||
// Some non-string values
|
||||
[0, 1, false, true, undefined, null, Number.NaN],
|
||||
];
|
||||
|
||||
// This is the encoding produced by Excel (latest version on Mac as of March 2017).
|
||||
const sampleEncoded = `plain value\tplain value
|
||||
quotes "inside" hello\tquotes "inside" hello
|
||||
"half" quotes\t"half" quotes
|
||||
half "quotes"\thalf "quotes"
|
||||
"full quotes"\t"full quotes"
|
||||
"extra" "quotes"\t"extra" "quotes"
|
||||
"has" ""double"" quotes"\t"has" ""double"" quotes"
|
||||
"more ""double""\t"more ""double""
|
||||
"tab\tinside"\t"tab\tinside"
|
||||
"\ttab first"\t"\ttab first"
|
||||
"tab last\t"\t"tab last\t"
|
||||
space first\t space first
|
||||
space last \tspace last ` /* the trailing space is intentional */ + `
|
||||
"\nnewline first"\t"\nnewline first"
|
||||
"newline last\n"\t"newline last\n"
|
||||
"newline\ninside"\t"newline\ninside"
|
||||
"""tab\tinside quotes outside"""\t"""tab\tinside quotes outside"""
|
||||
"""tab""\tbetween ""quoted"""\t"""tab""\tbetween ""quoted"""
|
||||
"""newline\ninside quotes outside"""\t"""newline\ninside quotes outside"""
|
||||
"""newline""\nbetween ""quoted"""\t"""newline""\nbetween ""quoted"""
|
||||
"\t"
|
||||
""\t""
|
||||
\t \t"\t"\t"\n"\t'\t\\
|
||||
0\t1\tfalse\ttrue\t\t\tNaN`;
|
||||
|
||||
const sampleDecoded = [
|
||||
['plain value', 'plain value'],
|
||||
['quotes "inside" hello', 'quotes "inside" hello'],
|
||||
['half quotes', 'half quotes'], // not what was encoded, but matches Excel
|
||||
['half "quotes"', 'half "quotes"'],
|
||||
['full quotes', 'full quotes'], // not what was encoded, but matches Excel
|
||||
['extra "quotes"', 'extra "quotes"'], // not what was encoded, but matches Excel
|
||||
['has ""double"" quotes"', 'has ""double"" quotes"'], // not what was encoded, but matches Excel
|
||||
['more "double"\tmore ""double""'], // not what was encoded, but matches Excel
|
||||
['tab\tinside', 'tab\tinside'],
|
||||
['\ttab first', '\ttab first'],
|
||||
['tab last\t', 'tab last\t'],
|
||||
[' space first', ' space first'],
|
||||
['space last ', 'space last '],
|
||||
['\nnewline first', '\nnewline first'],
|
||||
['newline last\n', 'newline last\n'],
|
||||
['newline\ninside', 'newline\ninside'],
|
||||
['"tab\tinside quotes outside"', '"tab\tinside quotes outside"'],
|
||||
['"tab"\tbetween "quoted"', '"tab"\tbetween "quoted"'],
|
||||
['"newline\ninside quotes outside"', '"newline\ninside quotes outside"'],
|
||||
['"newline"\nbetween "quoted"', '"newline"\nbetween "quoted"'],
|
||||
['\t'], // not what was encoded, but matches Excel
|
||||
['', ''], // not what was encoded, but matches Excel
|
||||
// A few special characters on their own that should work correctly.
|
||||
['', ' ', '\t', '\n', "'", "\\"],
|
||||
// All values get parsed as strings.
|
||||
['0', '1', 'false', 'true', '', '', 'NaN'],
|
||||
];
|
||||
|
||||
describe('tsvFormat', function() {
|
||||
it('should encode tab-separated values as Excel does', function() {
|
||||
assert.deepEqual(tsvEncode(sampleData), sampleEncoded);
|
||||
});
|
||||
|
||||
it('should decode tab-separated values as Excel does', function() {
|
||||
assert.deepEqual(tsvDecode(sampleEncoded), sampleDecoded);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user