From 56ac38f7e4be60772f1dc22fb7911672172ab036 Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Mon, 18 Dec 2023 10:07:46 +1300 Subject: [PATCH] Dust off (some of) the core tests --- packages/core/jest.config.ts | 2 - packages/core/test/bag.test.ts | 149 ++----- packages/core/test/dataflow.test.ts | 369 ++++++++++-------- ...ace.test.ts => dataspace.test.ts.DISABLED} | 0 ...eton.test.ts => skeleton.test.ts.DISABLED} | 0 packages/core/test/test-utils.ts | 38 ++ 6 files changed, 279 insertions(+), 279 deletions(-) rename packages/core/test/{dataspace.test.ts => dataspace.test.ts.DISABLED} (100%) rename packages/core/test/{skeleton.test.ts => skeleton.test.ts.DISABLED} (100%) create mode 100644 packages/core/test/test-utils.ts diff --git a/packages/core/jest.config.ts b/packages/core/jest.config.ts index 415e548..ac0047a 100644 --- a/packages/core/jest.config.ts +++ b/packages/core/jest.config.ts @@ -1,8 +1,6 @@ /// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2016-2023 Tony Garnock-Jones -import 'preserves'; - export default { preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/core/test/bag.test.ts b/packages/core/test/bag.test.ts index cb3e14e..38fd118 100644 --- a/packages/core/test/bag.test.ts +++ b/packages/core/test/bag.test.ts @@ -1,117 +1,50 @@ /// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2016-2023 Tony Garnock-Jones -"use strict"; +import { KeyedSet } from '@preserves/core'; +import { Bag, ChangeDescription } from '../src/runtime/bag'; -const assert = require('assert'); -const Immutable = require('immutable'); -const Bag = require('../src/bag.js'); +describe('bag', () => { + it('should be initializable from a set', () => { + const b = new Bag(new KeyedSet(['a', 'b', 'c'])); + expect(b.size).toBe(3); + expect(b.get('a')).toBe(1); + expect(b.get('z')).toBe(0); + }); -describe('immutable bag', function () { - it('should be initializable from a set', function () { - var b = Bag.fromSet(Immutable.Set(['a', 'b', 'c'])); - assert.strictEqual(b.count(), 3); - assert.strictEqual(Bag.get(b, 'a'), 1); - assert.strictEqual(Bag.get(b, 'z'), 0); - }); + it('should be mutable', () => { + const b = new Bag(); + b.change('a', 1); + b.change('a', 1); + expect(b.get('a')).toBe(2); + }); - it('should be initializable from an array', function () { - var b = Bag.fromSet(['a', 'b', 'c', 'a']); - assert.strictEqual(b.count(), 3); - assert.strictEqual(Bag.get(b, 'a'), 1); - assert.strictEqual(Bag.get(b, 'z'), 0); - }); + it('should count up', () => { + const b = new Bag(); + expect(b.change('a', 1)).toBe(ChangeDescription.ABSENT_TO_PRESENT); + expect(b.change('a', 1)).toBe(ChangeDescription.PRESENT_TO_PRESENT); + expect(b.get('a')).toBe(2); + expect(b.get('z')).toBe(0); + }); - it('should be immutable', function () { - var b = Bag.Bag(); - Bag.change(b, 'a', 1); - Bag.change(b, 'a', 1); - assert(Immutable.is(b, Bag.Bag())); - }); + it('should count down', () => { + const b = new Bag(new KeyedSet(['a'])); + expect(b.change('a', 1)).toBe(ChangeDescription.PRESENT_TO_PRESENT); + expect(b.change('a', -1)).toBe(ChangeDescription.PRESENT_TO_PRESENT); + expect(b.size).toBe(1); + expect(b.change('a', -1)).toBe(ChangeDescription.PRESENT_TO_ABSENT); + expect(b.size).toBe(0); + expect(b.get('a')).toBe(0); + expect(b.get('z')).toBe(0); + expect(b.change('a', -1)).toBe(ChangeDescription.ABSENT_TO_PRESENT); + expect(b.size).toBe(1); + expect(b.get('a')).toBe(-1); + }); - it('should count up', function () { - var b = Bag.Bag(); - var change1, change2; - ({bag: b, net: change1} = Bag.change(b, 'a', 1)); - ({bag: b, net: change2} = Bag.change(b, 'a', 1)); - assert.strictEqual(change1, Bag.ABSENT_TO_PRESENT); - assert.strictEqual(change2, Bag.PRESENT_TO_PRESENT); - assert.strictEqual(Bag.get(b, 'a'), 2); - assert.strictEqual(Bag.get(b, 'z'), 0); - }); - - it('should count down', function () { - var b = Bag.fromSet(['a']); - var c1, c2, c3, c4; - ({bag: b, net: c1} = Bag.change(b, 'a', 1)); - ({bag: b, net: c2} = Bag.change(b, 'a', -1)); - assert.strictEqual(b.count(), 1); - assert.strictEqual(c1, Bag.PRESENT_TO_PRESENT); - assert.strictEqual(c2, Bag.PRESENT_TO_PRESENT); - ({bag: b, net: c3} = Bag.change(b, 'a', -1)); - assert.strictEqual(b.count(), 0); - assert.strictEqual(c3, Bag.PRESENT_TO_ABSENT); - assert.strictEqual(Bag.get(b, 'a'), 0); - assert.strictEqual(Bag.get(b, 'z'), 0); - ({bag: b, net: c4} = Bag.change(b, 'a', -1)); - assert.strictEqual(b.count(), 1); - assert.strictEqual(c4, Bag.ABSENT_TO_PRESENT); - assert.strictEqual(Bag.get(b, 'a'), -1); - }); - - it('should be clamped', function() { - var b = Bag.fromSet(['a']); - ({bag: b} = Bag.change(b, 'a', -1, true)); - ({bag: b} = Bag.change(b, 'a', -1, true)); - ({bag: b} = Bag.change(b, 'a', -1, true)); - ({bag: b} = Bag.change(b, 'a', -1, true)); - assert.strictEqual(b.count(), 0); - assert.strictEqual(Bag.get(b, 'a'), 0); - }); -}); - -describe('mutable bag', function () { - it('should be initializable from a set', function () { - var b = new Bag.MutableBag(Immutable.Set(['a', 'b', 'c'])); - assert.strictEqual(b.count(), 3); - assert.strictEqual(b.get('a'), 1); - assert.strictEqual(b.get('z'), 0); - }); - - it('should be initializable from an array', function () { - var b = new Bag.MutableBag(['a', 'b', 'c', 'a']); - assert.strictEqual(b.count(), 3); - assert.strictEqual(b.get('a'), 1); - assert.strictEqual(b.get('z'), 0); - }); - - it('should be mutable', function () { - var b = new Bag.MutableBag(); - b.change('a', 1); - b.change('a', 1); - assert.strictEqual(b.get('a'), 2); - assert.strictEqual(b.get('z'), 0); - }); - - it('should count up', function () { - var b = new Bag.MutableBag(); - assert.strictEqual(b.change('a', 1), Bag.ABSENT_TO_PRESENT); - assert.strictEqual(b.change('a', 1), Bag.PRESENT_TO_PRESENT); - assert.strictEqual(b.get('a'), 2); - assert.strictEqual(b.get('z'), 0); - }); - - it('should count down', function () { - var b = new Bag.MutableBag(['a']); - assert.strictEqual(b.change('a', 1), Bag.PRESENT_TO_PRESENT); - assert.strictEqual(b.change('a', -1), Bag.PRESENT_TO_PRESENT); - assert.strictEqual(b.count(), 1); - assert.strictEqual(b.change('a', -1), Bag.PRESENT_TO_ABSENT); - assert.strictEqual(b.count(), 0); - assert.strictEqual(b.get('a'), 0); - assert.strictEqual(b.get('z'), 0); - assert.strictEqual(b.change('a', -1), Bag.ABSENT_TO_PRESENT); - assert.strictEqual(b.count(), 1); - assert.strictEqual(b.get('a'), -1); - }); + it('should be clamped', function() { + const b = new Bag(new KeyedSet(['a'])); + for (let i = 0; i < 4; i++) b.change('a', -1, true); + expect(b.size).toBe(0); + expect(b.get('a')).toBe(0); + }); }); diff --git a/packages/core/test/dataflow.test.ts b/packages/core/test/dataflow.test.ts index c8b4860..8b88683 100644 --- a/packages/core/test/dataflow.test.ts +++ b/packages/core/test/dataflow.test.ts @@ -1,197 +1,228 @@ /// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2016-2023 Tony Garnock-Jones -"use strict"; +import { Canonicalizer, embeddedId, FlexSet } from '@preserves/core'; +import { Cell, Field, Graph } from '../src/runtime/dataflow'; +import './test-utils'; -const assert = require('assert'); -var Immutable = require('immutable'); +class CountingField extends Field { + readCount = 0; + writeCount = 0; -var Dataflow = require('../src/dataflow.js'); + get value(): V { + this.readCount++; + return super.value; + } -function Cell(graph, initialValue, name) { - this.objectId = graph.defineObservableProperty(this, 'value', initialValue, { - objectId: name, - noopGuard: (a, b) => a === b - }); + set value(v: V) { + this.writeCount++; + super.value = v; + } + + spy(): V { + // Retrieve the value without bumping the counters! + return this.__value as V; + } + + checkCounts(expectedReadCount: number, expectedWriteCount: number) { + expect(this.readCount).toBe(expectedReadCount); + expect(this.writeCount).toBe(expectedWriteCount); + } } -function DerivedCell(graph, name, valueThunk) { - var c = new Cell(graph, undefined, name); - c.refresh = function () { c.value = valueThunk(); }; - graph.withSubject(c, function () { c.refresh(); }); - return c; +class TestGraph extends Graph { + constructor(subjectIdCanonicalizer: Canonicalizer) { + super(subjectIdCanonicalizer, + Cell.canonicalizer, + g => Array.from(g.values())); + } + + checkDamagedNodes(expectedObjects: FlexSet) { + expect(this.damagedNodes).is(expectedObjects); + } } -function expectSetsEqual(a, bArray) { - assert(Immutable.is(a, Immutable.Set(bArray))); +class BlockGraph extends TestGraph<() => void> { + constructor() { + super(b => '' + embeddedId(b)); + } + + run(subject: () => void): () => void { + this.withSubject(subject, subject); + return subject; + } + + eqn(name: string, f: () => V): CountingField { + const field = new CountingField(this, null!, name); + this.run(() => field.value = f()); + return field; + } } -function checkDamagedNodes(g, expectedObjects) { - expectSetsEqual(g.damagedNodes, expectedObjects); +class StringGraph extends TestGraph { + constructor() { + super(s => s); + } } describe('dataflow', () => { - describe('edges, damage and subjects', () => { - it('should be recorded', () => { - var g = new Dataflow.Graph(); - var c = new Cell(g, 123); + describe('edges, damage and subjects', () => { + it('should be recorded', () => { + const g = new StringGraph(); + const c = new CountingField(g, 123); - g.withSubject('s', () => { c.value; }); - g.withSubject('t', () => { c.value; }); - g.withSubject('s', () => { c.value; }); + g.withSubject('s', () => { c.value; }); + g.withSubject('t', () => { c.value; }); + g.withSubject('s', () => { c.value; }); - c.value = 234; - assert.strictEqual(g.damagedNodes.size, 1); + c.value = 234; + expect(g.damagedNodes.size).toBe(1); - var subjects = Immutable.Set(); - g.repairDamage(function (subjectId) { subjects = subjects.add(subjectId); }); - expectSetsEqual(subjects, ['s', 't']); - }); - }); - - describe('DerivedCell', () => { - describe('simple case', () => { - var g = new Dataflow.Graph(); - var c = DerivedCell(g, 'c', () => 123); - var d = DerivedCell(g, 'd', () => c.value * 2); - it('should be properly initialized', () => { - assert.strictEqual(c.value, 123); - assert.strictEqual(d.value, 246); - }); - it('should lead initially to damaged everything', () => { - assert.strictEqual(g.damagedNodes.size, 2); - }); - it('should repair idempotently after initialization', () => { - g.repairDamage(function (c) { c.refresh(); }); - assert.strictEqual(c.value, 123); - assert.strictEqual(d.value, 246); - }); - it('should be inconsistent after modification but before repair', () => { - c.value = 124; - assert.strictEqual(c.value, 124); - assert.strictEqual(d.value, 246); - }); - it('should repair itself properly', () => { - g.repairDamage(function (c) { c.refresh(); }); - assert.strictEqual(c.value, 124); - assert.strictEqual(d.value, 248); - }); + const subjects = new FlexSet(s => s); + g.repairDamage(s => subjects.add(s)); + expect(subjects).is(new Set(['s', 't'])); + }); }); - describe('a more complex case', () => { - var g = new Dataflow.Graph(); + describe('dataflow blocks', () => { + describe('simple case', () => { + const g = new BlockGraph(); + const c = new CountingField(g, 0); + const d = new CountingField(g, 0); + g.run(() => c.value = 123); + g.run(() => d.value = c.value * 2); + it('should be properly initialized', () => { + expect(c.value).toBe(123); + expect(d.value).toBe(246); + }); + it('should lead initially to damaged everything', () => { + expect(g.damagedNodes.size).toBe(2); + }); + it('should repair idempotently after initialization', () => { + g.repairDamage(c => g.run(c)); + expect(c.value).toBe(123); + expect(d.value).toBe(246); + }); + it('should be inconsistent after modification but before repair', () => { + c.value = 124; + expect(c.value).toBe(124); + expect(d.value).toBe(246); + }); + it('should repair itself properly', () => { + g.repairDamage(c => g.run(c)); + expect(c.value).toBe(124); + expect(d.value).toBe(248); + }); + }); - function add(a, b) { return a + b; } - var xs = new Cell(g, Immutable.List.of(1, 2, 3, 4), 'xs'); - var sum = DerivedCell(g, 'sum', () => xs.value.reduce(add, 0)); - var len = DerivedCell(g, 'len', () => xs.value.size); - var avg = DerivedCell(g, 'avg', () => { - if (len.value === 0) return null; - return sum.value / len.value; - }); - var scale = new Cell(g, 1, 'scale'); - var ans = DerivedCell(g, 'ans', () => { - if (scale.value === 0) return null; - return typeof avg.value === 'number' && avg.value / scale.value; - }); + describe('a more complex case', () => { + const g = new BlockGraph(); - function expectValues(vs) { - g.repairDamage(function (c) { c.refresh(); }); - assert.deepStrictEqual( - [xs.value.toJS(), sum.value, len.value, avg.value, scale.value, ans.value], - vs); - } + const xs = new CountingField(g, [1, 2, 3, 4]); + const sum = g.eqn('sum', () => xs.value.reduce((a, b) => a + b, 0)); + const len = g.eqn('len', () => xs.value.length); + const avg = g.eqn('avg', () => { + if (len.value === 0) return null; + return sum.value / len.value; + }); + const scale = new CountingField(g, 1, 'scale'); + const ans = g.eqn('ans', () => { + if (scale.value === 0) return null; + return typeof avg.value === 'number' && avg.value / scale.value; + }); - it('initially', () => { - expectValues([ [1,2,3,4], 10, 4, 2.5, 1, 2.5 ]); - }); - it('at scale zero', () => { - scale.value = 0; - expectValues([ [1,2,3,4], 10, 4, 2.5, 0, null ]); - }); - it('with nine and zero', () => { - xs.value = xs.value.concat([9, 0]); - expectValues([ [1,2,3,4,9,0], 19, 6, 19/6, 0, null ]); - }); - it('with five and four', () => { - xs.value = xs.value.skipLast(2).concat([5, 4]); - expectValues([ [1,2,3,4,5,4], 19, 6, 19/6, 0, null ]); - }); - it('at scale one', () => { - scale.value = 1; - expectValues([ [1,2,3,4,5,4], 19, 6, 19/6, 1, 19/6 ]); - }); - it('empty', () => { - xs.value = Immutable.List(); - expectValues([ [], 0, 0, null, 1, false ]); - }); - it('four, five, and six', () => { - xs.value = Immutable.List.of(4, 5, 6); - expectValues([ [4,5,6], 15, 3, 15/3, 1, 15/3 ]); - }); - }); - }); + function expectValues(vs: [ + typeof xs.value, + typeof sum.value, + typeof len.value, + typeof avg.value, + typeof scale.value, + typeof ans.value, + ]) { + g.repairDamage(c => g.run(c)); + expect([ + xs.value, + sum.value, + len.value, + avg.value, + scale.value, + ans.value, + ]).is(vs); + } - describe('scopes', () => { - var g = new Dataflow.Graph(); - - function buildScopes() { - var rootScope = {}; - var midScope = Dataflow.Graph.newScope(rootScope); - var outerScope = Dataflow.Graph.newScope(midScope); - return {root: rootScope, mid: midScope, outer: outerScope}; - } - - it('should make rootward props visible further out', () => { - var ss = buildScopes(); - g.defineObservableProperty(ss.root, 'p', 123); - assert.strictEqual(ss.root.p, 123); - assert.strictEqual(ss.mid.p, 123); - assert.strictEqual(ss.outer.p, 123); - assert('p' in ss.root); - assert('p' in ss.mid); - assert('p' in ss.outer); + it('initially', () => { + expectValues([[1, 2, 3, 4], 10, 4, 2.5, 1, 2.5]); + }); + it('at scale zero', () => { + scale.value = 0; + expectValues([[1, 2, 3, 4], 10, 4, 2.5, 0, null]); + }); + it('with nine and zero', () => { + xs.value = [... xs.value, 9, 0]; + expectValues([[1, 2, 3, 4, 9, 0], 19, 6, 19 / 6, 0, null]); + }); + it('with five and four', () => { + xs.value = [... xs.value.slice(0, -2), 5, 4]; + expectValues([[1, 2, 3, 4, 5, 4], 19, 6, 19 / 6, 0, null]); + }); + it('at scale one', () => { + scale.value = 1; + expectValues([[1, 2, 3, 4, 5, 4], 19, 6, 19 / 6, 1, 19 / 6]); + }); + it('empty', () => { + xs.value = []; + expectValues([[], 0, 0, null, 1, false]); + }); + it('four, five, and six', () => { + xs.value = [4, 5, 6]; + expectValues([[4, 5, 6], 15, 3, 15 / 3, 1, 15 / 3]); + }); + }); }); - it('should make changes at root visible at leaves', () => { - var ss = buildScopes(); - g.defineObservableProperty(ss.root, 'p', 123); - assert.strictEqual(ss.outer.p, 123); - ss.root.p = 234; - assert.strictEqual(ss.root.p, 234); - assert.strictEqual(ss.outer.p, 234); + describe('update batching', () => { + const g = new BlockGraph(); + const a = new CountingField(g, 1); + const b = new CountingField(g, 2); + const c = g.eqn('c', () => a.value + b.value); + it('has correct initial values', () => { + expect(a.spy()).toBe(1); + expect(b.spy()).toBe(2); + expect(c.spy()).toBe(3); + }); + it('has correct initial counts', () => { + a.checkCounts(1, 0); + b.checkCounts(1, 0); + c.checkCounts(0, 1); + }); + it('should repair idempotently after initialization', () => { + g.repairDamage(c => g.run(c)); + a.checkCounts(1, 0); + b.checkCounts(1, 0); + c.checkCounts(0, 1); + }); + it('should update on the left', () => { + a.value = 3; + g.repairDamage(c => g.run(c)); + a.checkCounts(2, 1); + b.checkCounts(2, 0); + c.checkCounts(0, 2); + }); + it('should update on the right', () => { + b.value = 3; + g.repairDamage(c => g.run(c)); + a.checkCounts(3, 1); + b.checkCounts(3, 1); + c.checkCounts(0, 3); + }); + it('should batch simultaneous updates on the left and right', () => { + a.value = 4; + b.value = 4; + g.repairDamage(c => g.run(c)); + expect(c.spy()).toBe(8); + c.checkCounts(0, 4); + a.checkCounts(4, 2); + b.checkCounts(4, 2); + }); }); - - it('should make changes at leaves visible at root', () => { - var ss = buildScopes(); - g.defineObservableProperty(ss.root, 'p', 123); - assert.strictEqual(ss.outer.p, 123); - ss.outer.p = 234; - assert.strictEqual(ss.root.p, 234); - assert.strictEqual(ss.outer.p, 234); - }); - - it('should hide definitions at leaves from roots', () => { - var ss = buildScopes(); - g.defineObservableProperty(ss.outer, 'p', 123); - assert.strictEqual(ss.outer.p, 123); - assert.strictEqual(ss.mid.p, undefined); - assert.strictEqual(ss.root.p, undefined); - assert(!('p' in ss.root)); - assert(!('p' in ss.mid)); - assert('p' in ss.outer); - }); - - it('should hide middle definitions from roots but show to leaves', () => { - var ss = buildScopes(); - g.defineObservableProperty(ss.mid, 'p', 123); - assert.strictEqual(ss.outer.p, 123); - assert.strictEqual(ss.mid.p, 123); - assert.strictEqual(ss.root.p, undefined); - assert(!('p' in ss.root)); - assert('p' in ss.mid); - assert('p' in ss.outer); - }); - }); - }); diff --git a/packages/core/test/dataspace.test.ts b/packages/core/test/dataspace.test.ts.DISABLED similarity index 100% rename from packages/core/test/dataspace.test.ts rename to packages/core/test/dataspace.test.ts.DISABLED diff --git a/packages/core/test/skeleton.test.ts b/packages/core/test/skeleton.test.ts.DISABLED similarity index 100% rename from packages/core/test/skeleton.test.ts rename to packages/core/test/skeleton.test.ts.DISABLED diff --git a/packages/core/test/test-utils.ts b/packages/core/test/test-utils.ts new file mode 100644 index 0000000..54b2326 --- /dev/null +++ b/packages/core/test/test-utils.ts @@ -0,0 +1,38 @@ +/// SPDX-License-Identifier: GPL-3.0-or-later +/// SPDX-FileCopyrightText: Copyright © 2016-2023 Tony Garnock-Jones + +import { is, preserves } from '@preserves/core'; + +declare global { + namespace jest { + interface Matchers { + is(expected: any): R; + toThrowFilter(f: (e: Error) => boolean): R; + } + } +} + +expect.extend({ + is(actual, expected) { + return is(actual, expected) + ? { message: () => preserves`expected ${actual} not to be Preserves.is to ${expected}`, + pass: true } + : { message: () => preserves`expected ${actual} to be Preserves.is to ${expected}`, + pass: false }; + }, + + toThrowFilter(thunk, f) { + try { + thunk(); + return { message: () => preserves`expected an exception`, pass: false }; + } catch (e) { + if (f(e)) { + return { message: () => preserves`expected an exception not matching the filter`, + pass: true }; + } else { + return { message: () => preserves`expected an exception matching the filter: ${(e as any)?.constructor?.name}`, + pass: false }; + } + } + } +});