diff --git a/README.md b/README.md index 65d5d5b..6c1888b 100644 --- a/README.md +++ b/README.md @@ -7,34 +7,34 @@ ## Usage ```js -import { Recordable } from './index.js' +import { Recordable } from '@nicholaswmin/recordable' const task = new Recordable() -task.record(1) -task.record(100) +task.record(1) for (let i = 0; i < 600; i++) task.record(Math.round(Math.random() * 20) + 1) +task.record(100) console.log(task.min) // 3.05 ms console.log(task.mean) -// 11.42 ms +// 23.42 ms console.log(task.max) -// 85.17 m +// 85.17 ms console.log(task.stddev) -// 5.17 ms +// 15.17 ms ``` ### Plotting ```js const task = new Recordable() -task.record(1) -task.record(100) +task.record(1) for (let i = 0; i < 600; i++) task.record(Math.round(Math.random() * 20) + 1) +task.record(100) task.plot() ``` diff --git a/index.js b/index.js index ac9f05b..ea3e354 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,6 @@ +import { StatsView } from './src/stats-view.js' +import { StatsList } from './src/stats-list.js' +import { StatsStore } from './src/stats-store.js' import { Recordable } from './src/recordable.js' -export { Recordable } +export { Recordable, StatsStore, StatsList, StatsView } diff --git a/test/clamped-averages.test.js b/recordable/clamped-averages.test.js similarity index 98% rename from test/clamped-averages.test.js rename to recordable/clamped-averages.test.js index 15c0ae3..4e805e3 100644 --- a/test/clamped-averages.test.js +++ b/recordable/clamped-averages.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' await test('#toClampedAverages(maxLength)', async t => { let recordable, result = null diff --git a/test/constructor.test.js b/recordable/constructor.test.js similarity index 94% rename from test/constructor.test.js rename to recordable/constructor.test.js index 54bd16e..e537081 100644 --- a/test/constructor.test.js +++ b/recordable/constructor.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' await test('#constructor(name)', async t => { let recordable = null diff --git a/test/histogram-values.test.js b/recordable/histogram-values.test.js similarity index 96% rename from test/histogram-values.test.js rename to recordable/histogram-values.test.js index 0caddc0..0c1c29f 100644 --- a/test/histogram-values.test.js +++ b/recordable/histogram-values.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' await test('#recordable.histogram values', async t => { let recordable diff --git a/test/histogram.test.js b/recordable/histogram.test.js similarity index 96% rename from test/histogram.test.js rename to recordable/histogram.test.js index 652343a..8174567 100644 --- a/test/histogram.test.js +++ b/recordable/histogram.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' await test('#historicalMeans', async t => { let recordable, histogram = null diff --git a/recordable/index.test.js b/recordable/index.test.js new file mode 100644 index 0000000..395d28c --- /dev/null +++ b/recordable/index.test.js @@ -0,0 +1,10 @@ +import './constructor.test.js' +import './record-delta.test.js' +import './histogram.test.js' +import './histogram-values.test.js' +import './clamped-averages.test.js' +import './to-json.test.js' +import './record.test.js' +import './reset.test.js' +import './plot.test.js' +import './tick.test.js' diff --git a/recordable/patch-event.test.js b/recordable/patch-event.test.js new file mode 100644 index 0000000..0b6d90e --- /dev/null +++ b/recordable/patch-event.test.js @@ -0,0 +1,39 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#patchEvent', async t => { + let recordable + + t.beforeEach(() => { + recordable = new Recordable({ name: 'foo' }) + }) + + await t.test('a value is recorded', async t => { + let message = null + + t.beforeEach(async () => { + recordable.ee.on('value:recorded', msg => { + message = msg + }) + + recordable.record(20) + + return new Promise(res => setTimeout(res, 50)) + }) + + await t.test('event is fired', async t => { + t.assert.ok(message) + }) + + await t.test('message look ok', async t => { + t.assert.ok(typeof message, 'object') + t.assert.ok(Object.hasOwn(message, 'name')) + t.assert.ok(Object.hasOwn(message, 'val')) + }) + + await t.test('has correct values', async t => { + t.assert.strictEqual(message.name, 'foo') + t.assert.strictEqual(message.val, 20) + }) + }) +}) diff --git a/test/plot.test.js b/recordable/plot.test.js similarity index 93% rename from test/plot.test.js rename to recordable/plot.test.js index 3b6a90d..26591a8 100644 --- a/test/plot.test.js +++ b/recordable/plot.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' await test('#plot()', async t => { let plot = null diff --git a/test/record-delta.test.js b/recordable/record-delta.test.js similarity index 98% rename from test/record-delta.test.js rename to recordable/record-delta.test.js index b87e3f6..4101383 100644 --- a/test/record-delta.test.js +++ b/recordable/record-delta.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' const sleep = (ms = 5) => new Promise(resolve => setTimeout(resolve, ms)) diff --git a/test/record.test.js b/recordable/record.test.js similarity index 96% rename from test/record.test.js rename to recordable/record.test.js index b37aa13..4f65d80 100644 --- a/test/record.test.js +++ b/recordable/record.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' await test('#record(val)', async t => { let recordable diff --git a/recordable/remote-patch.test.js b/recordable/remote-patch.test.js new file mode 100644 index 0000000..9c21e12 --- /dev/null +++ b/recordable/remote-patch.test.js @@ -0,0 +1,63 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#applyRemotePatch', async t => { + let recordable + + t.beforeEach(() => { + recordable = new Recordable({ name: 'foo' }) + }) + + await t.test('name is different', async t => { + await t.test('throws Error', t => { + t.assert.throws(() => { + recordable.applyRemotePatch({ name: 'bar', val: 10 }) + }, { + name: 'RangeError' + }) + }) + }) + + await t.test('value is not a positive integer', async t => { + await t.test('throws Error', t => { + t.assert.throws(() => { + recordable.applyRemotePatch({ name: 'foo', val: 0 }) + }, { + name: 'RangeError' + }) + }) + }) + + await t.test('patch is valid', async t => { + t.beforeEach(() => { + recordable.applyRemotePatch({ name: 'foo', val: 10 }) + recordable.applyRemotePatch({ name: 'foo', val: 20 }) + }) + + await t.test('does not throw error', t => { + t.assert.doesNotThrow(() => { + recordable.applyRemotePatch({ name: 'foo', val: 10 }) + }) + }) + + await t.test('does not emit "value:recorded" event', async t => { + recordable.ee.on('value:recorded', e => { + }) + + recordable.applyRemotePatch({ name: 'foo', val: 10 }) + await new Promise(resolve => setTimeout(resolve, 25)) + }) + + await t.test('increases count', t => { + t.assert.strictEqual(recordable.count, 2) + }) + + await t.test('increases mean', t => { + t.assert.strictEqual(recordable.mean, 15) + }) + + await t.test('adds to items', t => { + t.assert.strictEqual(recordable.values.length, 2) + }) + }) +}) diff --git a/test/reset.test.js b/recordable/reset.test.js similarity index 95% rename from test/reset.test.js rename to recordable/reset.test.js index 4cf2483..38da463 100644 --- a/test/reset.test.js +++ b/recordable/reset.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' await test('#reset()', async t => { let recordable = null diff --git a/test/tick.test.js b/recordable/tick.test.js similarity index 85% rename from test/tick.test.js rename to recordable/tick.test.js index 168c5b1..b9d22df 100644 --- a/test/tick.test.js +++ b/recordable/tick.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' await test('#tick()', async t => { const recordable = new Recordable() diff --git a/test/to-json.test.js b/recordable/to-json.test.js similarity index 97% rename from test/to-json.test.js rename to recordable/to-json.test.js index 0dbb2cc..1c0ef79 100644 --- a/test/to-json.test.js +++ b/recordable/to-json.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' await test('#record(val)', async t => { await t.test('reimporting its JSON revives it to same state', async t => { diff --git a/src/colorlist.js b/src/colorlist.js new file mode 100644 index 0000000..11b8b53 --- /dev/null +++ b/src/colorlist.js @@ -0,0 +1,21 @@ +const colorlist = { + black: '\x1B[30m', + red: '\x1B[31m', + green: '\x1B[32m', + yellow: '\x1B[33m', + blue: '\x1B[34m', + magenta: '\x1B[35m', + cyan: '\x1B[36m', + lightgray: '\x1B[37m', + default: '\x1B[39m', + darkgray: '\x1B[90m', + lightred: '\x1B[91m', + lightgreen: '\x1B[92m', + lightyellow: '\x1B[93m', + lightblue: '\x1B[94m', + lightmagenta: '\x1B[95m', + lightcyan: '\x1B[96m', + white: '\x1B[97m' +} + +export default colorlist diff --git a/src/recordable.js b/src/recordable.js index 3913bc6..1f58193 100644 --- a/src/recordable.js +++ b/src/recordable.js @@ -41,9 +41,9 @@ class Recordable { }) }) - this._recordFn = this.histogram.record.bind(this.histogram) + this.histogramRecord = this.histogram.record.bind(this.histogram) this.histogram.record = val => { - const result = this._recordFn(val) + const result = this.histogramRecord(val) this.values.push(val) diff --git a/src/stats-list.js b/src/stats-list.js new file mode 100644 index 0000000..7fce5d4 --- /dev/null +++ b/src/stats-list.js @@ -0,0 +1,57 @@ +class StatsList { + constructor({ + title = '', subtitle = '', maxRows = 5, + sort = function(a, b) { a - b }, + fields = [ + ['foo.mean', 'Mean Task Duration (ms)'], + ['bar.count', 'Total Count'] + ] + } = {}, statsStore = null) { + this.statsStore = statsStore + + this.title = title + this.subtitle = subtitle + this.maxRows = maxRows + this.fields = fields + + this.rows = [] + this.sort = sort + } + + render() { + const rows = this.statsStore + ? [this.statsStore.getRow()] + : this.rows.slice(this.rows.length - this.maxRows, this.rows.length) + + const values = this.rows + .sort(this.sort) + .slice(this.rows.length - this.maxRows, this.rows.length) + .map(row => { + return this.fields.reduce((acc, field) => { + const split = field[0].split('.') + return { + ...acc, + [field[1]] : row[2] + ? row[2](row[split[0]][split[1]]) + : row[split[0]][split[1]] + } + }, {}) + }) + + this.title + ? console.log('Title:', this.title) + : null + + console.table(values) + + this.subtitle + ? console.log(this.subtitle) + : null + } + + append(row) { + this.rows.push(row) + } +} + +export { StatsList } diff --git a/src/stats-store.js b/src/stats-store.js new file mode 100644 index 0000000..5358e29 --- /dev/null +++ b/src/stats-store.js @@ -0,0 +1,23 @@ +import { Recordable } from './recordable.js' + +class StatsStore { + constructor(...args) { + Object.assign(this, args.reduce((acc, name) => { + return { ...acc, [name]: new Recordable({ name }) } + }, {})) + } + + getMembers() { + return Object.values(this).filter(value => value instanceof Recordable) + } + + getRow() { + return this.getMembers().reduce((acc, member) => { + return { + ...acc, [member.name]: member.histogram.toJSON() + } + }, { id: process.pid }) + } +} + +export { StatsStore } diff --git a/src/stats-view.js b/src/stats-view.js new file mode 100644 index 0000000..70c291e --- /dev/null +++ b/src/stats-view.js @@ -0,0 +1,36 @@ +class StatsView { + constructor(lists, { + title = '', + subtitle = '', + updateInterval = 500 + } = {}) { + this.title = title + this.subtitle = subtitle + this.lists = lists + + this.timer = setInterval(this.render.bind(this), updateInterval) + } + + render() { + console.clear() + + console.log('\n') + + this.title + ? console.log('Title:', this.title) + : null + + this.lists.forEach(list => { + list.render() + console.log('\n') + }) + + console.log('\n') + } + + stop() { + clearInterval(this.timer) + } +} + +export { StatsView } diff --git a/test/index.test.js b/test/index.test.js index 395d28c..647f4f0 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,10 +1 @@ -import './constructor.test.js' -import './record-delta.test.js' -import './histogram.test.js' -import './histogram-values.test.js' -import './clamped-averages.test.js' -import './to-json.test.js' -import './record.test.js' -import './reset.test.js' -import './plot.test.js' -import './tick.test.js' +import './recordable/index.test.js' diff --git a/test/recordable/clamped-averages.test.js b/test/recordable/clamped-averages.test.js new file mode 100644 index 0000000..4e805e3 --- /dev/null +++ b/test/recordable/clamped-averages.test.js @@ -0,0 +1,87 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#toClampedAverages(maxLength)', async t => { + let recordable, result = null + + t.beforeEach(() => { + recordable = new Recordable() + }) + + await t.test('passed "maxLength"', async t => { + await t.test('is not a positive integer', async t => { + await t.test('throws an error', t => { + t.assert.throws(() => { + recordable.toClampedAverages(0.01) + }, { name: 'RangeError' }) + }) + }) + + await t.test('is valid', async t => { + await t.test('has no values', async t => { + result = recordable.toClampedAverages(10) + + await t.test('returns empty array', async t => { + t.assert.strictEqual(result.length, 0) + }) + }) + + await t.test('has 1 value', async t => { + t.beforeEach(() => { + recordable.record(15) + result = recordable.toClampedAverages(10) + }) + + await t.test('returns that 1 value', async t => { + t.assert.strictEqual(result.length, 1) + t.assert.strictEqual(result.at(0), 15) + }) + }) + + await t.test('is less than total count of values', async t => { + t.beforeEach(() => { + for (let i = 1; i < 1001; i++) + recordable.record(i) + + result = recordable.toClampedAverages(10) + }) + + await t.test('returns count around maxLength', async t => { + t.assert.strictEqual(result.length, 10) + + await t.test('includes the first value', t => { + t.assert.strictEqual(result[0], 1) + }) + + await t.test('includes the last value', t => { + t.assert.strictEqual(result.at(-1), 1000) + }) + + await t.test('includes the in-between as means of values', t => { + t.assert.strictEqual(result[1], 50.5) + t.assert.strictEqual(result[4], 350.5) + t.assert.strictEqual(result[8], 750.5) + }) + }) + }) + + await t.test('is more than total count of values', async t => { + t.beforeEach(() => { + result = recordable.toClampedAverages(2000) + + for (let i = 1; i < 5 + 1; i++) + recordable.record(i) + }) + + await t.test('returns all the values', async t => { + t.assert.strictEqual(result.length, 5) + + await t.test('unchanged', t => { + t.assert.strictEqual(result.at(0), 1) + t.assert.strictEqual(result.at(-1), 5) + }) + }) + }) + }) + }) +}) diff --git a/test/recordable/constructor.test.js b/test/recordable/constructor.test.js new file mode 100644 index 0000000..e537081 --- /dev/null +++ b/test/recordable/constructor.test.js @@ -0,0 +1,30 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#constructor(name)', async t => { + let recordable = null + + await t.test('"name" parameter', async t => { + await t.test('is present & valid', async t => { + t.beforeEach(() => { + recordable = new Recordable({ name: 'foo' }) + }) + + await t.test('instantiates', async t => { + t.assert.ok(recordable) + + await t.test('with the name', t => { + t.assert.strictEqual(recordable.name, 'foo') + }) + }) + }) + + await t.test('is not present', async t => { + await t.test('does not throw', t => { + t.assert.doesNotThrow(() => { + recordable = new Recordable() + }) + }) + }) + }) +}) diff --git a/test/recordable/histogram-values.test.js b/test/recordable/histogram-values.test.js new file mode 100644 index 0000000..0c1c29f --- /dev/null +++ b/test/recordable/histogram-values.test.js @@ -0,0 +1,45 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#recordable.histogram values', async t => { + let recordable + + await t.test('object has recorded values', async t => { + t.beforeEach(() => { + recordable = new Recordable() + + recordable.record(10) + recordable.record(20) + recordable.record(30) + recordable.record(40) + recordable.record(50) + }) + + await t.test('contains properties in milliseconds', async t => { + await t.test('a min value in ms', t => { + t.assert.strictEqual(recordable.min, 10) + }) + + await t.test('a mean value in ms', t => { + t.assert.strictEqual(recordable.mean, 30) + }) + + await t.test('a max value in ms', t => { + t.assert.strictEqual(recordable.max, 50) + }) + + await t.test('a stddev value in ms', t => { + t.assert.ok(recordable.stddev > 10, recordable.stddev) + t.assert.ok(recordable.stddev < 20, recordable.stddev) + }) + + await t.test('percentiles in ms', async t => { + t.assert.ok(Object.hasOwn(recordable, 'percentiles')) + + await t.test('percentiles in ms', t => { + t.assert.ok(recordable.percentiles['100']) + }) + }) + }) + }) +}) diff --git a/test/recordable/histogram.test.js b/test/recordable/histogram.test.js new file mode 100644 index 0000000..8174567 --- /dev/null +++ b/test/recordable/histogram.test.js @@ -0,0 +1,53 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#historicalMeans', async t => { + let recordable, histogram = null + + t.beforeEach(() => { + recordable = new Recordable() + }) + + await t.test('instantiates', t => { + t.assert.ok(recordable, 'is falsy') + }) + + await t.test('"recordable"', async t => { + t.beforeEach(() => { + histogram = recordable.histogram + }) + + await t.test('has a histogram property', t => { + t.assert.ok(Object.hasOwn(recordable, 'histogram'), 'no such property') + }) + + await t.test('of type RecordableHistogram', t => { + t.assert.strictEqual(histogram.constructor.name, 'RecordableHistogram') + }) + + await t.test('#recordable.histogram.record(val)', async t => { + await t.test('when passed an invalid value', async t => { + await t.test('throws an error', t => { + t.assert.throws(() => { + histogram.record('') + }, { name: 'TypeError' }) + }) + }) + + await t.test('when passed a valid numeric value', async t => { + t.beforeEach(() => { + histogram.record(5) + }) + + await t.test('records it', t => { + t.assert.strictEqual(recordable.count, 1) + }) + + await t.test('stores the value', t => { + t.assert.strictEqual(recordable.values.length, 1) + t.assert.ok(recordable.values.includes(5), 'cannot find 5') + }) + }) + }) + }) +}) diff --git a/test/recordable/index.test.js b/test/recordable/index.test.js new file mode 100644 index 0000000..395d28c --- /dev/null +++ b/test/recordable/index.test.js @@ -0,0 +1,10 @@ +import './constructor.test.js' +import './record-delta.test.js' +import './histogram.test.js' +import './histogram-values.test.js' +import './clamped-averages.test.js' +import './to-json.test.js' +import './record.test.js' +import './reset.test.js' +import './plot.test.js' +import './tick.test.js' diff --git a/test/recordable/plot.test.js b/test/recordable/plot.test.js new file mode 100644 index 0000000..26591a8 --- /dev/null +++ b/test/recordable/plot.test.js @@ -0,0 +1,28 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#plot()', async t => { + let plot = null + + t.beforeEach(() => { + const recordable = new Recordable() + + recordable.histogram.record(13) + recordable.histogram.record(20) + recordable.histogram.record(36) + + plot = recordable.plot() + }) + + await t.test('returns a plot', async t => { + t.assert.ok(plot) + }) + + await t.test('plot includes the min', async t => { + t.assert.ok(plot.includes('13.00'), 'plot doesnt plot min value') + }) + + await t.test('plot includes the max', async t => { + t.assert.ok(plot.includes('36.00'), 'plot doesnt plot max value') + }) +}) diff --git a/test/recordable/record-delta.test.js b/test/recordable/record-delta.test.js new file mode 100644 index 0000000..4101383 --- /dev/null +++ b/test/recordable/record-delta.test.js @@ -0,0 +1,83 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +const sleep = (ms = 5) => new Promise(resolve => setTimeout(resolve, ms)) + +await test('#recordDelta()', async t => { + let recordable = null + + t.beforeEach(async () => { + recordable = new Recordable() + }) + + await t.test('When the key', async t => { + await t.test('is set for the first time', async t => { + await t.test('returns 0', t => { + const result = recordable.recordDelta('baz123') + + t.assert.strictEqual(result, 0) + }) + }) + + await t.test('matches with open key', async t => { + await t.test('returns the delta', async t => { + recordable.recordDelta('baz456') + + await sleep(5) + + const result = recordable.recordDelta('baz456') + + t.assert.ok(result > 0) + t.assert.ok(result < 10) + }) + }) + }) + + + await t.test('When key is not passed', async t => { + t.beforeEach(async () => { + recordable.recordDelta('bar') + await sleep(25) + recordable.recordDelta('foo') + await sleep(15) + recordable.recordDelta('foo') + await sleep(5) + recordable.recordDelta('foo') + await sleep(10) + recordable.recordDelta('bar') + }) + + await t.test('matches with previous entries with no key', async t => { + await t.test('recorded 3 values', t => { + t.assert.strictEqual(recordable.values.length, 3) + }) + + await t.test('1st and last values are about ok', t => { + t.assert.ok(recordable.values.at(0) - 15 < 5) // tolerance + t.assert.ok(recordable.values.at(-1) - 55 < 10) // tolerance + }) + }) + }) + + await t.test('When a key is provided', async t => { + t.beforeEach(async () => { + recordable.recordDelta('bar') + recordable.recordDelta('foo') + await sleep(10) + recordable.recordDelta('foo') + await sleep(10) + recordable.recordDelta('foo') + await sleep(5) + recordable.recordDelta('bar') + }) + + await t.test('recorded 4 values', t => { + t.assert.strictEqual(recordable.values.length, 3) + }) + + await t.test('1st and last values are about ok', t => { + t.assert.ok(recordable.values.at(0) - 15 < 5) // tolerance + t.assert.ok(recordable.values.at(-1) - 50 < 10) // tolerance + }) + }) +}) diff --git a/test/recordable/record.test.js b/test/recordable/record.test.js new file mode 100644 index 0000000..4f65d80 --- /dev/null +++ b/test/recordable/record.test.js @@ -0,0 +1,50 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#record(val)', async t => { + let recordable + + t.beforeEach(() => { + recordable = new Recordable() + }) + + await t.test('"value" parameter', async t => { + await t.test('is missing', async t => { + await t.test('throws an error', t => { + t.assert.throws(() => { + recordable.record() + }, { name: 'TypeError' }) + }) + + await t.test('is a float', async t => { + await t.test('throws an error', t => { + t.assert.throws(() => { + recordable.record(0.01) + }, { name: 'RangeError' }) + }) + }) + + await t.test('is a negative integer', async t => { + await t.test('throws an error', t => { + t.assert.throws(() => { + recordable.record(0.01) + }, { name: 'RangeError' }) + }) + }) + }) + + await t.test('is present & valid', async t => { + t.beforeEach(() => { + recordable.record(10) + }) + + await t.test('records its own value', t => { + t.assert.strictEqual(recordable.histogram.count, 1) + }) + + await t.test('in its histogram', t => { + t.assert.strictEqual(recordable.histogram.count, 1) + }) + }) + }) +}) diff --git a/test/recordable/reset.test.js b/test/recordable/reset.test.js new file mode 100644 index 0000000..38da463 --- /dev/null +++ b/test/recordable/reset.test.js @@ -0,0 +1,36 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#reset()', async t => { + let recordable = null + + t.beforeEach(() => { + recordable = new Recordable() + + recordable.histogram.record(10) + recordable.histogram.record(20) + recordable.histogram.record(30) + }) + + await t.test('histogram has recorded values', async t => { + await t.test('histogram has recorded values', t => { + t.assert.strictEqual(recordable.histogram.count, 3) + t.assert.ok(recordable.histogram.mean > 0, 'mean is not > 0') + }) + + await t.test('calling the .reset() method', async t => { + t.beforeEach(() => { + recordable.reset() + }) + + await t.test('resets the recorded values', t => { + t.assert.strictEqual(recordable.histogram.count, 0) + t.assert.strictEqual(recordable.histogram.mean, NaN) + }) + + await t.test('empties the accumulated values', t => { + t.assert.strictEqual(recordable.values.length, 0) + }) + }) + }) +}) diff --git a/test/recordable/tick.test.js b/test/recordable/tick.test.js new file mode 100644 index 0000000..b9d22df --- /dev/null +++ b/test/recordable/tick.test.js @@ -0,0 +1,13 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#tick()', async t => { + const recordable = new Recordable() + + await t.test('records a value of 1', async t => { + recordable.tick() + recordable.tick() + + t.assert.strictEqual(recordable.histogram.count, 2) + }) +}) diff --git a/test/recordable/to-json.test.js b/test/recordable/to-json.test.js new file mode 100644 index 0000000..1c0ef79 --- /dev/null +++ b/test/recordable/to-json.test.js @@ -0,0 +1,60 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#record(val)', async t => { + await t.test('reimporting its JSON revives it to same state', async t => { + let recordable = null + + t.beforeEach(() => { + const instance = new Recordable({ name: 'foo' }) + + for (let i = 1; i < 101; i++) + instance.record(i) + + recordable = new Recordable(JSON.parse(JSON.stringify(instance))) + }) + + await t.test('has same name', async t => { + t.assert.strictEqual(recordable.name, 'foo') + }) + + await t.test('has same values', async t => { + await t.test('has same count of values', async t => { + t.assert.strictEqual(recordable.values.length, 100) + }) + + await t.test('has same 1st value', async t => { + t.assert.strictEqual(recordable.values.at(0), 1) + }) + + await t.test('has same middle value', async t => { + t.assert.strictEqual(recordable.values.at(49), 50) + }) + + await t.test('has same last value', async t => { + t.assert.strictEqual(recordable.values.at(-1), 100) + }) + }) + + await t.test('histogram', async t => { + await t.test('is a RecordableHistogram', async t => { + const type = recordable.histogram.constructor.name + t.assert.strictEqual(type, 'RecordableHistogram') + }) + + await t.test('has same count', async t => { + t.assert.strictEqual(recordable.histogram.count, 100) + }) + + await t.test('has same means', async t => { + t.assert.strictEqual(recordable.histogram.mean, 50.5) + }) + + await t.test('can record()', async t => { + t.assert.doesNotThrow(() => { + recordable.histogram.record(1) + }) + }) + }) + }) +})