/// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2016-2021 Tony Garnock-Jones "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)); });