/// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2016-2023 Tony Garnock-Jones import { Canonicalizer, embeddedId, FlexSet } from '@preserves/core'; import { Cell, Field, Graph } from '../src/runtime/dataflow'; import './test-utils'; class CountingField extends Field { readCount = 0; writeCount = 0; get value(): V { this.readCount++; return super.value; } 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); } } class TestGraph extends Graph { constructor(subjectIdCanonicalizer: Canonicalizer) { super(subjectIdCanonicalizer, Cell.canonicalizer, g => Array.from(g.values())); } checkDamagedNodes(expectedObjects: FlexSet) { expect(this.damagedNodes).is(expectedObjects); } } 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; } } class StringGraph extends TestGraph { constructor() { super(s => s); } } describe('dataflow', () => { 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; }); c.value = 234; expect(g.damagedNodes.size).toBe(1); const subjects = new FlexSet(s => s); g.repairDamage(s => subjects.add(s)); expect(subjects).is(new Set(['s', 't'])); }); }); 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); }); }); describe('a more complex case', () => { const g = new BlockGraph(); 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; }); 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); } 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]); }); }); }); 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); }); }); });