2021-12-01 16:24:29 +00:00
|
|
|
/// SPDX-License-Identifier: GPL-3.0-or-later
|
2024-02-03 14:59:22 +00:00
|
|
|
/// SPDX-FileCopyrightText: Copyright © 2016-2024 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
2021-12-01 16:24:29 +00:00
|
|
|
|
2023-12-17 21:07:46 +00:00
|
|
|
import { Canonicalizer, embeddedId, FlexSet } from '@preserves/core';
|
|
|
|
import { Cell, Field, Graph } from '../src/runtime/dataflow';
|
|
|
|
import './test-utils';
|
2018-10-22 14:29:07 +00:00
|
|
|
|
2023-12-17 21:07:46 +00:00
|
|
|
class CountingField<V> extends Field<V> {
|
|
|
|
readCount = 0;
|
|
|
|
writeCount = 0;
|
2018-10-22 14:29:07 +00:00
|
|
|
|
2023-12-17 21:07:46 +00:00
|
|
|
get value(): V {
|
|
|
|
this.readCount++;
|
|
|
|
return super.value;
|
|
|
|
}
|
2018-10-22 14:29:07 +00:00
|
|
|
|
2023-12-17 21:07:46 +00:00
|
|
|
set value(v: V) {
|
|
|
|
this.writeCount++;
|
|
|
|
super.value = v;
|
|
|
|
}
|
2018-10-22 14:29:07 +00:00
|
|
|
|
2023-12-17 21:07:46 +00:00
|
|
|
spy(): V {
|
|
|
|
// Retrieve the value without bumping the counters!
|
|
|
|
return this.__value as V;
|
|
|
|
}
|
2018-10-22 14:29:07 +00:00
|
|
|
|
2023-12-17 21:07:46 +00:00
|
|
|
checkCounts(expectedReadCount: number, expectedWriteCount: number) {
|
|
|
|
expect(this.readCount).toBe(expectedReadCount);
|
|
|
|
expect(this.writeCount).toBe(expectedWriteCount);
|
|
|
|
}
|
2018-10-22 14:29:07 +00:00
|
|
|
}
|
|
|
|
|
2023-12-17 21:07:46 +00:00
|
|
|
class TestGraph<SubjectId> extends Graph<SubjectId, Cell> {
|
|
|
|
constructor(subjectIdCanonicalizer: Canonicalizer<SubjectId>) {
|
|
|
|
super(subjectIdCanonicalizer,
|
|
|
|
Cell.canonicalizer,
|
|
|
|
g => Array.from(g.values()));
|
|
|
|
}
|
2018-10-22 14:29:07 +00:00
|
|
|
|
2023-12-17 21:07:46 +00:00
|
|
|
checkDamagedNodes(expectedObjects: FlexSet<Cell>) {
|
|
|
|
expect(this.damagedNodes).is(expectedObjects);
|
|
|
|
}
|
|
|
|
}
|
2018-10-22 14:29:07 +00:00
|
|
|
|
2023-12-17 21:07:46 +00:00
|
|
|
class BlockGraph extends TestGraph<() => void> {
|
|
|
|
constructor() {
|
|
|
|
super(b => '' + embeddedId(b));
|
|
|
|
}
|
2018-10-22 14:29:07 +00:00
|
|
|
|
2023-12-17 21:07:46 +00:00
|
|
|
run(subject: () => void): () => void {
|
|
|
|
this.withSubject(subject, subject);
|
|
|
|
return subject;
|
2018-10-22 14:29:07 +00:00
|
|
|
}
|
|
|
|
|
2023-12-17 21:07:46 +00:00
|
|
|
eqn<V>(name: string, f: () => V): CountingField<V> {
|
|
|
|
const field = new CountingField<V>(this, null!, name);
|
|
|
|
this.run(() => field.value = f());
|
|
|
|
return field;
|
|
|
|
}
|
|
|
|
}
|
2018-10-22 14:29:07 +00:00
|
|
|
|
2023-12-17 21:07:46 +00:00
|
|
|
class StringGraph extends TestGraph<string> {
|
|
|
|
constructor() {
|
|
|
|
super(s => s);
|
|
|
|
}
|
|
|
|
}
|
2018-10-22 14:29:07 +00:00
|
|
|
|
2023-12-17 21:07:46 +00:00
|
|
|
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<string>(s => s);
|
|
|
|
g.repairDamage(s => subjects.add(s));
|
|
|
|
expect(subjects).is(new Set(['s', 't']));
|
|
|
|
});
|
2018-10-22 14:29:07 +00:00
|
|
|
});
|
|
|
|
|
2023-12-17 21:07:46 +00:00
|
|
|
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]);
|
|
|
|
});
|
|
|
|
});
|
2018-10-22 14:29:07 +00:00
|
|
|
});
|
|
|
|
|
2023-12-17 21:07:46 +00:00
|
|
|
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);
|
|
|
|
});
|
2018-10-22 14:29:07 +00:00
|
|
|
});
|
|
|
|
});
|