Files
xod/packages/xod-project/test/patch.spec.js

1700 lines
50 KiB
JavaScript

import R from 'ramda';
import chai, { expect, assert } from 'chai';
import dirtyChai from 'dirty-chai';
import * as XF from 'xod-func-tools';
import * as Pin from '../src/pin';
import * as Patch from '../src/patch';
import * as Node from '../src/node';
import * as Link from '../src/link';
import * as Comment from '../src/comment';
import * as Attachment from '../src/attachment';
import * as CONST from '../src/constants';
import { formatString } from '../src/utils';
import * as PPU from '../src/patchPathUtils';
import { TERMINALS_LIB_NAME } from '../src/internal/patchPathUtils';
import * as Helper from './helpers';
chai.use(dirtyChai);
const emptyPatch = Helper.defaultizePatch({});
describe('Patch', () => {
// constructors
describe('createPatch', () => {
it('should return Patch that is an object', () => {
const patch = Patch.createPatch();
expect(patch).is.an('object');
});
it('should have key: nodes === {}', () => {
const patch = Patch.createPatch();
expect(patch)
.to.have.property('nodes')
.that.is.an('object')
.that.is.empty();
});
it('should have key: links === {}', () => {
const patch = Patch.createPatch();
expect(patch)
.to.have.property('links')
.that.is.an('object')
.that.is.empty();
});
});
describe('duplicatePatch', () => {
const patch = Helper.defaultizePatch({ nodes: {}, label: 'test' });
it('should return new patch (not the same object)', () => {
const newPatch = Patch.duplicatePatch(patch);
expect(newPatch)
.to.be.an('object')
.and.not.to.be.equal(patch);
});
it('should be deeply cloned (not the same nested objects)', () => {
const newPatch = Patch.duplicatePatch(patch);
expect(newPatch)
.have.property('label')
.that.equal(patch.label);
expect(newPatch)
.have.property('nodes')
.that.not.equal(patch.nodes);
});
});
describe('implementation', () => {
describe('getImpl', () => {
it('should return Nothing for empty patch', () => {
const maybeImpl = Patch.getImpl(emptyPatch);
assert.isTrue(maybeImpl.isNothing);
});
it('should return Nothing for patch without impl attachment', () => {
const patch = Helper.defaultizePatch({
attachments: [
Attachment.createAttachment(
'some-random-file.cpp',
'utf-8',
'// whatever'
),
],
});
const maybeImpl = Patch.getImpl(patch);
assert.isTrue(maybeImpl.isNothing);
});
it('should return Just Source with implementation for patch with impl attachment', () => {
const expectedSource = '// ok!';
const patch = Helper.defaultizePatch({
attachments: [Attachment.createImplAttachment(expectedSource)],
});
const maybeImpl = Patch.getImpl(patch);
assert.isTrue(maybeImpl.isJust);
assert.equal(maybeImpl.getOrElse(null), expectedSource);
});
});
describe('hasImpl', () => {
it('should return false for empty patch', () => {
assert.isFalse(Patch.hasImpl(emptyPatch));
});
it('should return false for patch without impl attachment', () => {
const patch = Helper.defaultizePatch({
attachments: [
Attachment.createAttachment(
'some-random-file.cpp',
'utf-8',
'// whatever'
),
],
});
assert.isFalse(Patch.hasImpl(patch));
});
it('should return true for patch with impl attachment', () => {
const expectedSource = '// ok!';
const patch = Helper.defaultizePatch({
attachments: [Attachment.createImplAttachment(expectedSource)],
});
assert.isTrue(Patch.hasImpl(patch));
});
});
describe('setImpl', () => {
it('should add impl attachment if none existed', () => {
const patch = Helper.defaultizePatch({
attachments: [
Attachment.createAttachment(
'not-an-implementation.cpp',
'utf-8',
'// whatever'
),
],
});
const expectedSource = '// ok!';
const updatedPatch = Patch.setImpl(expectedSource, patch);
assert.equal(
Patch.getImpl(updatedPatch).getOrElse(null),
expectedSource
);
assert.lengthOf(Patch.getPatchAttachments(updatedPatch), 2);
});
it('should update existing impl attachment', () => {
const patch = Helper.defaultizePatch({
attachments: [
Attachment.createAttachment(
'some-random-file.cpp',
'utf-8',
'// whatever'
),
Attachment.createImplAttachment('// initial implementation'),
],
});
const expectedSource = '// updated!';
const updatedPatch = Patch.setImpl(expectedSource, patch);
assert.equal(
Patch.getImpl(updatedPatch).getOrElse(null),
expectedSource,
'implementation source is updated'
);
const getAttachmentFilenames = R.compose(
R.map(Attachment.getFilename),
Patch.getPatchAttachments
);
assert.sameMembers(
getAttachmentFilenames(patch),
getAttachmentFilenames(updatedPatch),
'no new attachments is added'
);
});
});
});
describe('isTerminalPatch', () => {
it('should return false for empty', () => {
expect(Patch.isTerminalPatch(emptyPatch)).to.be.false();
});
it('should return false for patch without terminal pin', () => {
const patch = Helper.defaultizePatch({});
expect(Patch.isTerminalPatch(patch)).to.be.false();
});
it('should return true for input terminal', () => {
const patch = Helper.defaultizePatch({
path: 'xod/patch-nodes/input-number',
});
expect(Patch.isTerminalPatch(patch)).to.be.true();
});
it('should return true for output terminal', () => {
const patch = Helper.defaultizePatch({
path: 'xod/patch-nodes/output-number',
});
expect(Patch.isTerminalPatch(patch)).to.be.true();
});
});
// entity getters
describe('listNodes', () => {
const patch = Helper.defaultizePatch({
nodes: {
rndId: { id: 'rndId' },
rndId2: { id: 'rndId2' },
},
});
it('should return an empty array for empty patch', () => {
expect(Patch.listNodes(emptyPatch))
.to.be.instanceof(Array)
.to.be.empty();
});
it('should return an array of nodes', () => {
expect(Patch.listNodes(patch))
.to.be.instanceof(Array)
.to.have.members([patch.nodes.rndId, patch.nodes.rndId2]);
});
});
describe('nodeIdEquals', () => {
it('should return false for not equal ids', () => {
expect(Patch.nodeIdEquals('1', '2')).to.be.false();
expect(Patch.nodeIdEquals('1', { id: '2' })).to.be.false();
});
it('should return true for equal ids', () => {
expect(Patch.nodeIdEquals('1', '1')).to.be.true();
expect(Patch.nodeIdEquals('1', { id: '1' })).to.be.true();
});
});
describe('getNodeById', () => {
const patch = Helper.defaultizePatch({
nodes: {
rndId: { id: 'rndId' },
},
});
it('should Maybe.Nothing for non-existent node', () => {
const maybeNode = Patch.getNodeById('non-existent', emptyPatch);
expect(maybeNode.isNothing).to.be.true();
});
it('should Maybe.Just with node for existent node', () => {
expect(Patch.getNodeById('rndId', patch).isJust).to.be.true();
expect(Patch.getNodeById('rndId', patch).getOrElse(null)).to.be.equal(
patch.nodes.rndId
);
});
});
describe('getNodeByIdUnsafe', () => {
const patch = Helper.defaultizePatch({
path: 'test/test/test',
nodes: {
rndId: { id: 'rndId' },
},
});
it('should throw Error', () => {
const nodeId = 'non-existent';
const fn = () => Patch.getNodeByIdUnsafe(nodeId, patch);
expect(fn).to.throw(
Error,
formatString(CONST.ERROR.NODE_NOT_FOUND, {
nodeId,
patchPath: patch.path,
})
);
});
it('should return Node', () => {
const node = Patch.getNodeByIdUnsafe('rndId', patch);
assert.equal(node, patch.nodes.rndId);
});
});
describe('listLinks', () => {
const patch = Helper.defaultizePatch({
links: {
1: { id: '1' },
2: { id: '2' },
},
});
it('should return an empty array for empty patch', () => {
expect(Patch.listLinks(emptyPatch))
.to.be.instanceof(Array)
.to.be.empty();
});
it('should return an array of links', () => {
expect(Patch.listLinks(patch))
.to.be.instanceof(Array)
.to.have.members([patch.links['1'], patch.links['2']]);
});
});
describe('linkIdEquals', () => {
it('should return false for not equal ids', () => {
expect(Patch.linkIdEquals('1', '2')).to.be.false();
expect(Patch.linkIdEquals('1', { id: '2' })).to.be.false();
});
it('should return true for equal ids', () => {
expect(Patch.linkIdEquals('1', '1')).to.be.true();
expect(Patch.linkIdEquals('1', { id: '1' })).to.be.true();
});
});
describe('getLinkById', () => {
const patch = Helper.defaultizePatch({
links: {
1: { id: '1' },
},
});
it('should Maybe.Nothing for non-existent link', () => {
expect(
Patch.getLinkById('non-existent', emptyPatch).isNothing
).to.be.true();
});
it('should Maybe.Just with link for existent link', () => {
expect(Patch.getLinkById('1', patch).isJust).to.be.true();
expect(Patch.getLinkById('1', patch).getOrElse(null)).to.be.equal(
patch.links[1]
);
});
});
describe('listLinksByNode', () => {
const patch = Helper.defaultizePatch({
links: {
1: {
id: '1',
input: { pinKey: 'fromPin', nodeId: '@/from' },
output: { pinKey: 'toPin', nodeId: '@/to' },
},
},
nodes: {
'@/from': { id: '@/from' },
'@/to': { id: '@/to' },
},
});
it('should return empty array for non-existent node', () => {
expect(Patch.listLinksByNode('@/non-existent', patch))
.to.be.instanceof(Array)
.and.to.be.empty();
});
it('should return empty array for empty patch', () => {
expect(Patch.listLinksByNode('@/a', emptyPatch))
.to.be.instanceof(Array)
.and.to.be.empty();
});
it('should return array with one link', () => {
expect(Patch.listLinksByNode('@/from', patch))
.to.be.instanceof(Array)
.and.to.have.members([patch.links[1]]);
});
});
describe('listLinksByPin', () => {
const patch = Helper.defaultizePatch({
links: {
1: {
id: '1',
input: { pinKey: 'fromPin', nodeId: '@/from' },
output: { pinKey: 'toPin', nodeId: '@/to' },
},
},
nodes: {
'@/from': { id: '@/from' },
'@/to': { id: '@/to' },
},
});
it('should return empty array for non-existent node', () => {
expect(Patch.listLinksByPin('fromPin', '@/non-existent', patch))
.to.be.instanceof(Array)
.and.to.be.empty();
});
it('should return empty array for empty patch', () => {
expect(Patch.listLinksByPin('fromPin', '@/from', emptyPatch))
.to.be.instanceof(Array)
.and.to.be.empty();
});
it('should return empty array for pinKey in input and nodeId in output', () => {
expect(Patch.listLinksByPin('fromPin', '@/to', patch))
.to.be.instanceof(Array)
.and.to.be.empty();
});
it('should return array with one link', () => {
expect(Patch.listLinksByPin('fromPin', '@/from', patch))
.to.be.instanceof(Array)
.and.to.have.members([patch.links[1]]);
});
});
describe('listLibraryNamesUsedInPatch', () => {
it('returns an empty list for empty patch', () =>
assert.isEmpty(
Patch.listLibraryNamesUsedInPatch(Helper.defaultizePatch({}))
));
it('returns an empty list if Patch used only Nodes from local project or builtIns', () =>
assert.isEmpty(
Patch.listLibraryNamesUsedInPatch(
Helper.defaultizePatch({
nodes: {
a: { type: '@/my-patch' },
b: { type: `${TERMINALS_LIB_NAME}/input-number` },
},
})
)
));
it('returns a list of two libNames', () =>
assert.sameMembers(
Patch.listLibraryNamesUsedInPatch(
Helper.defaultizePatch({
nodes: {
a: { type: 'xod/core/flip-flop' },
b: { type: 'xod/core/clock' },
c: { type: 'xod/common-hardware/led' },
},
})
),
['xod/core', 'xod/common-hardware']
));
});
describe('pins', () => {
const patch = Helper.defaultizePatch({
nodes: {
in: {
id: 'in',
type: 'xod/patch-nodes/input-boolean',
label: 'in',
},
out: {
id: 'out',
type: 'xod/patch-nodes/output-boolean',
label: 'out',
},
},
});
const expectedPins = {
in: {
'@@type': 'xod-project/Pin',
key: 'in',
direction: CONST.PIN_DIRECTION.INPUT,
type: 'boolean',
label: 'in',
description: '',
order: 0,
defaultValue: false,
isBindable: true,
},
out: {
'@@type': 'xod-project/Pin',
key: 'out',
direction: CONST.PIN_DIRECTION.OUTPUT,
type: 'boolean',
label: 'out',
description: '',
order: 0,
defaultValue: false,
isBindable: false,
},
};
describe('getPinByKey', () => {
it('should return Maybe.Nothing for empty patch', () => {
const res = Patch.getPinByKey('a', emptyPatch);
expect(res.isNothing).to.be.true();
});
it('should return Maybe.Just for patch with pin node', () => {
const aPatch = Helper.defaultizePatch({
nodes: {
a: {
id: 'a',
type: 'xod/patch-nodes/input-boolean',
position: { x: 0, y: 0 },
},
},
});
const res = Patch.getPinByKey('a', aPatch);
expect(res.isJust).to.be.true();
expect(res.getOrElse(null))
.to.be.an('object')
.that.have.property('key')
.that.equal('a');
});
});
describe('listPins', () => {
it('should return empty array for empty patch', () => {
expect(Patch.listPins(emptyPatch))
.to.be.instanceof(Array)
.and.to.be.empty();
});
it('should return array with two pins', () => {
assert.sameDeepMembers(
[expectedPins.in, expectedPins.out],
Patch.listPins(patch)
);
});
});
describe('listInputPins', () => {
it('should return empty array for empty patch', () => {
expect(Patch.listInputPins(emptyPatch))
.to.be.instanceof(Array)
.and.to.be.empty();
});
it('should return array with one pin', () => {
assert.sameDeepMembers([expectedPins.in], Patch.listInputPins(patch));
});
});
describe('listOutputPins', () => {
it('should return empty array for empty patch', () => {
expect(Patch.listOutputPins(emptyPatch))
.to.be.instanceof(Array)
.and.to.be.empty();
});
it('should return array with one pin', () => {
assert.sameDeepMembers([expectedPins.out], Patch.listOutputPins(patch));
});
});
describe('computing from terminals', () => {
it('should order pins by x coordinate of terminal nodes', () => {
const inputBooleanType = PPU.getTerminalPath(
CONST.PIN_DIRECTION.INPUT,
CONST.PIN_TYPE.BOOLEAN
);
/**
+-----+ +-----+ +-----+
| | | | | |
| 0 | | 1 | | 2 |
| | | | | |
+-----+ +-----+ +-----+
*/
const testPatch = Helper.defaultizePatch({
nodes: {
in0: {
type: inputBooleanType,
position: { x: 0, y: 0 },
},
in1: {
type: inputBooleanType,
position: { x: 100, y: 0 },
},
in2: {
type: inputBooleanType,
position: { x: 200, y: 0 },
},
},
});
const orderedPinKeys = R.compose(
R.map(Pin.getPinKey),
R.sortBy(Pin.getPinOrder),
Patch.listPins
)(testPatch);
assert.deepEqual(orderedPinKeys, ['in0', 'in1', 'in2']);
});
it('should order pins with the same x coordinate of terminal nodes by y', () => {
const inputBooleanType = PPU.getTerminalPath(
CONST.PIN_DIRECTION.INPUT,
CONST.PIN_TYPE.BOOLEAN
);
/**
+-----+ +-----+
| | | |
| 0 | | 2 |
| | | |
+-----+ +-----+
+-----+
| |
| 1 |
| |
+-----+
*/
const testPatch = Helper.defaultizePatch({
nodes: {
in0: {
type: inputBooleanType,
position: { x: 0, y: 0 },
},
in1: {
type: inputBooleanType,
position: { x: 0, y: 100 },
},
in2: {
type: inputBooleanType,
position: { x: 100, y: 0 },
},
},
});
const orderedPinKeys = R.compose(
R.map(Pin.getPinKey),
R.sortBy(Pin.getPinOrder),
Patch.listPins
)(testPatch);
assert.deepEqual(orderedPinKeys, ['in0', 'in1', 'in2']);
});
it("should extract defaultValue from terminal's bound values", () => {
const testPatch = Helper.defaultizePatch({
nodes: {
inStr: {
type: PPU.getTerminalPath(
CONST.PIN_DIRECTION.INPUT,
CONST.PIN_TYPE.STRING
),
boundValues: {
[CONST.TERMINAL_PIN_KEYS[CONST.PIN_DIRECTION.OUTPUT]]: 'hello',
},
},
outNum: {
type: PPU.getTerminalPath(
CONST.PIN_DIRECTION.OUTPUT,
CONST.PIN_TYPE.NUMBER
),
boundValues: {
[CONST.TERMINAL_PIN_KEYS[CONST.PIN_DIRECTION.INPUT]]: 42,
},
},
},
});
const pinDefaultValues = R.compose(
R.map(Pin.getPinDefaultValue),
R.indexBy(Pin.getPinKey),
Patch.listPins
)(testPatch);
assert.deepEqual({ inStr: 'hello', outNum: 42 }, pinDefaultValues);
});
it('should set defaultValue to default for type if terminal has no bound values', () => {
const testPatch = Helper.defaultizePatch({
nodes: {
inStr: {
type: PPU.getTerminalPath(
CONST.PIN_DIRECTION.INPUT,
CONST.PIN_TYPE.STRING
),
boundValues: {},
},
outNum: {
type: PPU.getTerminalPath(
CONST.PIN_DIRECTION.OUTPUT,
CONST.PIN_TYPE.NUMBER
),
boundValues: {},
},
},
});
const pinDefaultValues = R.compose(
R.map(Pin.getPinDefaultValue),
R.indexBy(Pin.getPinKey),
Patch.listPins
)(testPatch);
assert.deepEqual({ inStr: '', outNum: 0 }, pinDefaultValues);
});
});
});
describe('comments', () => {
const testComment1 = Comment.createComment(
{ x: 101, y: 201 },
{ width: 1101, height: 2001 },
'test comment 1'
);
const testComment2 = Comment.createComment(
{ x: 102, y: 202 },
{ width: 1102, height: 2002 },
'test comment 2'
);
const patchWithComments = R.compose(
Patch.assocComment(testComment2),
Patch.assocComment(testComment1)
)(emptyPatch);
describe('assocComment', () => {
it('should return a new Patch with a specified Comment added', () => {
const newPatch = Patch.assocComment(testComment1, emptyPatch);
assert.deepEqual(
Patch.getCommentByIdUnsafe(
Comment.getCommentId(testComment1),
newPatch
),
testComment1
);
});
});
describe('dissocComment', () => {
it('should return a new Patch without added Comment', () => {
const commentId = Comment.getCommentId(testComment1);
const newPatch = Patch.dissocComment(commentId, patchWithComments);
const maybeComment = Patch.getCommentById(commentId, newPatch);
assert.equal(maybeComment.isNothing, true);
});
});
describe('listComments', () => {
it('should return an empty array for an empty patch', () => {
assert.deepEqual(
Patch.listComments(emptyPatch),
[],
'empty patch should not have any comments'
);
});
it('should return an array of comments added to patch', () => {
assert.sameMembers(Patch.listComments(patchWithComments), [
testComment1,
testComment2,
]);
});
});
describe('getCommentById', () => {
it('should return Just Commment for an existing Comment', () => {
const maybeComment = Patch.getCommentById(
Comment.getCommentId(testComment1),
patchWithComments
);
assert.equal(maybeComment.isJust, true);
});
it('should return Nothing for a non-existing Comment', () => {
const maybeComment = Patch.getCommentById(
'non-existing',
patchWithComments
);
assert.equal(maybeComment.isNothing, true);
});
});
describe('getCommentByIdUnsafe', () => {
it('should return a Commment if a Comment with a given CommentId exists', () => {
const comment = Patch.getCommentByIdUnsafe(
Comment.getCommentId(testComment1),
patchWithComments
);
assert.equal(comment, testComment1);
});
it('should throw Error if a Comment with a given CommentId does not exist', () => {
const nonExistingCommentId = 'non-existing';
assert.throws(
() => {
Patch.getCommentByIdUnsafe(nonExistingCommentId, patchWithComments);
},
formatString(CONST.ERROR.COMMENT_NOT_FOUND, {
commentId: nonExistingCommentId,
patchPath: patchWithComments.path,
})
);
});
});
describe('upsertComments', () => {
it('should return a new patch with upserted links', () => {
const commentsList = [testComment1, testComment2];
const newPatch = Patch.upsertComments(commentsList, emptyPatch);
assert.sameMembers(Patch.listComments(newPatch), commentsList);
});
});
});
// entity setters
describe('assocNode', () => {
it('should return Patch with new Node', () => {
const node = Helper.defaultizeNode({ id: '1' });
const newPatch = Patch.assocNode(node, emptyPatch);
expect(newPatch)
.to.have.property('nodes')
.to.have.property('1')
.that.equals(node);
});
it('should replace old Node with same id', () => {
const patch = Helper.defaultizePatch({
nodes: {
1: { id: '1', label: 'old' },
},
});
const node = Helper.defaultizeNode({
id: '1',
label: 'new',
});
const newPatch = Patch.assocNode(node, patch);
expect(newPatch)
.to.have.property('nodes')
.to.have.property('1')
.that.equals(node);
});
it('should add pin by associating terminal node', () => {
const node = Helper.defaultizeNode({
id: '1',
type: 'xod/patch-nodes/input-number',
label: 'A',
});
const newPatch = Patch.assocNode(node, emptyPatch);
const expectedPin = Pin.createPin(
'1',
'number',
'input',
0,
'A',
'',
true,
0
);
assert.deepEqual([expectedPin], Patch.listPins(newPatch));
});
it('should update pin by associating terminal Node with the same id', () => {
const patch = Helper.defaultizePatch({
nodes: {
1: {
id: '1',
type: 'xod/patch-nodes/output-string',
},
},
});
const expectedPinBeforeUpdate = Pin.createPin(
'1',
'string',
'output',
0,
'',
'',
false,
''
);
assert.deepEqual([expectedPinBeforeUpdate], Patch.listPins(patch));
const node = Helper.defaultizeNode({
id: '1',
type: 'xod/patch-nodes/input-number',
label: 'A',
});
const newPatch = Patch.assocNode(node, patch);
const expectedPinAfterUpdate = Pin.createPin(
'1',
'number',
'input',
0,
'A',
'',
true,
0
);
assert.deepEqual([expectedPinAfterUpdate], Patch.listPins(newPatch));
});
});
describe('dissocNode', () => {
const patch = Helper.defaultizePatch({
nodes: {
rndId: { id: 'rndId' },
rndId2: { id: 'rndId2' },
},
links: {
1: {
id: '1',
output: { pinKey: 'out', nodeId: 'rndId' },
input: { pinKey: 'in', nodeId: 'rndId2' },
},
},
});
it('should remove node by id', () => {
const newPatch = Patch.dissocNode('rndId', patch);
expect(newPatch)
.to.be.an('object')
.that.have.property('nodes')
.that.not.have.keys(['rndId']);
});
it('should remove node by Node object', () => {
const node = patch.nodes.rndId;
const newPatch = Patch.dissocNode(node, patch);
expect(newPatch)
.to.be.an('object')
.that.have.property('nodes')
.that.not.have.keys(['rndId']);
});
it('should remove connected link', () => {
const node = patch.nodes.rndId;
const newPatch = Patch.dissocNode(node, patch);
expect(newPatch)
.to.be.an('object')
.that.have.property('links')
.that.empty();
});
it('should not affect on other nodes', () => {
const newPatch = Patch.dissocNode('rndId', patch);
expect(newPatch)
.to.be.an('object')
.that.have.property('nodes')
.that.have.keys(['rndId2'])
.and.not.have.keys(['rndId']);
});
it('should return unchanges Patch for non-existent node/id', () => {
expect(Patch.dissocNode('@/non-existent', patch))
.to.be.an('object')
.and.deep.equals(patch);
expect(Patch.dissocNode({ id: '@/non-existent' }, patch))
.to.be.an('object')
.and.deep.equals(patch);
});
it('should remove pin from patch on dissoc pinNode', () => {
const patchWithPins = Helper.defaultizePatch({
nodes: {
a: { id: 'a', type: 'xod/patch-nodes/input-number' },
b: { id: 'b', type: 'xod/patch-nodes/output-number' },
},
});
const newPatch = Patch.dissocNode('a', patchWithPins);
assert.deepEqual(
['b'],
R.compose(R.map(Pin.getPinKey), Patch.listPins)(newPatch)
);
});
});
describe('assocLink', () => {
// TODO: Add patch for assocLink
// const patch = {
// nodes: {
// in: { id: 'in' },
// out: { id: 'out' },
// },
// };
// it('should return Either.Left for invalid link', () => {
// expect(Patch.assocLink({}, {}).isLeft).to.be.true();
// })
// it('should return Either.Right with patch and assigned new link', () => {
//
// });
});
describe('dissocLink', () => {
const patch = Helper.defaultizePatch({
links: {
1: { id: '1' },
2: { id: '2' },
},
});
it('should remove link by id', () => {
const newPatch = Patch.dissocLink('1', patch);
expect(newPatch)
.to.be.an('object')
.that.have.property('links')
.that.not.have.keys(['1']);
});
it('should remove node by Link object', () => {
const link = patch.links['1'];
const newPatch = Patch.dissocLink(link, patch);
expect(newPatch)
.to.be.an('object')
.that.have.property('links')
.that.not.have.keys(['1']);
});
it('should not affect on other links', () => {
const newPatch = Patch.dissocLink('1', patch);
expect(newPatch)
.to.be.an('object')
.that.have.property('links')
.that.have.keys(['2'])
.and.not.have.keys(['1']);
});
it('should return unchanges Patch for non-existent link/id', () => {
expect(Patch.dissocLink('3', patch))
.to.be.an('object')
.and.deep.equals(patch);
expect(Patch.dissocLink({ id: '3' }, patch))
.to.be.an('object')
.and.deep.equals(patch);
});
});
describe('upsertNodes & upsertLinks', () => {
const patch = Patch.createPatch();
it('should return patch with upserted nodes and upserted links', () => {
const newNodes = [
Node.createNode({ x: 0, y: 0 }, '@/yay'),
Node.createNode({ x: 0, y: 0 }, '@/awesome'),
];
const patchWithNodes = Patch.upsertNodes(newNodes, patch);
assert.sameDeepMembers(Patch.listNodes(patchWithNodes), newNodes);
const nodeIds = R.map(Node.getNodeId, newNodes);
const newLinks = [
Link.createLink('IN_A', nodeIds[0], 'OUTA', nodeIds[1]),
Link.createLink('IN_B', nodeIds[0], 'OUTB', nodeIds[1]),
];
const patchWithLinks = Patch.upsertLinks(newLinks, patchWithNodes);
assert.isTrue(patchWithLinks.isRight);
assert.sameDeepMembers(
Patch.listLinks(XF.explodeEither(patchWithLinks)),
newLinks
);
});
});
describe('validateLink', () => {
const patch = Helper.defaultizePatch({
nodes: {
out: { id: 'out' },
in: { id: 'in' },
},
});
const linkId = '1';
const validInput = {
nodeId: 'in',
pinKey: 'in',
};
const validOutput = {
nodeId: 'out',
pinKey: 'out',
};
it('should return Either.Left for non-existent input node in the patch', () => {
const link = {
id: linkId,
input: { nodeId: 'non-existent', pinKey: 'a' },
output: validOutput,
};
const err = Patch.validateLink(link, patch);
Helper.expectEitherError(CONST.ERROR.LINK_INPUT_NODE_NOT_FOUND, err);
});
it('should return Either.Left for non-existent output node in the patch', () => {
const link = {
id: linkId,
input: validInput,
output: { nodeId: 'non-existent', pinKey: 'a' },
};
const err = Patch.validateLink(link, patch);
Helper.expectEitherError(CONST.ERROR.LINK_OUTPUT_NODE_NOT_FOUND, err);
});
it('should return Either.Right with link', () => {
const link = { id: linkId, input: validInput, output: validOutput };
const valid = Patch.validateLink(link, patch);
Helper.expectEitherRight(
validLink => expect(validLink).to.be.equal(link),
valid
);
});
});
// utils
describe('utils', () => {
describe('topology utils', () => {
const patch = Helper.defaultizePatch({
nodes: {
a: { id: 'a' },
b: { id: 'b' },
c: { id: 'c' },
},
links: {
x: {
id: 'x',
input: { nodeId: 'b', pinKey: 'x' },
output: { nodeId: 'a', pinKey: 'x' },
},
y: {
id: 'y',
input: { nodeId: 'c', pinKey: 'x' },
output: { nodeId: 'b', pinKey: 'x' },
},
},
attachments: [
Attachment.createAttachment(
'patch.cpp',
'utf-8',
'// implementation'
),
],
});
const expectedPatch = Helper.defaultizePatch({
nodes: {
0: { id: '0' },
1: { id: '1' },
2: { id: '2' },
},
links: {
x: {
id: 'x',
input: { nodeId: '1', pinKey: 'x' },
output: { nodeId: '0', pinKey: 'x' },
},
y: {
id: 'y',
input: { nodeId: '2', pinKey: 'x' },
output: { nodeId: '1', pinKey: 'x' },
},
},
attachments: [
Attachment.createAttachment(
'patch.cpp',
'utf-8',
'// implementation'
),
],
});
it('toposortNodes: should return same patch with nodes and links with new ids', () => {
const sortedPatch = XF.explodeEither(Patch.toposortNodes(patch));
assert.deepEqual(sortedPatch, expectedPatch);
});
it('getTopology: should return correct topology', () => {
Helper.expectEitherRight(topology => {
expect(topology).to.be.deep.equal(['a', 'b', 'c']);
}, Patch.getTopology(patch));
Helper.expectEitherRight(topology => {
expect(topology).to.be.deep.equal(['0', '1', '2']);
}, Patch.getTopology(expectedPatch));
});
});
describe('isEffectPatch', () => {
it('should return true for a patch with pulse inputs', () => {
const someLocalPatch = Helper.defaultizePatch({
path: PPU.getLocalPath('my-patch'),
nodes: {
plsin: {
type: PPU.getTerminalPath(
CONST.PIN_DIRECTION.INPUT,
CONST.PIN_TYPE.PULSE
),
},
},
});
expect(Patch.isEffectPatch(someLocalPatch)).to.be.true();
const terminalPulse = Helper.defaultizePatch({
path: PPU.getTerminalPath(
CONST.PIN_DIRECTION.OUTPUT,
CONST.PIN_TYPE.PULSE
),
});
expect(Patch.isEffectPatch(terminalPulse)).to.be.true();
});
it('should return true for a patch with pulse outputs', () => {
const someLocalPatch = Helper.defaultizePatch({
path: PPU.getLocalPath('my-patch'),
nodes: {
plsout: {
type: PPU.getTerminalPath(
CONST.PIN_DIRECTION.OUTPUT,
CONST.PIN_TYPE.PULSE
),
},
},
});
expect(Patch.isEffectPatch(someLocalPatch)).to.be.true();
const terminalPulse = Helper.defaultizePatch({
path: PPU.getTerminalPath(
CONST.PIN_DIRECTION.INPUT,
CONST.PIN_TYPE.PULSE
),
});
expect(Patch.isEffectPatch(terminalPulse)).to.be.true();
});
it('should return false for a patch without pulse pins', () => {
const noPinsAtAll = Helper.defaultizePatch({
path: PPU.getLocalPath('my-patch'),
});
expect(Patch.isEffectPatch(noPinsAtAll)).to.be.false();
const withPins = Helper.defaultizePatch({
path: PPU.getLocalPath('my-patch'),
nodes: {
someInputNode: {
type: PPU.getTerminalPath(
CONST.PIN_DIRECTION.INPUT,
CONST.PIN_TYPE.NUMBER
),
},
someOutputNode: {
type: PPU.getTerminalPath(
CONST.PIN_DIRECTION.INPUT,
CONST.PIN_TYPE.BOOLEAN
),
},
},
});
expect(Patch.isEffectPatch(withPins)).to.be.false();
});
});
describe('isDeprecatedPatch', () => {
it('returns true for patch with deprecated marker', () => {
const patch = Helper.defaultizePatch({
nodes: {
a: {
type: 'xod/patch-nodes/deprecated',
},
},
});
assert.equal(Patch.isDeprecatedPatch(patch), true);
});
it('returns false for patch without deprecated marker', () => {
const patch = Helper.defaultizePatch({});
assert.equal(Patch.isDeprecatedPatch(patch), false);
});
});
describe('getDeprecationReason', () => {
it('returns Maybe String for patch with deprecated marker', () => {
const patch = Helper.defaultizePatch({
nodes: {
a: {
type: 'xod/patch-nodes/deprecated',
description: 'You should not use it anymore',
},
},
});
assert.equal(
Patch.getDeprecationReason(patch).getOrElse(null),
'You should not use it anymore'
);
});
it('returns Maybe.Nothing for patch without deprecated marker', () => {
const patch = Helper.defaultizePatch({});
assert.equal(Patch.getDeprecationReason(patch).isNothing, true);
});
});
describe('canBindToOutputs', () => {
it('should return true for a patch with pulse inputs', () => {
const patch = Helper.defaultizePatch({
path: PPU.getLocalPath('my-patch'),
nodes: {
plsin: {
type: PPU.getTerminalPath(
CONST.PIN_DIRECTION.INPUT,
CONST.PIN_TYPE.PULSE
),
},
},
});
expect(Patch.canBindToOutputs(patch)).to.be.true();
});
it('should return true for a patch with pulse outputs', () => {
const patch = Helper.defaultizePatch({
path: PPU.getLocalPath('my-patch'),
nodes: {
plsout: {
type: PPU.getTerminalPath(
CONST.PIN_DIRECTION.OUTPUT,
CONST.PIN_TYPE.PULSE
),
},
},
});
expect(Patch.canBindToOutputs(patch)).to.be.true();
});
it('should return true for constant patches', () => {
const patch = Helper.defaultizePatch({
path: CONST.CONST_NODETYPES[CONST.PIN_TYPE.NUMBER],
nodes: {
out: {
type: PPU.getTerminalPath(
CONST.PIN_DIRECTION.OUTPUT,
CONST.PIN_TYPE.NUMBER
),
},
},
});
expect(Patch.canBindToOutputs(patch)).to.be.true();
});
it('should return true for terminal patches', () => {
const patch = Helper.defaultizePatch({
path: PPU.getTerminalPath(
CONST.PIN_DIRECTION.OUTPUT,
CONST.PIN_TYPE.NUMBER
),
});
expect(Patch.canBindToOutputs(patch)).to.be.true();
});
it('should return false for a patch without pulse pins(a.k.a functional patch)', () => {
const patch = Helper.defaultizePatch({
path: PPU.getLocalPath('my-patch'),
});
expect(Patch.canBindToOutputs(patch)).to.be.false();
});
});
describe('remove debug nodes', () => {
it('should return patch without debug nodes and links', () => {
const origPatch = Helper.defaultizePatch({
nodes: {
a: {
id: 'a',
type: 'xod/core/watch',
},
b: {
id: 'b',
type: 'xod/core/add',
},
},
links: {
b2a: {
id: 'b2a',
input: { nodeId: 'a', pinKey: 'IN' },
output: { nodeId: 'a', pinKey: 'OUT' },
},
},
});
const expectedPatch = Helper.defaultizePatch({
nodes: {
b: {
id: 'b',
type: 'xod/core/add',
},
},
links: {},
});
assert.deepEqual(Patch.removeDebugNodes(origPatch), expectedPatch);
});
it('should not affect patch without debug nodes', () => {
const origPatch = Helper.defaultizePatch({
nodes: {
a: {
id: 'a',
type: 'xod/core/multiply',
},
b: {
id: 'b',
type: 'xod/core/add',
},
},
links: {
b2a: {
id: 'b2a',
input: { nodeId: 'a', pinKey: 'IN_0' },
output: { nodeId: 'a', pinKey: 'OUT' },
},
},
});
assert.strictEqual(Patch.removeDebugNodes(origPatch), origPatch);
});
});
});
// variadics
describe('variadic utils', () => {
describe('isVariadicPatch', () => {
it('returns false for Patch without variadic markers', () => {
const patch = Helper.defaultizePatch({
nodes: {
a: {
id: 'a',
type: 'xod/core/add',
},
},
});
assert.equal(Patch.isVariadicPatch(patch), false);
});
it('returns false for Patch with not valid variadic marker', () => {
const patch = Helper.defaultizePatch({
nodes: {
a: {
id: 'a',
type: 'xod/patch-nodes/variadic-99',
},
},
});
assert.equal(Patch.isVariadicPatch(patch), false);
});
it('returns true for Patch with valid variadic marker', () => {
const patch = Helper.defaultizePatch({
nodes: {
a: {
id: 'a',
type: 'xod/patch-nodes/variadic-2',
},
},
});
assert.equal(Patch.isVariadicPatch(patch), true);
});
});
describe('getArityStepFromPatch', () => {
it('returns Nothing for patch without variadic Node', () => {
const patch = Helper.defaultizePatch({});
assert.equal(Patch.getArityStepFromPatch(patch).isNothing, true);
});
it('returns Nothing for patch with wrong kind of variadic Node', () => {
const patch = Helper.defaultizePatch({
nodes: {
a: {
id: 'a',
type: 'xod/patch-nodes/variadic-99',
},
},
});
assert.equal(Patch.getArityStepFromPatch(patch).isNothing, true);
});
const createTestForJust = n => {
it(`returns Maybe ${n} for patch with variadic Node`, () => {
const patch = Helper.defaultizePatch({
nodes: {
a: {
id: 'a',
type: `xod/patch-nodes/variadic-${n}`,
},
},
});
assert.equal(Patch.getArityStepFromPatch(patch).getOrElse(null), n);
});
};
createTestForJust(1);
createTestForJust(2);
createTestForJust(3);
});
describe('computeVariadicPins', () => {
it('returns Either.Left Error for Patch without variadic markers', () => {
const patch = Helper.defaultizePatch({
path: '@/test',
});
const res = Patch.computeVariadicPins(patch);
Helper.expectEitherError(
'Patch "@/test" has no variadic markers.',
res
);
});
it('returns Either.Left Error for Patch with >1 variadic marker', () => {
const patch = Helper.defaultizePatch({
path: '@/test',
nodes: {
a: {
id: 'a',
type: 'xod/patch-nodes/variadic-1',
},
b: {
id: 'b',
type: 'xod/patch-nodes/variadic-1',
},
},
});
const res = Patch.computeVariadicPins(patch);
Helper.expectEitherError(
'Patch "@/test" has more than one variadic-* marker',
res
);
});
it('returns Either.Left Error for patch with variadic marker and without output pins', () => {
const patch = Helper.defaultizePatch({
nodes: {
a: {
id: 'a',
type: 'xod/patch-nodes/variadic-2',
},
},
});
const res = Patch.computeVariadicPins(patch);
Helper.expectEitherError(
'A variadic patch should have at least one output',
res
);
});
it('returns Either.Left Error for patch with variadic marker and with less inputs than needed', () => {
const patch = Helper.defaultizePatch({
nodes: {
a: {
id: 'a',
type: 'xod/patch-nodes/variadic-2',
},
b: {
id: 'b',
type: 'xod/patch-nodes/input-number',
},
d: {
id: 'd',
type: 'xod/patch-nodes/output-number',
},
},
});
const res = Patch.computeVariadicPins(patch);
Helper.expectEitherError(
'A variadic-2 patch node with 1 outputs should have at least 3 inputs',
res
);
});
it('returns Either.Left Error for patch with different types of output pins and accumulator input pins', () => {
const patch = Helper.defaultizePatch({
nodes: {
a: {
id: 'a',
type: 'xod/patch-nodes/variadic-2',
},
b: {
id: 'b',
label: 'X',
type: 'xod/patch-nodes/input-number',
},
c: {
id: 'c',
label: 'Y',
type: 'xod/patch-nodes/input-number',
},
d: {
id: 'd',
label: 'ADD',
type: 'xod/patch-nodes/input-number',
},
e: {
id: 'e',
label: 'ADD2',
type: 'xod/patch-nodes/input-number',
},
f: {
id: 'f',
label: 'A',
type: 'xod/patch-nodes/output-number',
},
g: {
id: 'g',
label: 'B',
type: 'xod/patch-nodes/output-boolean',
},
},
});
const res = Patch.computeVariadicPins(patch);
Helper.expectEitherError(
'Types of inputs Y should match the types of outputs B',
res
);
});
it('returns Either.Right VariadicPins for patch with variadic marker and correct amount of pins', () => {
const patch = Helper.defaultizePatch({
nodes: {
a: {
id: 'a',
type: 'xod/patch-nodes/variadic-2',
},
b: {
id: 'b',
type: 'xod/patch-nodes/input-number',
position: { x: 0, y: 0 },
},
c: {
id: 'c',
type: 'xod/patch-nodes/input-number',
position: { x: 10, y: 0 },
},
d: {
id: 'd',
type: 'xod/patch-nodes/input-number',
position: { x: 20, y: 0 },
},
e: {
id: 'e',
type: 'xod/patch-nodes/output-number',
},
},
});
const res = Patch.computeVariadicPins(patch);
assert.equal(res.isRight, true);
const resPins = XF.explodeEither(res);
assert.equal(resPins.value[0].key, 'c');
assert.equal(resPins.value[1].key, 'd');
assert.equal(resPins.acc[0].key, 'b');
assert.equal(resPins.outputs[0].key, 'e');
assert.lengthOf(resPins.shared, 0);
});
it('returns Either.Right VariadicPins with shared pins', () => {
const patch = Helper.defaultizePatch({
nodes: {
a: {
id: 'a',
type: 'xod/patch-nodes/variadic-1',
},
b: {
id: 'b',
type: 'xod/patch-nodes/input-number',
position: { x: 0, y: 0 },
},
c: {
id: 'c',
type: 'xod/patch-nodes/input-number',
position: { x: 10, y: 0 },
},
d: {
id: 'd',
type: 'xod/patch-nodes/input-number',
position: { x: 20, y: 0 },
},
e: {
id: 'e',
type: 'xod/patch-nodes/output-number',
},
},
});
const res = Patch.computeVariadicPins(patch);
assert.equal(res.isRight, true);
const resPins = XF.explodeEither(res);
assert.equal(resPins.value[0].key, 'd');
assert.equal(resPins.acc[0].key, 'c');
assert.equal(resPins.shared[0].key, 'b');
assert.equal(resPins.outputs[0].key, 'e');
});
});
});
describe('abstract patches', () => {
describe('validateAbstractPatch', () => {
it('should ignore regular patches', () => {
const patch = Helper.defaultizePatch({
nodes: {
foo: {
type: PPU.getLocalPath('whatever'),
},
},
});
Helper.expectEitherRight(
R.equals(patch),
Patch.validateAbstractPatch(patch)
);
});
it('should check that abstract patches have generic inputs', () => {
const patch = Helper.defaultizePatch({
nodes: {
a: {
type: CONST.ABSTRACT_MARKER_PATH,
},
},
});
Helper.expectEitherError(
CONST.ERROR.GENERIC_TERMINALS_REQUIRED,
Patch.validateAbstractPatch(patch)
);
});
it('should check that generic terminals are used sequentually', () => {
const patch = Helper.defaultizePatch({
nodes: {
a: {
type: CONST.ABSTRACT_MARKER_PATH,
},
b: {
type: PPU.getTerminalPath(
CONST.PIN_DIRECTION.INPUT,
CONST.PIN_TYPE.T1
),
},
c: {
type: PPU.getTerminalPath(
CONST.PIN_DIRECTION.INPUT,
CONST.PIN_TYPE.T3
),
},
},
});
Helper.expectEitherError(
'Generic inputs should be employed sequentially. Use t1, t2',
Patch.validateAbstractPatch(patch)
);
});
});
});
});