syndicate-js/packages/core/test/skeleton.test.ts

328 lines
15 KiB
TypeScript

/// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2016-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
"use strict";
const assert = require('assert');
const Immutable = require('immutable');
const Syndicate = require('../src/index.js');
const { Skeleton, Capture, Discard, Record, Observe } = Syndicate;
const __ = Discard();
const _$ = Capture(Discard());
const Event = Record.makeConstructor('Event', ['label', 'type', 'values']);
function eventCallback(traceHolder, label) {
return (e, vs) => { traceHolder.push(Event(label, e, vs)) };
}
function skeletonTrace(f) {
let traceHolder = {
trace: Immutable.List(),
push: function (e) { this.trace = this.trace.push(e); }
};
let i = new Skeleton.Index();
f(i, traceHolder);
return traceHolder.trace;
}
function _analyzeAssertion(a) {
return Skeleton.analyzeAssertion(Immutable.fromJS(a));
}
function assertImmutableEqual(a, b) {
assert(Immutable.is(Immutable.fromJS(a), Immutable.fromJS(b)));
}
describe('skeleton', () => {
const A = Record.makeConstructor('A', ['x', 'y']);
const B = Record.makeConstructor('B', ['v']);
const C = Record.makeConstructor('C', ['v']);
describe('pattern analysis', () => {
it('should handle leaf captures', () => {
assertImmutableEqual(_analyzeAssertion(A(B(_$), _$)),
{assertion: Observe(A(B(_$), _$)),
skeleton: [A.constructorInfo, [B.constructorInfo, null], null],
constPaths: Immutable.fromJS([]),
constVals: Immutable.fromJS([]),
capturePaths: Immutable.fromJS([[0, 0], [1]])});
});
it('should handle atomic constants', () => {
assertImmutableEqual(_analyzeAssertion(A(B("x"), _$)),
{assertion: Observe(A(B("x"), _$)),
skeleton: [A.constructorInfo, [B.constructorInfo, null], null],
constPaths: Immutable.fromJS([[0, 0]]),
constVals: Immutable.fromJS(["x"]),
capturePaths: Immutable.fromJS([[1]])});
});
it('should handle complex constants (1)', () => {
// Marker: (***)
// Really this comes about when compiled code has no static
// visibility into the value of a constant, and that constant
// will end up being complex at runtime. We can't properly test
// that situation without the static analysis half of the code.
// TODO later.
const complexPlaceholder = new Object();
const analysis = _analyzeAssertion(A(complexPlaceholder, C(_$)));
const expected = {
assertion: Observe(A(complexPlaceholder, C(_$))),
skeleton: [A.constructorInfo, null, [C.constructorInfo, null]],
constPaths: Immutable.fromJS([[0]]),
constVals: Immutable.fromJS([complexPlaceholder]),
capturePaths: Immutable.fromJS([[1, 0]]),
};
assertImmutableEqual(analysis, expected);
});
it('should handle complex constants (2)', () => {
// Marker: (***)
// Really this comes about when compiled code has no static
// visibility into the value of a constant, and that constant
// will end up being complex at runtime. We can't properly test
// that situation without the static analysis half of the code.
// TODO later.
assertImmutableEqual(_analyzeAssertion(A(B(B("y")), Capture(C(__)))),
{assertion: Observe(A(B(B("y")), Capture(C(__)))),
skeleton: [A.constructorInfo,
[B.constructorInfo, [B.constructorInfo, null]],
[C.constructorInfo, null]],
constPaths: Immutable.fromJS([[0, 0, 0]]),
constVals: Immutable.fromJS(["y"]),
capturePaths: Immutable.fromJS([[1]])});
});
it('should handle list patterns with discards', () => {
assertImmutableEqual(_analyzeAssertion([__, __]),
{assertion: Observe([__, __]),
skeleton: [2, null, null],
constPaths: Immutable.fromJS([]),
constVals: Immutable.fromJS([]),
capturePaths: Immutable.fromJS([])});
});
it('should handle list patterns with constants and captures', () => {
assertImmutableEqual(_analyzeAssertion(["hi", _$, _$]),
{assertion: Observe(["hi", _$, _$]),
skeleton: [3, null, null, null],
constPaths: Immutable.fromJS([[0]]),
constVals: Immutable.fromJS(["hi"]),
capturePaths: Immutable.fromJS([[1],[2]])});
});
});
describe('nested structs', () => {
let trace = skeletonTrace((i, traceHolder) => {
i.addHandler(_analyzeAssertion(A(B(_$), _$)), eventCallback(traceHolder, "AB"));
i.addHandler(_analyzeAssertion(A(B("x"), _$)), eventCallback(traceHolder, "ABx"));
let complexConstantPattern1 =
{skeleton: [A.constructorInfo, null, [C.constructorInfo, null]],
constPaths: Immutable.fromJS([[0]]),
constVals: Immutable.fromJS([B("y")]),
capturePaths: Immutable.fromJS([[1, 0]])};
// ^ See comment in 'should handle complex constants (1)' test above (marked (***)).
i.addHandler(complexConstantPattern1, eventCallback(traceHolder, "AByC"));
let complexConstantPattern2 = {skeleton: [A.constructorInfo,
[B.constructorInfo, null],
[C.constructorInfo, null]],
constPaths: Immutable.fromJS([[0, 0]]),
constVals: Immutable.fromJS([B("y")]),
capturePaths: Immutable.fromJS([[1]])};
i.addHandler(complexConstantPattern2, eventCallback(traceHolder, "ABByC"));
i.addAssertion(Immutable.fromJS(A(B("x"),C(1))));
i.addAssertion(Immutable.fromJS(A(B("y"),C(2))));
i.addAssertion(Immutable.fromJS(A(B(B("y")),C(2))));
i.addAssertion(Immutable.fromJS(A(B("z"),C(3))));
});
// trace.forEach((e) => { console.log(e) });
it('should work', () => {
assertImmutableEqual(trace,
[Event("AB", Skeleton.EVENT_ADDED, ["x", C(1)]),
Event("ABx", Skeleton.EVENT_ADDED, [C(1)]),
Event("AB", Skeleton.EVENT_ADDED, ["y", C(2)]),
Event("AByC", Skeleton.EVENT_ADDED, [2]),
Event("AB", Skeleton.EVENT_ADDED, [B("y"), C(2)]),
Event("ABByC", Skeleton.EVENT_ADDED, [C(2)]),
Event("AB", Skeleton.EVENT_ADDED, ["z", C(3)])]);
});
});
describe('simple detail-erasing trace', () => {
let trace = skeletonTrace((i, traceHolder) => {
i.addHandler(_analyzeAssertion([__, __]), eventCallback(traceHolder, "2-EVENT"));
i.addAssertion(Immutable.fromJS(["hi", 123]));
i.addAssertion(Immutable.fromJS(["hi", 234]));
i.removeAssertion(Immutable.fromJS(["hi", 123]));
i.removeAssertion(Immutable.fromJS(["hi", 234]));
});
it('should have one add and one remove', () => {
assertImmutableEqual(trace,
[Event("2-EVENT", Skeleton.EVENT_ADDED, []),
Event("2-EVENT", Skeleton.EVENT_REMOVED, [])]);
});
});
describe('handler added after assertion (1)', () => {
let trace = skeletonTrace((i, traceHolder) => {
i.addAssertion(Immutable.fromJS(["hi", 123, 234]));
i.addHandler(_analyzeAssertion(["hi", _$, _$]), eventCallback(traceHolder, "X"));
i.removeAssertion(Immutable.fromJS(["hi", 123, 234]));
});
it('should get two events', () => {
assertImmutableEqual(trace,
[Event("X", Skeleton.EVENT_ADDED, [123, 234]),
Event("X", Skeleton.EVENT_REMOVED, [123, 234])]);
});
});
describe('handler added after assertion (2)', () => {
let trace = skeletonTrace((i, traceHolder) => {
i.addAssertion(Immutable.fromJS(["hi", 123, 234]));
i.addHandler(_analyzeAssertion(_$), eventCallback(traceHolder, "X"));
i.removeAssertion(Immutable.fromJS(["hi", 123, 234]));
});
it('should get two events', () => {
assertImmutableEqual(trace,
[Event("X", Skeleton.EVENT_ADDED, [["hi", 123, 234]]),
Event("X", Skeleton.EVENT_REMOVED, [["hi", 123, 234]])]);
});
});
describe('handler removed before assertion removed', () => {
let trace = skeletonTrace((i, traceHolder) => {
i.addAssertion(Immutable.fromJS(["hi", 123, 234]));
let h = _analyzeAssertion(["hi", _$, _$]);
h.callback = eventCallback(traceHolder, "X")
i.addHandler(h, h.callback);
i.removeHandler(h, h.callback);
i.removeAssertion(Immutable.fromJS(["hi", 123, 234]));
});
it('should get one event', () => {
assertImmutableEqual(trace,
[Event("X", Skeleton.EVENT_ADDED, [123, 234])]);
});
});
describe('simple list assertions trace', () => {
let trace = skeletonTrace((i, traceHolder) => {
i.addHandler(_analyzeAssertion(["hi", _$, _$]), eventCallback(traceHolder, "3-EVENT"));
i.addHandler(_analyzeAssertion([__, __]), eventCallback(traceHolder, "2-EVENT"));
i.addAssertion(Immutable.fromJS(["hi", 123, 234]));
i.addAssertion(Immutable.fromJS(["hi", 999, 999]));
i.addAssertion(Immutable.fromJS(["hi", 123]));
i.addAssertion(Immutable.fromJS(["hi", 123, 234]));
i.sendMessage(Immutable.fromJS(["hi", 303]));
i.sendMessage(Immutable.fromJS(["hi", 303, 404]));
i.sendMessage(Immutable.fromJS(["hi", 303, 404, 808]));
i.removeAssertion(Immutable.fromJS(["hi", 123, 234]));
i.removeAssertion(Immutable.fromJS(["hi", 999, 999]));
i.removeAssertion(Immutable.fromJS(["hi", 123, 234]));
i.addAssertion(Immutable.fromJS(["hi", 123]));
i.addAssertion(Immutable.fromJS(["hi", 234]));
i.removeAssertion(Immutable.fromJS(["hi", 123]));
i.removeAssertion(Immutable.fromJS(["hi", 123]));
i.removeAssertion(Immutable.fromJS(["hi", 234]));
});
it('should have 8 entries', () => {
assert.strictEqual(trace.size, 8);
});
it('should have a correct 3-EVENT subtrace', () => {
assertImmutableEqual(trace.filter((e) => { return Event._label(e) === "3-EVENT"; }),
[Event("3-EVENT", Skeleton.EVENT_ADDED, [123, 234]),
Event("3-EVENT", Skeleton.EVENT_ADDED, [999, 999]),
Event("3-EVENT", Skeleton.EVENT_MESSAGE, [303, 404]),
Event("3-EVENT", Skeleton.EVENT_REMOVED, [999, 999]),
Event("3-EVENT", Skeleton.EVENT_REMOVED, [123, 234])]);
});
it('should have a correct 2-EVENT subtrace', () => {
assertImmutableEqual(trace.filter((e) => { return Event._label(e) === "2-EVENT"; }),
[Event("2-EVENT", Skeleton.EVENT_ADDED, []),
Event("2-EVENT", Skeleton.EVENT_MESSAGE, []),
Event("2-EVENT", Skeleton.EVENT_REMOVED, [])]);
});
});
function expectMatch(a, b, r) {
assert(Immutable.is(Skeleton.match(Immutable.fromJS(a), Immutable.fromJS(b)), r));
}
describe('matching a single pattern against a value', () => {
it('should accept matching simple records', () => {
expectMatch(A(1, 2), A(1, 2), Immutable.List());
});
it('should capture from matching simple records', () => {
expectMatch(A(1, _$), A(1, 2), Immutable.List([2]));
});
it('should reject mismatching simple records', () => {
expectMatch(A(1, 2), A(1, "hi"), false);
});
it('should accept matching simple lists', () => {
expectMatch([1, 2, 3], [1, 2, 3], Immutable.List());
});
it('should accept matching nested lists', () => {
expectMatch([1, [2, 4], 3], [1, [2, 4], 3], Immutable.List());
});
it('should capture matches from simple lists', () => {
expectMatch([1, Capture(2), 3], [1, 2, 3], Immutable.List([2]));
});
it('should capture discards from simple lists', () => {
expectMatch([1, Capture(__), 3], [1, 2, 3], Immutable.List([2]));
});
it('should capture discards from nested lists', () => {
expectMatch([1, Capture(__), 3], [1, [2, 4], 3], Immutable.fromJS([[2, 4]]));
});
it('should capture nested discards from nested lists', () => {
expectMatch([1, Capture([__, 4]), 3], [1, [2, 4], 3], Immutable.fromJS([[2, 4]]));
});
it('should reject nested mismatches from nested lists', () => {
expectMatch([1, Capture([__, 5]), 3], [1, [2, 4], 3], false);
});
it('should reject mismatching captures from simple lists', () => {
expectMatch([1, Capture(9), 3], [1, 2, 3], false);
});
it('should reject simple lists varying in arity', () => {
expectMatch([1, 2, 3, 4], [1, 2, 3], false);
});
it('should reject simple lists varying in order', () => {
expectMatch([1, 3, 2], [1, 2, 3], false);
});
});
});
describe('path comparison', () => {
const { pathCmp } = require('../src/skeleton.js').__for_testing;
const L = (...args) => Immutable.List(args);
function c(a, b, expected) {
assert.strictEqual(pathCmp(a, b), expected);
}
it('should identify empty paths', () => c(L(), L(), 0));
it('should identify equal nonempty paths (1)', () => c(L(1, 1), L(1, 1), 0));
it('should identify equal nonempty paths (2)', () => c(L(2, 2), L(2, 2), 0));
it('should check upper end first (1)', () => c(L(2, 1), L(1, 1), +1));
it('should check upper end first (2)', () => c(L(1, 1), L(2, 1), -1));
it('should check upper end first (3)', () => c(L(2, 1), L(1, 2), +1));
it('should check upper end first (4)', () => c(L(1, 2), L(2, 1), -1));
it('should check upper end first (5)', () => c(L(2), L(1, 1), +1));
it('should check upper end first (6)', () => c(L(1), L(2, 1), -1));
it('should check upper end first (7)', () => c(L(2), L(1, 2), +1));
it('should check upper end first (8)', () => c(L(1), L(2, 1), -1));
it('should check upper end first (9)', () => c(L(2, 1), L(1), +1));
it('should check upper end first (A)', () => c(L(1, 1), L(2), -1));
it('should check upper end first (B)', () => c(L(2, 1), L(1), +1));
it('should check upper end first (C)', () => c(L(1, 2), L(2), -1));
it('should be lexicographic (1)', () => c(L(1, 2), L(1, 2), 0));
it('should be lexicographic (2)', () => c(L(1), L(1, 2), -1));
it('should be lexicographic (3)', () => c(L(1, 2), L(1), +1));
});