feat,test(xod-project): first step in flatten graph issue

This commit is contained in:
Kirill Shumilov
2017-01-18 21:30:56 +03:00
parent 81ebe36a33
commit eef1d6ee82
10 changed files with 669 additions and 10 deletions

View File

@@ -31,6 +31,7 @@ export const ERROR = {
PIN_KEY_INVALID: 'Pin key should be generated with shortid',
// other
DATATYPE_INVALID: 'Invalid data type',
IMPLEMENTATION_NOT_FOUND: 'Implementation not found',
};
/**

View File

@@ -0,0 +1,77 @@
import R from 'ramda';
import { Maybe } from 'ramda-fantasy';
import * as Project from './project';
import * as Patch from './patch';
import * as Node from './node';
// :: Monad a -> Monad a
const reduceChainOver = R.reduce(R.flip(R.chain));
// :: String[] -> Project -> Path -> [Path, Patch, ...]
const extractImplPatches = R.curry((impls, project, path, patch) => R.ifElse(
Patch.hasImpl(impls),
patchWithImpl => [path, patchWithImpl],
R.compose(
R.chain(R.compose(
type => Project.getPatchByPath(type, project)
.chain(extractImplPatches(impls, project, type)),
Node.getNodeType
)),
Patch.listNodes
)
)(patch));
// :: Node -> Boolean
const isNodeToImplPatch = implPatchPaths => R.compose(
R.contains(R.__, implPatchPaths),
Node.getNodeType
);
// :: Project -> String[] -> Patch -> Node[]
const extractNodes = (project, implPatchPaths) => R.compose(
R.chain(R.ifElse(
isNodeToImplPatch(implPatchPaths),
R.identity,
R.compose(
R.chain(patch => extractNodes(project, implPatchPaths)(patch)),
type => Project.getPatchByPath(type, project),
Node.getNodeType
)
)
),
Patch.listNodes
);
export default R.curry((project, path, impls) => {
const patch = Project.getPatchByPath(path, project);
const implPatches = patch
.map(extractImplPatches(impls, project, path))
.chain(R.splitEvery(2));
const assocImplPatches = implPatches.map(R.apply(Project.assocPatch));
let assocPatch = R.identity;
if (!patch.chain(Patch.hasImpl(impls))) {
const implPatchPaths = R.pluck(0, implPatches);
const nodes = patch.chain(extractNodes(project, implPatchPaths));
const assocNodes = nodes.map(node => Patch.assocNode(node));
const newPatch = R.reduce(
(p, fn) => fn(p),
Patch.createPatch(),
assocNodes
);
assocPatch = R.compose(
R.chain,
Project.assocPatch(path)
)(newPatch);
}
return R.compose(
assocPatch,
reduceChainOver(R.__, assocImplPatches),
Maybe.of
)(Project.createProject());
});

View File

@@ -51,19 +51,33 @@ export const setPatchLabel = R.useWith(
);
/**
* Returns a list of platforms for which a `patch` has native implementation
* Returns a list of implementations for which a `patch` has native implementation
*
* For example, `['js', 'arduino', 'espruino', 'nodejs']`.
*
* @function listPatchPlatforms
* @function listImpls
* @param {Patch} patch
* @returns {string[]}
*/
export const listPatchPlatforms = R.compose(
export const listImpls = R.compose(
R.keys,
R.propOr({}, 'impls')
);
/**
* Returns true if patch has any of specified implementations.
*
* @function hasImpl
* @param {string[]} impls
* @param {Patch} patch
* @type {Boolean}
*/
export const hasImpl = R.curry((impls, patch) => R.compose(
R.complement(R.isEmpty),
R.intersection(impls),
listImpls
)(patch));
/**
* @function validatePatch
* @param {Patch} patch

View File

@@ -141,6 +141,16 @@ export const listPatches = R.compose(
getPatches
);
/**
* @function listPatchPaths
* @param {Project} project - project bundle
* @returns {String[]} list of all patche paths not sorted in any arbitrary order
*/
export const listPatchPaths = R.compose(
R.keys,
getPatches
);
/**
* Return a list of local patches (excluding external libraries)
*

View File

@@ -0,0 +1,134 @@
const projectB = {
patches: {
'@/main': {
nodes: {
// blink
'a/a': {
id: 'a/a',
type: 'xod/core/clock',
pins: {
interval: {
injected: true,
value: 0.2,
},
},
},
'a/b': {
id: 'a/b',
type: 'xod/core/latch',
},
'a/generatedNodeId__out': {
id: 'a/generatedNodeId__out',
type: 'xod/core/outputNumber',
},
// led
'b/a': {
id: 'b/a',
type: 'xod/core/multiply',
},
'b/b': {
id: 'b/b',
type: 'xod/core/pwm-output',
pins: {
'hardware-pin': {
injected: true,
value: 13,
},
},
},
},
links: {
// blink
'a/b': {
id: 'a/b',
output: {
nodeId: 'a/a',
pinKey: 'tick',
},
input: {
nodeId: 'a/b',
pinKey: 'toggle',
},
},
// blink to led
'a-b/c': {
id: 'a-b/c',
output: {
nodeId: 'a/b',
pinKey: 'out',
},
input: {
nodeId: 'b/a',
pinKey: 'in1',
},
},
'a-b/d': {
id: 'a-b/d',
output: {
nodeId: 'a/b',
pinKey: 'out',
},
input: {
nodeId: 'b/a',
pinKey: 'in2',
},
},
// led
'b/c': {
id: 'b/c',
output: {
nodeId: 'b/a',
pinKey: 'out',
},
input: {
nodeId: 'b/b',
pinKey: 'duty',
},
},
},
},
'xod/core/multiply': {
nodes: {},
links: {},
pins: {
in1: {
key: 'in1',
type: 'number',
direction: 'input',
},
in2: {
key: 'in2',
type: 'number',
direction: 'input',
},
out: {
key: 'out',
type: 'number',
direction: 'output',
},
},
impls: {
js: '//ok',
},
},
'xod/core/pwm-output': {
nodes: {},
links: {},
pins: {
duty: {
key: 'duty',
type: 'number',
direction: 'input',
},
'hardware-pin': {
key: 'hardware-pin',
type: 'number',
direction: 'input',
},
},
impls: {
js: '//ok',
},
},
},
};

View File

@@ -0,0 +1,244 @@
const projectA = {
patches: {
'@/main': {
nodes: {
a: {
id: 'a',
type: '@/blink',
pins: {
generatedNodeId__interval: {
injected: true,
value: 0.2,
},
},
},
b: {
id: 'b',
type: 'xod/core/led',
pins: {
generatedNodeId__hardwarePin: {
injected: true,
value: 13,
},
},
},
},
links: {
a: {
id: 'a',
output: {
nodeId: 'a',
pinKey: 'generatedNodeId__out',
},
input: {
nodeId: 'b',
pinKey: 'generatedNodeId__interval',
},
},
},
},
'@/blink': {
nodes: {
generatedNodeId__interval: {
id: 'generatedNodeId__interval',
type: 'xod/core/inputNumber',
},
a: {
id: 'a',
type: 'xod/core/clock',
},
b: {
id: 'b',
type: 'xod/core/latch',
},
generatedNodeId__out: {
id: 'generatedNodeId__out',
type: 'xod/core/outputNumber',
},
},
links: {
a: {
id: 'a',
output: {
nodeId: 'generatedNodeId__interval',
pinKey: 'out',
},
input: {
nodeId: 'a',
pinKey: 'interval',
},
},
b: {
id: 'b',
output: {
nodeId: 'a',
pinKey: 'tick',
},
input: {
nodeId: 'b',
pinKey: 'toggle',
},
},
c: {
id: 'c',
output: {
nodeId: 'b',
pinKey: 'out',
},
input: {
nodeId: 'generatedNodeId__out',
pinKey: 'in',
},
},
},
pins: {
generatedNodeId__interval: {
key: 'generatedNodeId__interval',
type: 'number',
direction: 'input',
},
generatedNodeId__out: {
key: 'generatedNodeId__out',
type: 'number',
direction: 'output',
},
},
},
'xod/core/led': {
nodes: {
generatedNodeId__brightness: {
id: 'generatedNodeId__brightness',
type: 'xod/core/inputNumber',
},
generatedNodeId__hardwarePin: {
id: 'generatedNodeId__hardwarePin',
type: 'xod/core/inputNumber',
},
a: {
id: 'a',
type: 'xod/core/multiply',
},
b: {
id: 'b',
type: 'xod/core/pwm-output',
},
},
links: {
a: {
id: 'a',
output: {
nodeId: 'generatedNodeId__brightness',
pinKey: 'out',
},
input: {
nodeId: 'a',
pinKey: 'in1',
},
},
b: {
id: 'b',
output: {
nodeId: 'generatedNodeId__brightness',
pinKey: 'out',
},
input: {
nodeId: 'a',
pinKey: 'in2',
},
},
c: {
id: 'c',
output: {
nodeId: 'a',
pinKey: 'out',
},
input: {
nodeId: 'b',
pinKey: 'duty',
},
},
d: {
id: 'd',
output: {
nodeId: 'generatedNodeId__hardwarePin',
pinKey: 'out',
},
input: {
nodeId: 'b',
pinKey: 'hardware-pin',
},
},
},
pins: {
generatedNodeId__brightness: {
key: 'generatedNodeId__brightness',
type: 'number',
direction: 'input',
},
generatedNodeId__hardwarePin: {
key: 'generatedNodeId__hardwarePin',
type: 'string',
direction: 'input',
},
},
},
'xod/core/inputNumber': {
nodes: {},
links: {},
pins: {
out: {
key: 'out',
type: 'number',
direction: 'output',
},
},
},
'xod/core/multiply': {
nodes: {},
links: {},
pins: {
in1: {
key: 'in1',
type: 'number',
direction: 'input',
},
in2: {
key: 'in2',
type: 'number',
direction: 'input',
},
out: {
key: 'out',
type: 'number',
direction: 'output',
},
},
},
'xod/core/pwm-output': {
nodes: {},
links: {},
pins: {
duty: {
key: 'duty',
type: 'number',
direction: 'input',
},
'hardware-pin': {
key: 'hardware-pin',
type: 'number',
direction: 'input',
},
},
impls: {
js: '//ok',
},
},
'xod/core/or': {
nodes: {},
links: {},
impls: {
js: '//ok',
},
},
},
};

View File

@@ -7,7 +7,7 @@
"patches": {
"xod/core/inputNumber": {
"nodes": {},
"links": [],
"links": {},
"label": "<InputNumber>",
"pins": {
"PIN": {
@@ -19,7 +19,7 @@
},
"xod/core/pot": {
"nodes": {},
"links": [],
"links": {},
"pins": {
"sample": {
"key": "sample",
@@ -38,7 +38,7 @@
},
"xod/core/and": {
"nodes": {},
"links": [],
"links": {},
"label": "and",
"pins": {
"a": {
@@ -63,7 +63,7 @@
},
"xod/core/led": {
"nodes": {},
"links": [],
"links": {},
"label": "LED",
"pins": {
"brightness": {

View File

@@ -0,0 +1,138 @@
import R from 'ramda';
import chai, { expect } from 'chai';
import dirtyChai from 'dirty-chai';
import * as Helper from './helpers';
import * as CONST from '../src/constants';
import flatten from '../src/flatten';
chai.use(dirtyChai);
describe('Flatten', () => {
describe('trivial', () => {
const project = {
patches: {
'@/main': {
nodes: {
a: {
id: 'a',
type: 'xod/core/or',
},
},
links: {},
},
'xod/core/or': {
nodes: {},
links: {},
impls: {
js: '//ok',
},
},
},
};
it('should ignore not referred patches', () => {
const flatProject = flatten(project, 'xod/core/or', ['js']);
expect(flatProject.isRight).to.be.true();
Helper.expectEither(
(newProject) => {
expect(R.keys(newProject.patches))
.to.be.deep.equal(['xod/core/or']);
expect(newProject.patches['xod/core/or'])
.to.be.deep.equal(project.patches['xod/core/or']);
},
flatProject
);
});
it('should return patch with dependency', () => {
const flatProject = flatten(project, '@/main', ['js']);
expect(flatProject.isRight).to.be.true();
Helper.expectEither(
(newProject) => {
expect(R.keys(newProject.patches))
.to.be.deep.equal(['xod/core/or', '@/main']);
expect(newProject.patches)
.to.be.deep.equal(project.patches);
},
flatProject
);
});
// its('should return error if implementation not found', () => {
// const flatProject = flatten(project, '@/main', ['cpp']);
//
// expect(flatProject.isLeft).to.be.true();
// Helper.expectErrorMessage(expect, flatProject, CONST.ERROR.IMPLEMENTATION_NOT_FOUND);
// });
});
describe('recursive', () => {
const project = {
patches: {
'@/main': {
nodes: {
a: {
id: 'a',
type: '@/foo',
},
},
links: {},
},
'@/foo': {
nodes: {
a: {
id: 'a',
type: 'xod/core/or',
},
},
links: {},
},
'xod/core/or': {
nodes: {},
links: {},
impls: {
js: '//ok',
},
},
},
};
it('should ignore not referred patches', () => {
const flatProject = flatten(project, '@/foo', ['js']);
expect(flatProject.isRight).to.be.true();
Helper.expectEither(
(newProject) => {
expect(R.keys(newProject.patches))
.to.be.deep.equal(['xod/core/or', '@/foo']);
expect(newProject.patches['xod/core/or'])
.to.be.deep.equal(project.patches['xod/core/or']);
expect(newProject.patches['@/foo'])
.to.be.deep.equal(project.patches['@/foo']);
},
flatProject
);
});
it('should return patch with dependency', () => {
const flatProject = flatten(project, '@/main', ['js']);
expect(flatProject.isRight).to.be.true();
Helper.expectEither(
(newProject) => {
expect(R.keys(newProject.patches))
.to.be.deep.equal(['xod/core/or', '@/main']);
expect(newProject.patches['xod/core/or'])
.to.be.deep.equal(project.patches['xod/core/or']);
expect(R.values(newProject.patches['@/main'].nodes)[0])
.to.have.property('type')
.that.equals('xod/core/or');
},
flatProject
);
});
});
});

View File

@@ -89,9 +89,9 @@ describe('Patch', () => {
.that.equals('[object Object]');
});
});
describe('listPatchPlatforms', () => {
describe('listImpls', () => {
it('should return empty array for empty patch', () => {
expect(Patch.listPatchPlatforms({}))
expect(Patch.listImpls({}))
.to.be.instanceof(Array)
.to.be.empty();
});
@@ -102,11 +102,41 @@ describe('Patch', () => {
espruino: '',
},
};
expect(Patch.listPatchPlatforms(patch))
expect(Patch.listImpls(patch))
.to.be.an('array')
.to.have.members(['js', 'espruino']);
});
});
describe('hasImpl', () => {
it('should return false for empty', () => {
expect(Patch.hasImpl(['js'], {})).to.be.false();
});
it('should return false if impl not found', () => {
const patch = {
impls: {
js: '//ok',
},
};
expect(Patch.hasImpl(['cpp'], patch)).to.be.false();
});
it('should return true for the only correct impl', () => {
const patch = {
impls: {
js: '//ok',
},
};
expect(Patch.hasImpl(['js'], patch)).to.be.true();
});
it('should return true for a few existent impls', () => {
const patch = {
impls: {
js: '//ok',
nodejs: '//ok',
},
};
expect(Patch.hasImpl(['js', 'nodejs'], patch)).to.be.true();
});
});
// entity getters
describe('listNodes', () => {

View File

@@ -446,6 +446,17 @@ describe('Project', () => {
.and.have.lengthOf(2);
});
});
describe('listPatchPaths', () => {
it('should return empty array for empty project', () => {
expect(Project.listPatchPaths({}))
.to.be.instanceof(Array)
.and.to.be.empty();
});
it('should return array with two keys', () => {
expect(Project.listPatchPaths(project))
.to.be.deep.equal(['@/test', 'external/patch']);
});
});
describe('listLocalPatches', () => {
it('should return empty array for empty project', () => {
expect(Project.listLocalPatches({}))