@depup/artillery 2.0.30-depup.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.
Files changed (90) hide show
  1. package/README.md +63 -0
  2. package/bin/run +29 -0
  3. package/bin/run.cmd +3 -0
  4. package/changes.json +138 -0
  5. package/console-reporter.js +1 -0
  6. package/lib/artillery-global.js +33 -0
  7. package/lib/cli/banner.js +8 -0
  8. package/lib/cli/common-flags.js +80 -0
  9. package/lib/cli/hooks/version.js +20 -0
  10. package/lib/cmds/dino.js +109 -0
  11. package/lib/cmds/quick.js +122 -0
  12. package/lib/cmds/report.js +34 -0
  13. package/lib/cmds/run-aci.js +91 -0
  14. package/lib/cmds/run-fargate.js +192 -0
  15. package/lib/cmds/run-lambda.js +96 -0
  16. package/lib/cmds/run.js +671 -0
  17. package/lib/console-capture.js +92 -0
  18. package/lib/console-reporter.js +438 -0
  19. package/lib/create-bom/built-in-plugins.js +12 -0
  20. package/lib/create-bom/create-bom.js +301 -0
  21. package/lib/dispatcher.js +9 -0
  22. package/lib/dist.js +222 -0
  23. package/lib/index.js +5 -0
  24. package/lib/launch-platform.js +439 -0
  25. package/lib/load-plugins.js +113 -0
  26. package/lib/platform/aws/aws-cloudwatch.js +106 -0
  27. package/lib/platform/aws/aws-create-sqs-queue.js +58 -0
  28. package/lib/platform/aws/aws-ensure-s3-bucket-exists.js +78 -0
  29. package/lib/platform/aws/aws-get-account-id.js +26 -0
  30. package/lib/platform/aws/aws-get-bucket-region.js +18 -0
  31. package/lib/platform/aws/aws-get-credentials.js +28 -0
  32. package/lib/platform/aws/aws-get-default-region.js +26 -0
  33. package/lib/platform/aws/aws-whoami.js +15 -0
  34. package/lib/platform/aws/constants.js +7 -0
  35. package/lib/platform/aws/iam-cf-templates/aws-iam-fargate-cf-template.yml +219 -0
  36. package/lib/platform/aws/iam-cf-templates/aws-iam-lambda-cf-template.yml +125 -0
  37. package/lib/platform/aws/iam-cf-templates/gh-oidc-fargate.yml +241 -0
  38. package/lib/platform/aws/iam-cf-templates/gh-oidc-lambda.yml +153 -0
  39. package/lib/platform/aws-ecs/ecs.js +247 -0
  40. package/lib/platform/aws-ecs/legacy/aws-util.js +134 -0
  41. package/lib/platform/aws-ecs/legacy/bom.js +528 -0
  42. package/lib/platform/aws-ecs/legacy/constants.js +27 -0
  43. package/lib/platform/aws-ecs/legacy/create-s3-client.js +24 -0
  44. package/lib/platform/aws-ecs/legacy/create-test.js +247 -0
  45. package/lib/platform/aws-ecs/legacy/errors.js +34 -0
  46. package/lib/platform/aws-ecs/legacy/find-public-subnets.js +149 -0
  47. package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-inspect-script/index.js +27 -0
  48. package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/azure-aqs.js +80 -0
  49. package/lib/platform/aws-ecs/legacy/plugins/artillery-plugin-sqs-reporter/index.js +202 -0
  50. package/lib/platform/aws-ecs/legacy/plugins.js +16 -0
  51. package/lib/platform/aws-ecs/legacy/run-cluster.js +1994 -0
  52. package/lib/platform/aws-ecs/legacy/sqs-reporter.js +401 -0
  53. package/lib/platform/aws-ecs/legacy/tags.js +22 -0
  54. package/lib/platform/aws-ecs/legacy/test-run-status.js +9 -0
  55. package/lib/platform/aws-ecs/legacy/time.js +67 -0
  56. package/lib/platform/aws-ecs/legacy/util.js +97 -0
  57. package/lib/platform/aws-ecs/worker/Dockerfile +64 -0
  58. package/lib/platform/aws-ecs/worker/helpers.sh +80 -0
  59. package/lib/platform/aws-ecs/worker/loadgen-worker +656 -0
  60. package/lib/platform/aws-lambda/dependencies.js +130 -0
  61. package/lib/platform/aws-lambda/index.js +734 -0
  62. package/lib/platform/aws-lambda/lambda-handler/a9-handler-dependencies.js +73 -0
  63. package/lib/platform/aws-lambda/lambda-handler/a9-handler-helpers.js +43 -0
  64. package/lib/platform/aws-lambda/lambda-handler/a9-handler-index.js +235 -0
  65. package/lib/platform/aws-lambda/lambda-handler/package.json +15 -0
  66. package/lib/platform/aws-lambda/prices.js +29 -0
  67. package/lib/platform/az/aci.js +694 -0
  68. package/lib/platform/az/aqs-queue-consumer.js +88 -0
  69. package/lib/platform/az/regions.js +52 -0
  70. package/lib/platform/cloud/api.js +72 -0
  71. package/lib/platform/cloud/cloud.js +448 -0
  72. package/lib/platform/cloud/http-client.js +19 -0
  73. package/lib/platform/local/artillery-worker-local.js +154 -0
  74. package/lib/platform/local/index.js +174 -0
  75. package/lib/platform/local/worker.js +261 -0
  76. package/lib/platform/worker-states.js +13 -0
  77. package/lib/queue-consumer/index.js +56 -0
  78. package/lib/stash.js +41 -0
  79. package/lib/telemetry.js +78 -0
  80. package/lib/util/await-on-ee.js +24 -0
  81. package/lib/util/generate-id.js +9 -0
  82. package/lib/util/parse-tag-string.js +21 -0
  83. package/lib/util/prepare-test-execution-plan.js +216 -0
  84. package/lib/util/sleep.js +7 -0
  85. package/lib/util/validate-script.js +132 -0
  86. package/lib/util.js +294 -0
  87. package/lib/utils-config.js +31 -0
  88. package/package.json +323 -0
  89. package/types.d.ts +317 -0
  90. package/util.js +1 -0
