make-mp-data 1.4.4 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/launch.json +1 -1
- package/.vscode/settings.json +1 -0
- package/index.js +1410 -0
- package/package.json +12 -9
- package/schemas/adspend.js +96 -0
- package/schemas/anon.js +4 -4
- package/schemas/big.js +160 -0
- package/schemas/complex.js +32 -25
- package/schemas/foobar.js +22 -7
- package/schemas/funnels.js +12 -12
- package/schemas/mirror.js +129 -0
- package/schemas/simple.js +18 -32
- package/scratch.mjs +33 -11
- package/scripts/jsdoctest.js +1 -1
- package/scripts/new.sh +68 -0
- package/{core → src}/cli.js +10 -3
- package/{core → src}/utils.js +167 -197
- package/tests/benchmark/concurrency.mjs +52 -0
- package/tests/coverage/.gitkeep +0 -0
- package/tests/e2e.test.js +245 -58
- package/tests/int.test.js +618 -0
- package/tests/jest.config.js +11 -2
- package/tests/testSoup.mjs +3 -3
- package/tests/unit.test.js +351 -227
- package/tsconfig.json +1 -1
- package/types.d.ts +217 -97
- package/core/index.js +0 -1008
- package/schemas/deepNest.js +0 -106
- /package/{data → dungeons}/.gitkeep +0 -0
- /package/{core → src}/chart.js +0 -0
- /package/{core → src}/defaults.js +0 -0
package/tests/unit.test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const generate = require('../
|
|
1
|
+
const generate = require('../index.js');
|
|
2
2
|
const dayjs = require("dayjs");
|
|
3
3
|
const utc = require("dayjs/plugin/utc");
|
|
4
4
|
const fs = require('fs');
|
|
@@ -6,7 +6,17 @@ const u = require('ak-tools');
|
|
|
6
6
|
dayjs.extend(utc);
|
|
7
7
|
require('dotenv').config();
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/** @typedef {import('../types').Config} Config */
|
|
10
|
+
/** @typedef {import('../types').EventConfig} EventConfig */
|
|
11
|
+
/** @typedef {import('../types').ValueValid} ValueValid */
|
|
12
|
+
/** @typedef {import('../types').HookedArray} hookArray */
|
|
13
|
+
/** @typedef {import('../types').hookArrayOptions} hookArrayOptions */
|
|
14
|
+
/** @typedef {import('../types').Person} Person */
|
|
15
|
+
/** @typedef {import('../types').Funnel} Funnel */
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
applySkew,
|
|
10
20
|
boxMullerRandom,
|
|
11
21
|
choose,
|
|
12
22
|
date,
|
|
@@ -21,8 +31,8 @@ const { applySkew,
|
|
|
21
31
|
pick,
|
|
22
32
|
range,
|
|
23
33
|
pickAWinner,
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
weighNumRange,
|
|
35
|
+
|
|
26
36
|
fixFirstAndLast,
|
|
27
37
|
generateUser,
|
|
28
38
|
openFinder,
|
|
@@ -41,67 +51,123 @@ const { applySkew,
|
|
|
41
51
|
getChance,
|
|
42
52
|
initChance,
|
|
43
53
|
validateEventConfig,
|
|
44
|
-
|
|
54
|
+
validTime,
|
|
45
55
|
interruptArray,
|
|
46
56
|
optimizedBoxMuller,
|
|
47
|
-
|
|
48
|
-
datesBetween
|
|
49
|
-
|
|
57
|
+
|
|
58
|
+
datesBetween,
|
|
59
|
+
weighChoices
|
|
60
|
+
} = require('../src/utils.js');
|
|
61
|
+
|
|
62
|
+
const main = require('../index.js');
|
|
63
|
+
//todo: test for funnel inference
|
|
64
|
+
const { hookArray, inferFunnels } = main.meta;
|
|
50
65
|
|
|
51
66
|
|
|
52
67
|
describe('timesoup', () => {
|
|
68
|
+
|
|
53
69
|
test('always valid times', () => {
|
|
54
70
|
const dates = [];
|
|
71
|
+
const earliest = dayjs().subtract(50, 'D').unix();
|
|
72
|
+
const latest = dayjs().subtract(1, "D").unix();
|
|
55
73
|
for (let i = 0; i < 10000; i++) {
|
|
56
|
-
|
|
57
|
-
dates.push(TimeSoup());
|
|
74
|
+
dates.push(TimeSoup(earliest, latest));
|
|
58
75
|
}
|
|
59
76
|
const tooOld = dates.filter(d => dayjs(d).isBefore(dayjs.unix(0)));
|
|
60
77
|
const badYear = dates.filter(d => !d.startsWith('202'));
|
|
61
78
|
expect(dates.every(d => dayjs(d).isAfter(dayjs.unix(0)))).toBe(true);
|
|
62
79
|
expect(dates.every(d => d.startsWith('202'))).toBe(true);
|
|
80
|
+
expect(tooOld.length).toBe(0);
|
|
81
|
+
expect(badYear.length).toBe(0);
|
|
82
|
+
});
|
|
63
83
|
|
|
84
|
+
test('custom peaks', () => {
|
|
85
|
+
const dates = [];
|
|
86
|
+
const earliest = dayjs().subtract(50, 'D').unix();
|
|
87
|
+
const latest = dayjs().subtract(1, "D").unix();
|
|
88
|
+
for (let i = 0; i < 10000; i++) {
|
|
89
|
+
dates.push(TimeSoup(earliest, latest, 10));
|
|
90
|
+
}
|
|
91
|
+
const tooOld = dates.filter(d => dayjs(d).isBefore(dayjs.unix(0)));
|
|
92
|
+
const badYear = dates.filter(d => !d.startsWith('202'));
|
|
93
|
+
expect(dates.every(d => dayjs(d).isAfter(dayjs.unix(0)))).toBe(true);
|
|
94
|
+
expect(dates.every(d => d.startsWith('202'))).toBe(true);
|
|
95
|
+
expect(tooOld.length).toBe(0);
|
|
96
|
+
expect(badYear.length).toBe(0);
|
|
64
97
|
});
|
|
98
|
+
|
|
99
|
+
test('custom deviation', () => {
|
|
100
|
+
const dates = [];
|
|
101
|
+
const earliest = dayjs().subtract(50, 'D').unix();
|
|
102
|
+
const latest = dayjs().subtract(1, "D").unix();
|
|
103
|
+
for (let i = 0; i < 10000; i++) {
|
|
104
|
+
dates.push(TimeSoup(earliest, latest, 10, .5));
|
|
105
|
+
}
|
|
106
|
+
const tooOld = dates.filter(d => dayjs(d).isBefore(dayjs.unix(0)));
|
|
107
|
+
const badYear = dates.filter(d => !d.startsWith('202'));
|
|
108
|
+
expect(dates.every(d => dayjs(d).isAfter(dayjs.unix(0)))).toBe(true);
|
|
109
|
+
expect(dates.every(d => d.startsWith('202'))).toBe(true);
|
|
110
|
+
expect(tooOld.length).toBe(0);
|
|
111
|
+
expect(badYear.length).toBe(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
|
|
65
115
|
});
|
|
66
116
|
|
|
67
117
|
|
|
68
|
-
describe('
|
|
118
|
+
describe('filenames', () => {
|
|
69
119
|
|
|
70
120
|
test('default config', () => {
|
|
71
121
|
const config = { simulationName: 'testSim' };
|
|
72
122
|
const result = buildFileNames(config);
|
|
73
|
-
|
|
74
|
-
expect(
|
|
75
|
-
expect(
|
|
76
|
-
expect(
|
|
77
|
-
expect(
|
|
78
|
-
expect(
|
|
79
|
-
expect(
|
|
123
|
+
const { eventFiles, folder, groupFiles, lookupFiles, mirrorFiles, scdFiles, userFiles, adSpendFiles } = result;
|
|
124
|
+
expect(eventFiles).toEqual(['testSim-EVENTS.csv']);
|
|
125
|
+
expect(userFiles).toEqual(['testSim-USERS.csv']);
|
|
126
|
+
expect(scdFiles).toEqual([]);
|
|
127
|
+
expect(groupFiles).toEqual([]);
|
|
128
|
+
expect(lookupFiles).toEqual([]);
|
|
129
|
+
expect(mirrorFiles).toEqual([]);
|
|
130
|
+
expect(adSpendFiles).toEqual([]);
|
|
131
|
+
expect(folder).toEqual('./');
|
|
80
132
|
});
|
|
81
133
|
|
|
82
134
|
test('json format', () => {
|
|
135
|
+
/** @type {Config} */
|
|
83
136
|
const config = { simulationName: 'testSim', format: 'json' };
|
|
84
137
|
const result = buildFileNames(config);
|
|
85
|
-
|
|
86
|
-
expect(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
138
|
+
const { eventFiles, folder, groupFiles, lookupFiles, mirrorFiles, scdFiles, userFiles, adSpendFiles } = result;
|
|
139
|
+
expect(eventFiles).toEqual(['testSim-EVENTS.json']);
|
|
140
|
+
expect(userFiles).toEqual(['testSim-USERS.json']);
|
|
141
|
+
expect(scdFiles).toEqual([]);
|
|
142
|
+
expect(groupFiles).toEqual([]);
|
|
143
|
+
expect(lookupFiles).toEqual([]);
|
|
144
|
+
expect(mirrorFiles).toEqual([]);
|
|
145
|
+
expect(adSpendFiles).toEqual([]);
|
|
146
|
+
expect(folder).toEqual('./');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('scd tables', () => {
|
|
90
150
|
const config = {
|
|
91
151
|
simulationName: 'testSim',
|
|
92
152
|
scdProps: { prop1: {}, prop2: {} }
|
|
93
153
|
};
|
|
94
154
|
const result = buildFileNames(config);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
]);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
155
|
+
const { eventFiles, folder, groupFiles, lookupFiles, mirrorFiles, scdFiles, userFiles, adSpendFiles } = result;
|
|
156
|
+
expect(eventFiles).toEqual(['testSim-EVENTS.csv']);
|
|
157
|
+
expect(userFiles).toEqual(['testSim-USERS.csv']);
|
|
158
|
+
expect(scdFiles).toEqual(['testSim-prop1-SCD.csv', 'testSim-prop2-SCD.csv']);
|
|
159
|
+
expect(groupFiles).toEqual([]);
|
|
160
|
+
expect(lookupFiles).toEqual([]);
|
|
161
|
+
expect(mirrorFiles).toEqual([]);
|
|
162
|
+
expect(adSpendFiles).toEqual([]);
|
|
163
|
+
expect(folder).toEqual('./');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('group keys', () => {
|
|
167
|
+
/** @type {Config} */
|
|
102
168
|
const config = {
|
|
103
169
|
simulationName: 'testSim',
|
|
104
|
-
groupKeys: [['group1'], ['group2']]
|
|
170
|
+
groupKeys: [['group1', 10], ['group2', 20]]
|
|
105
171
|
};
|
|
106
172
|
const result = buildFileNames(config);
|
|
107
173
|
expect(result.groupFiles).toEqual([
|
|
@@ -110,10 +176,11 @@ describe('names', () => {
|
|
|
110
176
|
]);
|
|
111
177
|
});
|
|
112
178
|
|
|
113
|
-
test('
|
|
179
|
+
test('lookup tables', () => {
|
|
180
|
+
/** @type {Config} */
|
|
114
181
|
const config = {
|
|
115
182
|
simulationName: 'testSim',
|
|
116
|
-
lookupTables: [{ key: 'lookup1' }, { key: 'lookup2' }]
|
|
183
|
+
lookupTables: [{ key: 'lookup1', attributes: {}, entries: 10 }, { key: 'lookup2', attributes: {}, entries: 10 }]
|
|
117
184
|
};
|
|
118
185
|
const result = buildFileNames(config);
|
|
119
186
|
expect(result.lookupFiles).toEqual([
|
|
@@ -122,43 +189,45 @@ describe('names', () => {
|
|
|
122
189
|
]);
|
|
123
190
|
});
|
|
124
191
|
|
|
125
|
-
test('
|
|
192
|
+
test('mirror tables', () => {
|
|
193
|
+
/** @type {Config} */
|
|
126
194
|
const config = {
|
|
127
195
|
simulationName: 'testSim',
|
|
128
|
-
mirrorProps: { prop1: {} }
|
|
196
|
+
mirrorProps: { prop1: { values: [] } }
|
|
129
197
|
};
|
|
130
198
|
const result = buildFileNames(config);
|
|
131
199
|
expect(result.mirrorFiles).toEqual(['testSim-MIRROR.csv']);
|
|
132
200
|
});
|
|
133
201
|
|
|
134
|
-
test('
|
|
202
|
+
test('validate disk path', async () => {
|
|
135
203
|
const config = { simulationName: 'testSim', writeToDisk: true };
|
|
136
204
|
const result = await buildFileNames(config);
|
|
137
205
|
expect(result.folder).toBeDefined();
|
|
138
|
-
|
|
139
206
|
});
|
|
140
207
|
|
|
141
208
|
|
|
142
|
-
test('
|
|
209
|
+
test('bad name', () => {
|
|
210
|
+
/** @type {Config} */
|
|
211
|
+
// @ts-ignore
|
|
143
212
|
const config = { simulationName: 123 };
|
|
144
213
|
expect(() => buildFileNames(config)).toThrow('simName must be a string');
|
|
145
214
|
});
|
|
146
215
|
|
|
147
216
|
|
|
148
|
-
test('
|
|
217
|
+
test('JSON: writes', async () => {
|
|
149
218
|
const path = 'test.json';
|
|
150
219
|
const data = [{ a: 1, b: 2 }, { a: 3, b: 4 }];
|
|
151
|
-
await streamJSON(path, data);
|
|
220
|
+
const streamed = await streamJSON(path, data);
|
|
152
221
|
const content = fs.readFileSync(path, 'utf8');
|
|
153
222
|
const lines = content.trim().split('\n').map(line => JSON.parse(line));
|
|
154
223
|
expect(lines).toEqual(data);
|
|
155
224
|
fs.unlinkSync(path);
|
|
156
225
|
});
|
|
157
226
|
|
|
158
|
-
test('
|
|
227
|
+
test('CSV: writes', async () => {
|
|
159
228
|
const path = 'test.csv';
|
|
160
229
|
const data = [{ a: 1, b: 2 }, { a: 3, b: 4 }];
|
|
161
|
-
await streamCSV(path, data);
|
|
230
|
+
const streamed = await streamCSV(path, data);
|
|
162
231
|
const content = fs.readFileSync(path, 'utf8');
|
|
163
232
|
const lines = content.trim().split('\n');
|
|
164
233
|
expect(lines.length).toBe(3); // Including header
|
|
@@ -166,30 +235,12 @@ describe('names', () => {
|
|
|
166
235
|
});
|
|
167
236
|
|
|
168
237
|
|
|
169
|
-
test('generateUser: works', () => {
|
|
170
|
-
const uuid = { guid: jest.fn().mockReturnValue('uuid-123') };
|
|
171
|
-
const numDays = 30;
|
|
172
|
-
const user = generateUser(numDays);
|
|
173
|
-
expect(user).toHaveProperty('distinct_id');
|
|
174
|
-
expect(user).toHaveProperty('name');
|
|
175
|
-
expect(user).toHaveProperty('email');
|
|
176
|
-
expect(user).toHaveProperty('avatar');
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
test('enrichArray: works', () => {
|
|
180
|
-
const arr = [];
|
|
181
|
-
const enrichedArray = enrichArray(arr);
|
|
182
|
-
enrichedArray.hookPush(1);
|
|
183
|
-
enrichedArray.hookPush(2);
|
|
184
|
-
const match = JSON.stringify(enrichedArray) === JSON.stringify([1, 2]);
|
|
185
|
-
expect(match).toEqual(true);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
238
|
});
|
|
189
239
|
|
|
190
240
|
|
|
191
241
|
describe('determinism', () => {
|
|
192
|
-
|
|
242
|
+
|
|
243
|
+
test('seed from env', () => {
|
|
193
244
|
process.env.SEED = 'test-seed';
|
|
194
245
|
// @ts-ignore
|
|
195
246
|
initChance();
|
|
@@ -200,111 +251,69 @@ describe('determinism', () => {
|
|
|
200
251
|
|
|
201
252
|
});
|
|
202
253
|
|
|
203
|
-
test('
|
|
254
|
+
test('seed explicitly passed', () => {
|
|
204
255
|
const seed = 'initial-seed';
|
|
205
256
|
initChance(seed);
|
|
206
257
|
const chance1 = getChance();
|
|
207
258
|
initChance('new-seed');
|
|
208
259
|
const chance2 = getChance();
|
|
209
260
|
expect(chance1).toBe(chance2);
|
|
210
|
-
|
|
211
261
|
});
|
|
262
|
+
|
|
212
263
|
});
|
|
213
264
|
|
|
214
265
|
|
|
215
266
|
describe('generation', () => {
|
|
216
|
-
|
|
267
|
+
|
|
268
|
+
test('user: works', () => {
|
|
269
|
+
const uuid = { guid: jest.fn().mockReturnValue('uuid-123') };
|
|
217
270
|
const numDays = 30;
|
|
218
|
-
const user = generateUser('
|
|
271
|
+
const user = generateUser('123', { numDays });
|
|
219
272
|
expect(user).toHaveProperty('distinct_id');
|
|
220
273
|
expect(user).toHaveProperty('name');
|
|
221
274
|
expect(user).toHaveProperty('email');
|
|
222
|
-
expect(user).toHaveProperty('avatar');
|
|
275
|
+
expect(user).not.toHaveProperty('avatar');
|
|
223
276
|
expect(user).toHaveProperty('created');
|
|
224
|
-
expect(user).toHaveProperty('anonymousIds');
|
|
225
|
-
expect(user).toHaveProperty('sessionIds');
|
|
277
|
+
expect(user).not.toHaveProperty('anonymousIds');
|
|
278
|
+
expect(user).not.toHaveProperty('sessionIds');
|
|
226
279
|
});
|
|
227
280
|
|
|
228
281
|
test('user: in time range', () => {
|
|
229
282
|
const numDays = 30;
|
|
230
|
-
const user = generateUser('uuid-123', numDays);
|
|
283
|
+
const user = generateUser('uuid-123', { numDays });
|
|
231
284
|
const createdDate = dayjs(user.created, 'YYYY-MM-DD');
|
|
232
285
|
expect(createdDate.isValid()).toBeTruthy();
|
|
233
286
|
expect(createdDate.isBefore(dayjs.unix(global.NOW))).toBeTruthy();
|
|
234
287
|
});
|
|
235
288
|
|
|
236
|
-
test('winner: return func', () => {
|
|
237
|
-
const items = ['a', 'b', 'c'];
|
|
238
|
-
const result = pickAWinner(items, 0);
|
|
239
|
-
expect(typeof result).toBe('function');
|
|
240
|
-
});
|
|
241
289
|
|
|
242
|
-
test('
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
expect(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const items = ['a', 'b', 'c'];
|
|
254
|
-
const mostChosenIndex = 0;
|
|
255
|
-
const pickFunction = pickAWinner(items, mostChosenIndex);
|
|
256
|
-
const weightedList = pickFunction();
|
|
257
|
-
|
|
258
|
-
const secondMostChosenIndex = (mostChosenIndex + 1) % items.length;
|
|
259
|
-
|
|
260
|
-
// Expect the second most chosen item to appear at least once
|
|
261
|
-
expect(weightedList.includes(items[secondMostChosenIndex])).toBeTruthy();
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test('winner: third most', () => {
|
|
265
|
-
const items = ['a', 'b', 'c'];
|
|
266
|
-
const mostChosenIndex = 0;
|
|
267
|
-
const pickFunction = pickAWinner(items, mostChosenIndex);
|
|
268
|
-
const weightedList = pickFunction();
|
|
269
|
-
|
|
270
|
-
const thirdMostChosenIndex = (mostChosenIndex + 2) % items.length;
|
|
271
|
-
|
|
272
|
-
// Expect the third most chosen item to appear at least once
|
|
273
|
-
expect(weightedList.includes(items[thirdMostChosenIndex])).toBeTruthy();
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
test('winner: exceed array bounds', () => {
|
|
277
|
-
const items = ['a', 'b', 'c'];
|
|
278
|
-
const mostChosenIndex = 0;
|
|
279
|
-
const pickFunction = pickAWinner(items, mostChosenIndex);
|
|
280
|
-
const weightedList = pickFunction();
|
|
281
|
-
|
|
282
|
-
// Ensure all indices are within the bounds of the array
|
|
283
|
-
weightedList.forEach(item => {
|
|
284
|
-
expect(items.includes(item)).toBeTruthy();
|
|
285
|
-
});
|
|
290
|
+
test('person: works', () => {
|
|
291
|
+
const numDays = 30;
|
|
292
|
+
const user = person('uuid-123', numDays, false);
|
|
293
|
+
expect(user).toHaveProperty('distinct_id');
|
|
294
|
+
expect(user.distinct_id).toBe('uuid-123');
|
|
295
|
+
expect(user).toHaveProperty('name');
|
|
296
|
+
expect(user).toHaveProperty('email');
|
|
297
|
+
expect(user).not.toHaveProperty('avatar');
|
|
298
|
+
expect(user).toHaveProperty('created');
|
|
299
|
+
expect(user).not.toHaveProperty('anonymousIds');
|
|
300
|
+
expect(user).not.toHaveProperty('sessionIds');
|
|
286
301
|
});
|
|
287
302
|
|
|
288
|
-
test('
|
|
289
|
-
const
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
303
|
+
test('person: anon', () => {
|
|
304
|
+
const numDays = 30;
|
|
305
|
+
const user = person('uuid-123', numDays, true);
|
|
306
|
+
expect(user).toHaveProperty('distinct_id');
|
|
307
|
+
expect(user).toHaveProperty('name');
|
|
308
|
+
expect(user.name).toBe('Anonymous User');
|
|
309
|
+
expect(user).toHaveProperty('email');
|
|
310
|
+
expect(user.email.includes('*')).toBeTruthy();
|
|
311
|
+
expect(user).not.toHaveProperty('avatar');
|
|
312
|
+
expect(user).toHaveProperty('created');
|
|
313
|
+
expect(user).not.toHaveProperty('anonymousIds');
|
|
314
|
+
expect(user).not.toHaveProperty('sessionIds');
|
|
298
315
|
});
|
|
299
316
|
|
|
300
|
-
test('winner: empty array', () => {
|
|
301
|
-
const items = [];
|
|
302
|
-
const pickFunction = pickAWinner(items, 0);
|
|
303
|
-
const weightedList = pickFunction();
|
|
304
|
-
|
|
305
|
-
// Expect the result to be an empty array
|
|
306
|
-
expect(weightedList.length).toBe(0);
|
|
307
|
-
});
|
|
308
317
|
|
|
309
318
|
test('dates: same start end', () => {
|
|
310
319
|
const start = '2023-06-10';
|
|
@@ -380,13 +389,14 @@ describe('generation', () => {
|
|
|
380
389
|
|
|
381
390
|
});
|
|
382
391
|
|
|
392
|
+
|
|
383
393
|
describe('validation', () => {
|
|
384
394
|
|
|
385
395
|
beforeAll(() => {
|
|
386
396
|
global.NOW = 1672531200; // fixed point in time for testing
|
|
387
397
|
});
|
|
388
398
|
|
|
389
|
-
test('events:
|
|
399
|
+
test('events: non arrays', () => {
|
|
390
400
|
// @ts-ignore
|
|
391
401
|
expect(() => validateEventConfig("not an array")).toThrow("events must be an array");
|
|
392
402
|
});
|
|
@@ -427,58 +437,68 @@ describe('validation', () => {
|
|
|
427
437
|
expect(result[0].weight).toBeLessThanOrEqual(5);
|
|
428
438
|
});
|
|
429
439
|
|
|
430
|
-
test('
|
|
440
|
+
test('time: between', () => {
|
|
431
441
|
const chosenTime = global.NOW - (60 * 60 * 24 * 15); // 15 days ago
|
|
432
442
|
const earliestTime = global.NOW - (60 * 60 * 24 * 30); // 30 days ago
|
|
433
443
|
const latestTime = global.NOW;
|
|
434
|
-
expect(
|
|
444
|
+
expect(validTime(chosenTime, earliestTime, latestTime)).toBe(true);
|
|
435
445
|
});
|
|
436
446
|
|
|
437
|
-
test('
|
|
447
|
+
test('time: outside earliest', () => {
|
|
438
448
|
const chosenTime = global.NOW - (60 * 60 * 24 * 31); // 31 days ago
|
|
439
449
|
const earliestTime = global.NOW - (60 * 60 * 24 * 30); // 30 days ago
|
|
440
450
|
const latestTime = global.NOW;
|
|
441
|
-
expect(
|
|
451
|
+
expect(validTime(chosenTime, earliestTime, latestTime)).toBe(false);
|
|
442
452
|
});
|
|
443
453
|
|
|
444
|
-
test('
|
|
454
|
+
test('time: outside latest', () => {
|
|
445
455
|
const chosenTime = -1;
|
|
446
456
|
const earliestTime = global.NOW - (60 * 60 * 24 * 30); // 30 days ago
|
|
447
457
|
const latestTime = global.NOW;
|
|
448
|
-
expect(
|
|
458
|
+
expect(validTime(chosenTime, earliestTime, latestTime)).toBe(false);
|
|
449
459
|
});
|
|
450
460
|
|
|
451
|
-
test('
|
|
461
|
+
test('time: inference in', () => {
|
|
452
462
|
const chosenTime = global.NOW - (60 * 60 * 24 * 15); // 15 days ago
|
|
453
|
-
expect(
|
|
463
|
+
expect(validTime(chosenTime)).toBe(true);
|
|
454
464
|
});
|
|
455
465
|
|
|
456
|
-
test('
|
|
466
|
+
test('time: inference out', () => {
|
|
457
467
|
const chosenTime = global.NOW - (60 * 60 * 24 * 31); // 31 days ago
|
|
458
|
-
expect(
|
|
468
|
+
expect(validTime(chosenTime)).toBe(false);
|
|
459
469
|
});
|
|
460
470
|
});
|
|
461
471
|
|
|
462
472
|
describe('enrichment', () => {
|
|
463
|
-
|
|
473
|
+
|
|
474
|
+
test('hooks: noop', async () => {
|
|
475
|
+
const arr = [];
|
|
476
|
+
const enrichedArray = await hookArray(arr);
|
|
477
|
+
await enrichedArray.hookPush(1);
|
|
478
|
+
await enrichedArray.hookPush(2);
|
|
479
|
+
const match = JSON.stringify(enrichedArray) === JSON.stringify([1, 2]);
|
|
480
|
+
expect(match).toEqual(true);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test('hook: double', async () => {
|
|
464
484
|
const arr = [];
|
|
465
485
|
const hook = (item) => item * 2;
|
|
466
|
-
const enrichedArray =
|
|
467
|
-
enrichedArray.hookPush(1);
|
|
468
|
-
enrichedArray.hookPush(2);
|
|
486
|
+
const enrichedArray = await hookArray(arr, { hook });
|
|
487
|
+
await enrichedArray.hookPush(1);
|
|
488
|
+
await enrichedArray.hookPush(2);
|
|
469
489
|
expect(enrichedArray.includes(2)).toBeTruthy();
|
|
470
490
|
expect(enrichedArray.includes(4)).toBeTruthy();
|
|
471
491
|
});
|
|
472
492
|
|
|
473
|
-
test('filter
|
|
493
|
+
test('hooks: filter', async () => {
|
|
474
494
|
const arr = [];
|
|
475
495
|
const hook = (item) => item ? item.toString() : item;
|
|
476
|
-
const enrichedArray =
|
|
477
|
-
enrichedArray.hookPush(null);
|
|
478
|
-
enrichedArray.hookPush(undefined);
|
|
479
|
-
enrichedArray.hookPush({});
|
|
480
|
-
enrichedArray.hookPush({ a: 1 });
|
|
481
|
-
enrichedArray.hookPush([1, 2]);
|
|
496
|
+
const enrichedArray = await hookArray(arr, { hook });
|
|
497
|
+
await enrichedArray.hookPush(null);
|
|
498
|
+
await enrichedArray.hookPush(undefined);
|
|
499
|
+
await enrichedArray.hookPush({});
|
|
500
|
+
await enrichedArray.hookPush({ a: 1 });
|
|
501
|
+
await enrichedArray.hookPush([1, 2]);
|
|
482
502
|
expect(enrichedArray).toHaveLength(3);
|
|
483
503
|
expect(enrichedArray.includes('null')).toBeFalsy();
|
|
484
504
|
expect(enrichedArray.includes('undefined')).toBeFalsy();
|
|
@@ -505,7 +525,6 @@ describe('utilities', () => {
|
|
|
505
525
|
});
|
|
506
526
|
|
|
507
527
|
|
|
508
|
-
|
|
509
528
|
test('integer: diff', () => {
|
|
510
529
|
const min = 5;
|
|
511
530
|
const max = 10;
|
|
@@ -519,16 +538,6 @@ describe('utilities', () => {
|
|
|
519
538
|
});
|
|
520
539
|
|
|
521
540
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
test('person: fields', () => {
|
|
525
|
-
const generatedPerson = person('myId');
|
|
526
|
-
expect(generatedPerson).toHaveProperty('name');
|
|
527
|
-
expect(generatedPerson).toHaveProperty('email');
|
|
528
|
-
expect(generatedPerson).toHaveProperty('avatar');
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
|
|
532
541
|
test('date: past', () => {
|
|
533
542
|
const pastDate = date(10, true, 'YYYY-MM-DD')();
|
|
534
543
|
expect(dayjs(pastDate, 'YYYY-MM-DD').isValid()).toBeTruthy();
|
|
@@ -571,28 +580,6 @@ describe('utilities', () => {
|
|
|
571
580
|
expect(result).toBe('nested');
|
|
572
581
|
});
|
|
573
582
|
|
|
574
|
-
test('weightedRange: within range', () => {
|
|
575
|
-
const values = weightedRange(5, 15);
|
|
576
|
-
expect(values.every(v => v >= 5 && v <= 15)).toBe(true);
|
|
577
|
-
expect(values.length).toBe(50);
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
test('applySkew: skews', () => {
|
|
581
|
-
const value = optimizedBoxMuller();
|
|
582
|
-
const skewedValue = applySkew(value, .25);
|
|
583
|
-
expect(Math.abs(skewedValue)).toBeLessThanOrEqual(Math.abs(value) + 1);
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
test('mapToRange: works', () => {
|
|
587
|
-
const value = 0;
|
|
588
|
-
const mean = 10;
|
|
589
|
-
const sd = 5;
|
|
590
|
-
const mappedValue = mapToRange(value, mean, sd);
|
|
591
|
-
expect(mappedValue).toBe(10);
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
583
|
test('exhaust: elements', () => {
|
|
597
584
|
const arr = [1, 2, 3];
|
|
598
585
|
const exhaustFn = exhaust([...arr]);
|
|
@@ -611,25 +598,12 @@ describe('utilities', () => {
|
|
|
611
598
|
});
|
|
612
599
|
|
|
613
600
|
|
|
614
|
-
test('
|
|
615
|
-
const dates = [];
|
|
616
|
-
for (let i = 0; i < 10000; i++) {
|
|
617
|
-
const earliest = dayjs().subtract(u.rand(5, 50), 'D');
|
|
618
|
-
dates.push(TimeSoup());
|
|
619
|
-
}
|
|
620
|
-
const tooOld = dates.filter(d => dayjs(d).isBefore(dayjs.unix(0)));
|
|
621
|
-
const badYear = dates.filter(d => !d.startsWith('202'));
|
|
622
|
-
expect(dates.every(d => dayjs(d).isAfter(dayjs.unix(0)))).toBe(true);
|
|
623
|
-
expect(dates.every(d => d.startsWith('202'))).toBe(true);
|
|
624
|
-
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
test('date', () => {
|
|
601
|
+
test('date: valid', () => {
|
|
628
602
|
const result = date();
|
|
629
603
|
expect(dayjs(result()).isValid()).toBe(true);
|
|
630
604
|
});
|
|
631
605
|
|
|
632
|
-
test('dates', () => {
|
|
606
|
+
test('dates: valid', () => {
|
|
633
607
|
const result = dates();
|
|
634
608
|
expect(result).toBeInstanceOf(Array);
|
|
635
609
|
expect(result.length).toBe(5); // Assuming default numPairs is 5
|
|
@@ -641,7 +615,7 @@ describe('utilities', () => {
|
|
|
641
615
|
});
|
|
642
616
|
});
|
|
643
617
|
|
|
644
|
-
test('day', () => {
|
|
618
|
+
test('day: works', () => {
|
|
645
619
|
const start = '2020-01-01';
|
|
646
620
|
const end = '2020-01-30';
|
|
647
621
|
const result = day(start, end);
|
|
@@ -650,7 +624,7 @@ describe('utilities', () => {
|
|
|
650
624
|
expect(dayjs(dayResult.day).isBefore(dayjs(dayResult.end))).toBe(true);
|
|
651
625
|
});
|
|
652
626
|
|
|
653
|
-
test('exhaust', () => {
|
|
627
|
+
test('exhaust: works', () => {
|
|
654
628
|
const arr = [1, 2, 3];
|
|
655
629
|
const next = exhaust(arr);
|
|
656
630
|
expect(next()).toBe(1);
|
|
@@ -684,20 +658,8 @@ describe('utilities', () => {
|
|
|
684
658
|
});
|
|
685
659
|
|
|
686
660
|
|
|
687
|
-
test('weighArray: works', () => {
|
|
688
|
-
const arr = ['a', 'b', 'c'];
|
|
689
|
-
const weightedArr = weighArray(arr);
|
|
690
|
-
expect(weightedArr.length).toBeGreaterThanOrEqual(arr.length);
|
|
691
|
-
});
|
|
692
661
|
|
|
693
|
-
test('
|
|
694
|
-
const acc = [];
|
|
695
|
-
const funnel = { weight: 3 };
|
|
696
|
-
const result = weighFunnels(acc, funnel);
|
|
697
|
-
expect(result.length).toBe(3);
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
test('progress: outputs correctly', () => {
|
|
662
|
+
test('progress: output', () => {
|
|
701
663
|
// @ts-ignore
|
|
702
664
|
const mockStdoutWrite = jest.spyOn(process.stdout, 'write').mockImplementation(() => { });
|
|
703
665
|
progress([['test', 50]]);
|
|
@@ -706,13 +668,11 @@ describe('utilities', () => {
|
|
|
706
668
|
});
|
|
707
669
|
|
|
708
670
|
test('range: works', () => {
|
|
709
|
-
const result =
|
|
710
|
-
range.call(result, 1, 5);
|
|
671
|
+
const result = range(1, 5);
|
|
711
672
|
expect(result).toEqual([1, 2, 3, 4, 5]);
|
|
712
673
|
});
|
|
713
674
|
|
|
714
675
|
|
|
715
|
-
|
|
716
676
|
test('shuffleArray: works', () => {
|
|
717
677
|
const arr = [1, 2, 3, 4, 5];
|
|
718
678
|
const shuffled = shuffleArray([...arr]);
|
|
@@ -756,7 +716,7 @@ describe('utilities', () => {
|
|
|
756
716
|
expect(shuffled.slice(1, -1)).toEqual(arr.slice(1, -1));
|
|
757
717
|
});
|
|
758
718
|
|
|
759
|
-
test('box
|
|
719
|
+
test('box: distribution', () => {
|
|
760
720
|
const values = [];
|
|
761
721
|
for (let i = 0; i < 10000; i++) {
|
|
762
722
|
values.push(boxMullerRandom());
|
|
@@ -768,7 +728,7 @@ describe('utilities', () => {
|
|
|
768
728
|
expect(stdDev).toBeCloseTo(1, 1);
|
|
769
729
|
});
|
|
770
730
|
|
|
771
|
-
test('optimized box
|
|
731
|
+
test('optimized box: distribution', () => {
|
|
772
732
|
const values = [];
|
|
773
733
|
for (let i = 0; i < 10000; i++) {
|
|
774
734
|
values.push(optimizedBoxMuller());
|
|
@@ -782,3 +742,167 @@ describe('utilities', () => {
|
|
|
782
742
|
|
|
783
743
|
|
|
784
744
|
});
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
describe('weights', () => {
|
|
748
|
+
test('weighChoices: objects', () => {
|
|
749
|
+
const items = [
|
|
750
|
+
{ value: 'foo', weight: 3 },
|
|
751
|
+
{ value: 'bar', weight: 2 }
|
|
752
|
+
];
|
|
753
|
+
const generateWeightedArray = weighChoices(items);
|
|
754
|
+
const result = generateWeightedArray();
|
|
755
|
+
|
|
756
|
+
expect(result.filter(item => item === 'foo').length).toBe(3);
|
|
757
|
+
expect(result.filter(item => item === 'bar').length).toBe(2);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test('weighChoices: strings', () => {
|
|
761
|
+
const items = ['foo', 'bar', 'baz'];
|
|
762
|
+
const generateWeightedArray = weighChoices(items);
|
|
763
|
+
const result = generateWeightedArray();
|
|
764
|
+
|
|
765
|
+
// Check that each item has a unique weight
|
|
766
|
+
const counts = items.map(item => result.filter(r => r === item).length);
|
|
767
|
+
const uniqueCounts = new Set(counts);
|
|
768
|
+
|
|
769
|
+
expect(uniqueCounts.size).toBe(items.length);
|
|
770
|
+
counts.forEach(count => {
|
|
771
|
+
expect(count).toBeGreaterThanOrEqual(1);
|
|
772
|
+
expect(count).toBeLessThanOrEqual(items.length);
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
test('weighChoices: empty', () => {
|
|
777
|
+
const items = [];
|
|
778
|
+
const generateWeightedArray = weighChoices(items);
|
|
779
|
+
const result = generateWeightedArray();
|
|
780
|
+
|
|
781
|
+
expect(result).toEqual([]);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
test('weighChoices: one string', () => {
|
|
785
|
+
const items = ['foo'];
|
|
786
|
+
const generateWeightedArray = weighChoices(items);
|
|
787
|
+
const result = generateWeightedArray();
|
|
788
|
+
|
|
789
|
+
expect(result).toEqual(['foo']);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
test('weighChoices: one obj', () => {
|
|
793
|
+
const items = [{ value: 'foo', weight: 5 }];
|
|
794
|
+
const generateWeightedArray = weighChoices(items);
|
|
795
|
+
const result = generateWeightedArray();
|
|
796
|
+
|
|
797
|
+
expect(result).toEqual(['foo', 'foo', 'foo', 'foo', 'foo']);
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
test('winner: return func', () => {
|
|
802
|
+
const items = ['a', 'b', 'c'];
|
|
803
|
+
const result = pickAWinner(items, 0);
|
|
804
|
+
expect(typeof result).toBe('function');
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
test('winner: first most', () => {
|
|
808
|
+
const items = ['a', 'b', 'c'];
|
|
809
|
+
const mostChosenIndex = 0;
|
|
810
|
+
const pickFunction = pickAWinner(items, mostChosenIndex);
|
|
811
|
+
const weightedList = pickFunction();
|
|
812
|
+
|
|
813
|
+
// Expect the most chosen item to appear at least once
|
|
814
|
+
expect(weightedList.includes(items[mostChosenIndex])).toBeTruthy();
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
test('winner: second most', () => {
|
|
818
|
+
const items = ['a', 'b', 'c'];
|
|
819
|
+
const mostChosenIndex = 0;
|
|
820
|
+
const pickFunction = pickAWinner(items, mostChosenIndex);
|
|
821
|
+
const weightedList = pickFunction();
|
|
822
|
+
|
|
823
|
+
const secondMostChosenIndex = (mostChosenIndex + 1) % items.length;
|
|
824
|
+
|
|
825
|
+
// Expect the second most chosen item to appear at least once
|
|
826
|
+
expect(weightedList.includes(items[secondMostChosenIndex])).toBeTruthy();
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
test('winner: third most', () => {
|
|
830
|
+
const items = ['a', 'b', 'c'];
|
|
831
|
+
const mostChosenIndex = 0;
|
|
832
|
+
const pickFunction = pickAWinner(items, mostChosenIndex);
|
|
833
|
+
const weightedList = pickFunction();
|
|
834
|
+
|
|
835
|
+
const thirdMostChosenIndex = (mostChosenIndex + 2) % items.length;
|
|
836
|
+
|
|
837
|
+
// Expect the third most chosen item to appear at least once
|
|
838
|
+
expect(weightedList.includes(items[thirdMostChosenIndex])).toBeTruthy();
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
test('winner: exceed array bounds', () => {
|
|
842
|
+
const items = ['a', 'b', 'c'];
|
|
843
|
+
const mostChosenIndex = 0;
|
|
844
|
+
const pickFunction = pickAWinner(items, mostChosenIndex);
|
|
845
|
+
const weightedList = pickFunction();
|
|
846
|
+
|
|
847
|
+
// Ensure all indices are within the bounds of the array
|
|
848
|
+
weightedList.forEach(item => {
|
|
849
|
+
expect(items.includes(item)).toBeTruthy();
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
test('winner: single item array', () => {
|
|
854
|
+
const items = ['a'];
|
|
855
|
+
const mostChosenIndex = 0;
|
|
856
|
+
const pickFunction = pickAWinner(items, mostChosenIndex);
|
|
857
|
+
const weightedList = pickFunction();
|
|
858
|
+
|
|
859
|
+
// Since there's only one item, all winner: he same
|
|
860
|
+
weightedList.forEach(item => {
|
|
861
|
+
expect(item).toBe('a');
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
test('winner: empty array', () => {
|
|
866
|
+
const items = [];
|
|
867
|
+
const pickFunction = pickAWinner(items, 0);
|
|
868
|
+
const weightedList = pickFunction();
|
|
869
|
+
|
|
870
|
+
// Expect the result to be an empty array
|
|
871
|
+
expect(weightedList.length).toBe(0);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
test('weighNumRange: within range', () => {
|
|
875
|
+
const values = weighNumRange(5, 15);
|
|
876
|
+
expect(values.every(v => v >= 5 && v <= 15)).toBe(true);
|
|
877
|
+
expect(values.length).toBe(50);
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
test('applySkew: skews', () => {
|
|
881
|
+
const value = optimizedBoxMuller();
|
|
882
|
+
const skewedValue = applySkew(value, .25);
|
|
883
|
+
expect(Math.abs(skewedValue)).toBeLessThanOrEqual(Math.abs(value) + 1);
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
test('mapToRange: works', () => {
|
|
887
|
+
const value = 0;
|
|
888
|
+
const mean = 10;
|
|
889
|
+
const sd = 5;
|
|
890
|
+
const mappedValue = mapToRange(value, mean, sd);
|
|
891
|
+
expect(mappedValue).toBe(10);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
test('weighArray: works', () => {
|
|
895
|
+
const arr = ['a', 'b', 'c'];
|
|
896
|
+
const weightedArr = weighArray(arr);
|
|
897
|
+
expect(weightedArr.length).toBeGreaterThanOrEqual(arr.length);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
test('weighFunnels: works', () => {
|
|
901
|
+
const acc = [];
|
|
902
|
+
const funnel = { weight: 3 };
|
|
903
|
+
const result = weighFunnels(acc, funnel);
|
|
904
|
+
expect(result.length).toBe(3);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
});
|