@@ -0,0 +1,301 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
+
5
+ // TODO: async-ify this
6
+
7
+ const path = require('node:path');
8
+ const fs = require('node:fs');
9
+ const A = require('async');
10
+ const debug = require('debug')('bom');
11
+ const _ = require('lodash');
12
+ const Table = require('cli-table3');
13
+ const { getCustomJsDependencies } = require('../platform/aws-ecs/legacy/bom');
14
+
15
+ const { readScript, parseScript } = require('../util');
16
+
17
+ const BUILTIN_PLUGINS = require('./built-in-plugins');
18
+
19
+ // NOTE: Presumes ALL paths are absolute.
20
+ async function createBOM(absoluteScriptPath, extraFiles, opts, callback) {
21
+ A.waterfall(
22
+ [
23
+ A.constant(absoluteScriptPath),
24
+ readScript,
25
+ parseScript,
26
+ (scriptData, next) => {
27
+ return next(null, {
28
+ opts: {
29
+ scriptData,
30
+ absoluteScriptPath
31
+ },
32
+ localFilePaths: [absoluteScriptPath],
33
+ npmModules: []
34
+ });
35
+ },
36
+ getPlugins,
37
+ getCustomEngines,
38
+ getCustomJsDependencies,
39
+ getVariableDataFiles,
40
+ // getFileUploadPluginFiles,
41
+ getExtraFiles
42
+ // expandDirectories
43
+ ],
44
+
45
+ (err, context) => {
46
+ if (err) {
47
+ return callback(err, null);
48
+ }
49
+
50
+ context.localFilePaths = context.localFilePaths.concat(extraFiles);
51
+
52
+ // TODO: Entries in localFilePaths may be directories
53
+
54
+ // Handle case with only one entry, where the string itself
55
+ // will be the common prefix, meaning that when we substring() on it later, we'll
56
+ // get an empty string, ending up with a manifest like:
57
+ // { files:
58
+ // [ { orig: '/Users/h/tmp/artillery/hello.yaml', noPrefix: '' } ],
59
+ // modules: [] }
60
+ //
61
+ let prefix = '';
62
+ if (context.localFilePaths.length === 1) {
63
+ prefix = context.localFilePaths[0].substring(
64
+ 0,
65
+ context.localFilePaths[0].length -
66
+ path.basename(context.localFilePaths[0]).length
67
+ );
68
+
69
+ // This may still be an empty string if the script path is just 'hello.yml':
70
+ prefix = prefix.length === 0 ? context.localFilePaths[0] : prefix;
71
+ } else {
72
+ prefix = commonPrefix(context.localFilePaths);
73
+ }
74
+
75
+ debug('prefix', prefix);
76
+
77
+ //
78
+ // include package.json / package-lock.json / yarn.lock
79
+ //
80
+ let packageDescriptionFiles = ['.npmrc'];
81
+ if (opts.packageJsonPath) {
82
+ packageDescriptionFiles.push(opts.packageJsonPath);
83
+ } else {
84
+ packageDescriptionFiles = packageDescriptionFiles.concat([
85
+ 'package.json',
86
+ 'package-lock.json',
87
+ 'yarn.lock'
88
+ ]);
89
+ }
90
+ const dependencyFiles = packageDescriptionFiles.map((s) =>
91
+ path.join(prefix, s)
92
+ );
93
+
94
+ debug(dependencyFiles);
95
+
96
+ dependencyFiles.forEach((p) => {
97
+ try {
98
+ if (fs.statSync(p)) {
99
+ context.localFilePaths.push(p);
100
+ }
101
+ } catch (_ignoredErr) {}
102
+ });
103
+
104
+ const files = context.localFilePaths.map((p) => {
105
+ return { orig: p, noPrefix: p.substring(prefix.length, p.length) };
106
+ });
107
+
108
+ const pkgPath = _.find(files, (f) => {
109
+ return f.noPrefix === 'package.json';
110
+ });
111
+
112
+ if (pkgPath) {
113
+ const pkg = JSON.parse(fs.readFileSync(pkgPath.orig, 'utf8'));
114
+ const pkgDeps = [].concat(
115
+ Object.keys(pkg.dependencies || {}),
116
+ Object.keys(pkg.devDependencies || {})
117
+ );
118
+ context.pkgDeps = pkgDeps;
119
+ context.npmModules = _.uniq(context.npmModules.concat(pkgDeps)).sort();
120
+ } else {
121
+ context.pkgDeps = [];
122
+ }
123
+
124
+ return callback(null, {
125
+ files: _.uniqWith(files, _.isEqual),
126
+ modules: _.uniq(context.npmModules).filter(
127
+ (m) =>
128
+ m !== 'artillery' &&
129
+ m !== 'playwright' &&
130
+ !m.startsWith('@playwright/')
131
+ ),
132
+ pkgDeps: context.pkgDeps
133
+ });
134
+ }
135
+ );
136
+ }
137
+
138
+ function getPlugins(context, next) {
139
+ const environmentPlugins = _.reduce(
140
+ _.get(context, 'opts.scriptData.config.environments', {}),
141
+ function getEnvironmentPlugins(acc, envSpec, _envName) {
142
+ acc = acc.concat(Object.keys(envSpec.plugins || []));
143
+ return acc;
144
+ },
145
+ []
146
+ );
147
+ const pluginNames = Object.keys(
148
+ _.get(context, 'opts.scriptData.config.plugins', {})
149
+ ).concat(environmentPlugins);
150
+
151
+ const pluginPackages = _.uniq(
152
+ pluginNames
153
+ .filter((p) => BUILTIN_PLUGINS.indexOf(p) === -1)
154
+ .map((p) => `artillery-plugin-${p}`)
155
+ );
156
+
157
+ debug(pluginPackages);
158
+ context.npmModules = context.npmModules.concat(pluginPackages);
159
+
160
+ return next(null, context);
161
+ }
162
+
163
+ function getCustomEngines(context, next) {
164
+ // TODO: Environment-specific engines (see getPlugins())
165
+ const engineNames = _.uniq(
166
+ Object.keys(_.get(context, 'opts.scriptData.config.engines', {}))
167
+ );
168
+ const enginePackages = engineNames.map((x) => `artillery-engine-${x}`);
169
+ context.npmModules = context.npmModules.concat(enginePackages);
170
+
171
+ return next(null, context);
172
+ }
173
+
174
+ function getVariableDataFiles(context, next) {
175
+ // NOTE: Presuming that the script has been run through the functions
176
+ // that normalize the config.payload definition (presume it's an array).
177
+ // Also assuming that context.opts.scriptData contains both the config and
178
+ // the scenarios section.
179
+
180
+ // Iterate over environments
181
+
182
+ function resolvePayloadPaths(obj) {
183
+ const result = [];
184
+ if (obj.payload) {
185
+ if (_.isArray(obj.payload)) {
186
+ obj.payload.forEach((payloadSpec) => {
187
+ result.push(
188
+ path.resolve(
189
+ path.dirname(context.opts.absoluteScriptPath),
190
+ payloadSpec.path
191
+ )
192
+ );
193
+ });
194
+ } else if (_.isObject(obj.payload)) {
195
+ // NOTE: isObject returns true for arrays, so this branch must
196
+ // come second.
197
+ result.push(
198
+ path.resolve(
199
+ path.dirname(context.opts.absoluteScriptPath),
200
+ obj.payload.path
201
+ )
202
+ );
203
+ }
204
+ }
205
+ return result;
206
+ }
207
+
208
+ context.localFilePaths = context.localFilePaths.concat(
209
+ resolvePayloadPaths(context.opts.scriptData.config)
210
+ );
211
+ context.opts.scriptData.config.environments =
212
+ context.opts.scriptData.config.environments || {};
213
+ Object.keys(context.opts.scriptData.config.environments).forEach(
214
+ (envName) => {
215
+ const envSpec = context.opts.scriptData.config.environments[envName];
216
+ context.localFilePaths = context.localFilePaths.concat(
217
+ resolvePayloadPaths(envSpec)
218
+ );
219
+ }
220
+ );
221
+ return next(null, context);
222
+ }
223
+
224
+ function getExtraFiles(context, next) {
225
+ if (
226
+ context.opts.scriptData.config?.includeFiles
227
+ ) {
228
+ const absPaths = _.map(context.opts.scriptData.config.includeFiles, (p) => {
229
+ const includePath = path.resolve(
230
+ path.dirname(context.opts.absoluteScriptPath),
231
+ p
232
+ );
233
+ debug('includeFile:', includePath);
234
+ return includePath;
235
+ });
236
+ context.localFilePaths = context.localFilePaths.concat(absPaths);
237
+ return next(null, context);
238
+ } else {
239
+ return next(null, context);
240
+ }
241
+ }
242
+
243
+ function commonPrefix(paths, separator) {
244
+ if (
245
+ !paths ||
246
+ paths.length === 0 ||
247
+ paths.filter((s) => typeof s !== 'string').length > 0
248
+ ) {
249
+ return '';
250
+ }
251
+
252
+ if (paths.includes('/')) {
253
+ return '/';
254
+ }
255
+
256
+ const sep = separator ? separator : path.sep;
257
+
258
+ const splitPaths = paths.map((p) => p.split(sep));
259
+ const shortestPath = splitPaths.reduce((a, b) => {
260
+ return a.length < b.length ? a : b;
261
+ }, splitPaths[0]);
262
+
263
+ let furthestIndex = shortestPath.length;
264
+
265
+ for (const p of splitPaths) {
266
+ for (let i = 0; i < furthestIndex; i++) {
267
+ if (p[i] !== shortestPath[i]) {
268
+ furthestIndex = i;
269
+ break;
270
+ }
271
+ }
272
+ }
273
+
274
+ const joined = shortestPath.slice(0, furthestIndex).join(sep);
275
+
276
+ if (joined.length > 0) {
277
+ // Check if joined path already ends with separator which
278
+ // will happen when input is a root drive on Windows, e.g. "C:\"
279
+ return joined.endsWith(sep) ? joined : joined + sep;
280
+ } else {
281
+ return '';
282
+ }
283
+ }
284
+
285
+ function prettyPrint(manifest) {
286
+ const t = new Table({ head: ['Name', 'Type', 'Notes'] });
287
+ for (const f of manifest.files) {
288
+ t.push([f.noPrefix, 'file']);
289
+ }
290
+ for (const m of manifest.modules) {
291
+ t.push([
292
+ m,
293
+ 'package',
294
+ manifest.pkgDeps.indexOf(m) === -1 ? 'not in package.json' : ''
295
+ ]);
296
+ }
297
+ artillery.log(t.toString());
298
+ artillery.log();
299
+ }
300
+
301
+ module.exports = { createBOM, commonPrefix, prettyPrint };
@@ -0,0 +1,9 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
+
5
+
6
+
7
+ const core = require('@artilleryio/int-core');
8
+
9
+ module.exports = core;
package/lib/dist.js ADDED
@@ -0,0 +1,222 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
+
5
+
6
+
7
+ const assert = require('node:assert');
8
+ const L = require('lodash');
9
+ const isIdlePhase = require('@artilleryio/int-core').isIdlePhase;
10
+
11
+ module.exports = divideWork;
12
+
13
+ /**
14
+ *
15
+ * Create a number of scripts for workers from the script given to use by user.
16
+ *
17
+ * @param {Script} script
18
+ * @param {number} numWorkers
19
+ * @returns {Script[]} array of scripts distributed representing the work for each worker
20
+ *
21
+ * @todo: Distribute payload data to workers
22
+ */
23
+ function divideWork(script, numWorkers) {
24
+ const workerScripts = createWorkerScriptBases(numWorkers, script);
25
+ for (const phase of script.config.phases) {
26
+ // switching on phase type to determine how to distribute work
27
+ switch (true) {
28
+ case !!phase.rampTo: {
29
+ handleRampToPhase(phase, numWorkers, workerScripts);
30
+ break;
31
+ }
32
+ case !!phase.arrivalRate: {
33
+ handleArrivalRatePhase(phase, numWorkers, workerScripts);
34
+ break;
35
+ }
36
+ case !!phase.arrivalCount: {
37
+ // arrivalCount is executed in the first worker
38
+ // and replaced with a `pause` phase in the others
39
+ handleArrivalCountPhase(workerScripts, phase, numWorkers);
40
+ break;
41
+ }
42
+ case !!phase.pause: {
43
+ // nothing to adjust here, pause is executed in all workers
44
+ for (let i = 0; i < numWorkers; i++) {
45
+ workerScripts[i].config.phases.push(L.cloneDeep(phase));
46
+ }
47
+ break;
48
+ }
49
+ default: {
50
+ console.log(
51
+ 'Unknown phase spec definition, skipping.\n%j\n' +
52
+ 'This should not happen',
53
+ phase
54
+ );
55
+ }
56
+ }
57
+ }
58
+
59
+ // Filter out scripts which have only idle phases
60
+ const result = workerScripts.filter(
61
+ (workerScript) => !workerScript.config.phases.every(isIdlePhase)
62
+ );
63
+
64
+ // Add worker and totalWorkers properties to phases
65
+ const hasPayload = scriptHasPayload(script);
66
+ for (let i = 0; i < result.length; i++) {
67
+ for (const phase of result[i].config.phases) {
68
+ phase.totalWorkers = result.length;
69
+ phase.worker = i + 1;
70
+ }
71
+
72
+ // Distribute payload data to workers
73
+ if (hasPayload) {
74
+ for (
75
+ let payloadIdx = 0;
76
+ payloadIdx < script.config.payload.length;
77
+ payloadIdx++
78
+ ) {
79
+ // If there are more workers than payload data, then we will repeat the payload data
80
+ const scriptPayloadData = script.config.payload[payloadIdx].data;
81
+ const idxToMatch = i % scriptPayloadData.length;
82
+ result[i].config.payload[payloadIdx].data = scriptPayloadData.filter(
83
+ (_, index) => index % result.length === idxToMatch
84
+ );
85
+ }
86
+ }
87
+ }
88
+
89
+ return result;
90
+ }
91
+
92
+ function scriptHasPayload(script) {
93
+ return script.config.payload && script.config.payload.length > 0;
94
+ }
95
+
96
+ function handleArrivalCountPhase(workerScripts, phase, numWorkers) {
97
+ workerScripts[0].config.phases.push(L.cloneDeep(phase));
98
+
99
+ for (let i = 1; i < numWorkers; i++) {
100
+ workerScripts[i].config.phases.push({
101
+ name: phase.name,
102
+ pause: phase.duration
103
+ });
104
+ }
105
+ }
106
+
107
+ function handleArrivalRatePhase(phase, numWorkers, workerScripts) {
108
+ const rates = distribute(phase.arrivalRate, numWorkers);
109
+ const activeWorkers = rates.reduce(
110
+ (acc, rate) => acc + (rate > 0 ? 1 : 0),
111
+ 0
112
+ );
113
+ const maxVusers = phase.maxVusers
114
+ ? distribute(phase.maxVusers, activeWorkers)
115
+ : false;
116
+ for (let i = 0; i < numWorkers; i++) {
117
+ const newPhase = L.cloneDeep(phase);
118
+ newPhase.arrivalRate = rates[i];
119
+ if (maxVusers) {
120
+ newPhase.maxVusers = maxVusers[i];
121
+ }
122
+ workerScripts[i].config.phases.push(newPhase);
123
+ }
124
+ }
125
+
126
+ function handleRampToPhase(phase, numWorkers, workerScripts) {
127
+ phase.arrivalRate = phase.arrivalRate || 0;
128
+
129
+ const rate = phase.arrivalRate / numWorkers;
130
+ const ramp = phase.rampTo / numWorkers;
131
+ const activeWorkers = rate > 0 || ramp > 0 ? numWorkers : 0;
132
+ const maxVusers = phase.maxVusers
133
+ ? distribute(phase.maxVusers, activeWorkers)
134
+ : false;
135
+
136
+ for (let i = 0; i < numWorkers; i++) {
137
+ const newPhase = L.cloneDeep(phase);
138
+ newPhase.arrivalRate = rate;
139
+ newPhase.rampTo = ramp;
140
+ if (maxVusers) {
141
+ newPhase.maxVusers = maxVusers[i];
142
+ }
143
+ workerScripts[i].config.phases.push(newPhase);
144
+ }
145
+ }
146
+
147
+ function createWorkerScriptBases(numWorkers, script) {
148
+ const bases = [];
149
+ for (let i = 0; i < numWorkers; i++) {
150
+ const newScript = L.cloneDeep({
151
+ ...script,
152
+ config: {
153
+ ...script.config,
154
+ phases: [],
155
+ ...(scriptHasPayload(script) && {
156
+ payload: script.config.payload.map((payload) => {
157
+ return {
158
+ ...payload,
159
+ data: []
160
+ };
161
+ })
162
+ })
163
+ }
164
+ });
165
+ // 'before' and 'after' hooks are executed in the main thread
166
+ delete newScript.before;
167
+ delete newScript.after;
168
+
169
+ bases.push(newScript);
170
+ }
171
+ return bases;
172
+ }
173
+
174
+ function distribute(m, n) {
175
+ m = Number(m);
176
+ n = Number(n);
177
+
178
+ const result = [];
179
+
180
+ if (m < n) {
181
+ for (let i = 0; i < n; i++) {
182
+ result.push(i < m ? 1 : 0);
183
+ }
184
+ } else {
185
+ const baseCount = Math.floor(m / n);
186
+ let extraItems = m % n;
187
+ for (let i = 0; i < n; i++) {
188
+ result.push(baseCount);
189
+ if (extraItems > 0) {
190
+ result[i]++;
191
+ extraItems--;
192
+ }
193
+ }
194
+ }
195
+ assert(m === sum(result), `${m} === ${sum(result)}`);
196
+ return result;
197
+ }
198
+
199
+ function sum(a) {
200
+ let result = 0;
201
+ for (let i = 0; i < a.length; i++) {
202
+ result += a[i];
203
+ }
204
+ return result;
205
+ }
206
+
207
+ if (require.main === module) {
208
+ console.log(distribute(1, 4));
209
+ console.log(distribute(1, 10));
210
+ console.log(distribute(4, 4));
211
+ console.log(distribute(87, 4));
212
+ console.log(distribute(50, 8));
213
+ console.log(distribute(39, 20));
214
+ console.log(distribute(20, 4));
215
+ console.log(distribute(19, 4));
216
+ console.log(distribute(20, 3));
217
+ console.log(distribute(61, 4));
218
+ console.log(distribute(121, 4));
219
+ console.log(distribute(32, 3));
220
+ console.log(distribute(700, 31));
221
+ console.log(distribute(700, 29));
222
+ }
package/lib/index.js ADDED
@@ -0,0 +1,5 @@
1
+ const { getStash } = require('./stash');
2
+
3
+ module.exports = {
4
+ getStash
5
+ };