diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8ec874d9bbd407363e6264ae30611a3d358a684e..ffe1b195d489bf56fa2477e22389cd95a18bfb41 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,12 +9,10 @@ include: - apt-get update && apt-get install -y git default-jre - npm install - npm run build - - echo "@metabohub:registry=https://forgemia.inra.fr/api/v4/packages/npm/">.npmrc - - echo "//forgemia.inra.fr/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}">>.npmrc test: - image: node:latest + image: node:18 stage: test before_script: - npm install diff --git a/.npmrc b/.npmrc index f4c5e3ac00e2deb26b41d8c24d9d6a0548a754cb..9c9fc8246cf71c9b77fa234dd584e516bc037699 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,2 @@ -@metabohub:registry=https://forgemia.inra.fr/api/v4/packages/npm/ -//forgemia.inra.fr/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN} \ No newline at end of file +@metabohub:registry=https://forgemia.inra.fr/api/v4/projects/${CI_PROJECT_ID}/packages/npm/ +//forgemia.inra.fr/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN} diff --git a/src/composables/LayoutMainChain.ts b/src/composables/LayoutMainChain.ts index 9af059454f7de0cf2c48524fb1c8340d6bff001e..92cf43f3666e219fca07c9ed1f7cb86d820b4cbf 100644 --- a/src/composables/LayoutMainChain.ts +++ b/src/composables/LayoutMainChain.ts @@ -267,61 +267,80 @@ function findMaxKeys(obj: { [key: string]: number }): {key:string[],max:number} /** * Merge a new path in the object with all paths. If a common node is find between the new path and the other : the two paths are merged into one. - * Else the path is added as a new one. + * Else the path is added as a new one. If merge is false, one of the path with highest height is keeped. * @param source source node use to get the longest path from a source DAG * @param newPath path to add in the object with all paths * @param pathsFromSources object with all paths (array of node id with an path id) * @param [merge=true] if true, merge the path with an existing one if a common node is found * @returns all paths including the new one */ -async function mergeNewPath(source:string,newPath:{nodes:Array<string>, height:number},pathsFromSources:{[key:string]:{nodes:Array<string>, height:number}}, merge:boolean=true):Promise<{[key:string]:{nodes:Array<string>, height:number}}>{ - const keys=Object.keys(pathsFromSources).sort(); - let processed:boolean=false; - - // if no path in the object : add the new path - if (keys.length===0){ - pathsFromSources[source]=newPath; - return pathsFromSources; - } +async function mergeNewPath( source: string, newPath: { nodes: Array<string>, height: number }, pathsFromSources: { [key: string]: { nodes: Array<string>, height: number } }, merge: boolean = true ): Promise<{ [key: string]: { nodes: Array<string>, height: number } }> { + const keys = Object.keys(pathsFromSources).sort(); + let keysCommonNodes: string[] = []; - // for each path in the object - keys.forEach(key=>{ + // Searching paths with common nodes : keep their keys + keys.forEach(key => { const pathNodes = pathsFromSources[key].nodes; - // Check for common nodes + // common nodes ? const commonNodes = pathNodes.find(node => newPath.nodes.includes(node)); - if (commonNodes && commonNodes.length>0){ - processed=true; - if(merge){ - // Merge paths - const mergedPath = Array.from(new Set(pathNodes.concat(newPath.nodes))); - // Create new key if necessary - let newKey:string = key; - const sourceAlreadyInKey=key.split('__').includes(source); - if(!sourceAlreadyInKey){ - newKey= `${key}__${source}`; - } - // Update pathsFromSources object - const newheight=newPath.height>pathsFromSources[key].height?newPath.height:pathsFromSources[key].height; - pathsFromSources[newKey] = {nodes:mergedPath,height:newheight}; - - // Remove old key if the name has changed - if(!sourceAlreadyInKey){ - delete pathsFromSources[key]; + if (commonNodes) { + keysCommonNodes.push(key); + } + }); + + // if independant path + if (keysCommonNodes.length === 0) { + if (pathsFromSources[source]) throw new Error('source already in pathsFromSources, but supposed to have no common nodes'); + pathsFromSources[source] = newPath; + } else { + // if common nodes + if (merge) { + // if merge + let mergedPath = newPath.nodes; + let newHeight = newPath.height; + + keysCommonNodes.forEach(key => { + mergedPath = Array.from(new Set(mergedPath.concat(pathsFromSources[key].nodes))); + newHeight = Math.max(newHeight, pathsFromSources[key].height); + }); + + // suppression of keys with common nodes + keysCommonNodes.forEach(key => { + delete pathsFromSources[key]; + }); + + // adding new path (merged) + if (pathsFromSources[source]) throw new Error('newKey already exists in pathsFromSources : there is another path with same source but no common nodes'); + pathsFromSources[source] = { nodes: mergedPath, height: newHeight }; + } else { + // if no merge : keeping the path with the highest height + let maxHeight = newPath.height; + let maxHeightKey = source; + let maxPath = newPath.nodes; + + keysCommonNodes.forEach(key => { + if (pathsFromSources[key].height >= maxHeight) { + maxHeight = pathsFromSources[key].height; + maxPath = pathsFromSources[key].nodes; + maxHeightKey = key; } - }else{ - // Highest path is kept - if(newPath.height>pathsFromSources[key].height){ + }); + + // suppression of keys with common nodes, but the one that is keeped + keysCommonNodes.forEach(key => { + if (key !== maxHeightKey) { delete pathsFromSources[key]; - pathsFromSources[source]=newPath; } + }); + + // if the highest path is a path with the source node as key, + // either a previous one or the new one, do the update in case of the new one + if (maxHeightKey === source) { + pathsFromSources[source] = { nodes: maxPath, height: maxHeight }; } } - }); - if (!processed){ - // If no common nodes : path added on it's own - if(pathsFromSources[source]) throw new Error('source already in pathsFromSources, but supposed to have no common nodes'); - pathsFromSources[source]=newPath; } + return pathsFromSources; } diff --git a/src/composables/__tests__/LayoutMainChain.test.ts b/src/composables/__tests__/LayoutMainChain.test.ts index 44d2ffb0fd32cf7ca05fcfaa5b77181445c39c07..ba6b3722f5a528c8527fa821f7af597f5e88922b 100644 --- a/src/composables/__tests__/LayoutMainChain.test.ts +++ b/src/composables/__tests__/LayoutMainChain.test.ts @@ -460,7 +460,7 @@ describe('LayoutMainChain', () => { // DATA const pathExpected = { - A: { nodes: [ 'C','B','A','E','D' ], height: 3 } + A: { nodes: [ 'E','D','A','C','B' ], height: 3 } }; // TEST @@ -487,7 +487,7 @@ describe('LayoutMainChain', () => { // TEST const result = await LayoutMainChain.getPathSourcesToTargetNode(network, sources, false ,PathType.ALL_LONGEST); - + // EXPECT expect(DFSsourceDAGMock).toHaveBeenCalledTimes(1); expect(BFSMock).toHaveBeenCalledTimes(2); @@ -624,7 +624,7 @@ describe('LayoutMainChain', () => { // BFS start : H (DFS with D) BFSMock.mockReturnValueOnce([ 'H','F','E','G','D' ]); - const pathExpected = { A__D: { nodes: [ 'H','F','B','A','E','G','D' ], height: 5 } }; + const pathExpected = { D: { nodes: [ 'H','F','E','G','D','B','A' ], height: 5 } }; // TEST const result = await LayoutMainChain.getPathSourcesToTargetNode(network, sources, true ,PathType.ALL); @@ -687,33 +687,31 @@ describe('LayoutMainChain', () => { // DATA sources=["A","C"]; - // MOCK - graphGDS={ - adjacent:jest.fn((id: string) => { - switch (id) { - case 'A': - return ['B']; - case 'D': - return ['E','F']; - case 'C': - return ['D']; - default: - return []; - } - - }), - getEdgeWeight:jest.fn(() => (1)), - nodes: jest.fn(() => (['A', 'B', 'C', 'D', 'E', 'F'])) - } - - networkToGDSGraphMock.mockImplementation(async ()=>{ - return graphGDS; - }); - }); test ('case with one start nodes associated with 2 targets, and one start with one target, merge = true', async () => { - // MOCK + // MOCK + graphGDS={ + adjacent:jest.fn((id: string) => { + switch (id) { + case 'A': + return ['B']; + case 'D': + return ['E','F']; + case 'C': + return ['D']; + default: + return []; + } + + }), + getEdgeWeight:jest.fn(() => (1)), + nodes: jest.fn(() => (['A', 'B', 'C', 'D', 'E', 'F'])) + } + + networkToGDSGraphMock.mockImplementation(async ()=>{ + return graphGDS; + }); // DFS start : A DFSsourceDAGMock.mockReturnValueOnce(Promise.resolve( {dfs:[ @@ -737,7 +735,7 @@ describe('LayoutMainChain', () => { // BFS start : F (DFS with C) BFSMock.mockReturnValueOnce([ 'F','D','C' ]); - const pathExpected = {"A":{"nodes":["B","A"],"height":2},"C":{"nodes":["E","D","C","F"],"height":3}}; + const pathExpected = {"A":{"nodes":["B","A"],"height":2},"C":{"nodes":["F","D","C","E"],"height":3}}; // TEST const result = await LayoutMainChain.getPathSourcesToTargetNode(network, sources, true ,PathType.ALL); @@ -754,6 +752,128 @@ describe('LayoutMainChain', () => { }); + test ('case with one start nodes associated with 2 targets, and one start with one target of the first, merge = true', async () => { + // MOCK + graphGDS={ + adjacent:jest.fn((id: string) => { + switch (id) { + case 'A': + return ['D']; + case 'C': + return ['D','B']; + default: + return []; + } + + }), + getEdgeWeight:jest.fn(() => (1)), + nodes: jest.fn(() => (['A', 'B', 'C', 'D'])) + } + + networkToGDSGraphMock.mockImplementation(async ()=>{ + return graphGDS; + }); + // DFS start : A + DFSsourceDAGMock.mockReturnValueOnce(Promise.resolve( + {dfs:[ + 'A', 'D', + ], + graph:graphGDS} + )); + + // DFS start : C + DFSsourceDAGMock.mockReturnValueOnce(Promise.resolve( + {dfs:[ + 'C', 'D','B', + ], + graph:graphGDS} + )); + + // BFS start : D (DFS with A) + BFSMock.mockReturnValueOnce(['D','A' ]); + // BFS start : B (DFS with C) + BFSMock.mockReturnValueOnce([ 'B','C' ]); + // BFS start : D (DFS with C) + BFSMock.mockReturnValueOnce([ 'D','C' ]); + + const pathExpected = {"C":{"nodes":["D","C","A","B"],"height":2}}; + + // TEST + const result = await LayoutMainChain.getPathSourcesToTargetNode(network, sources, true ,PathType.ALL); + + // EXPECT + expect(DFSsourceDAGMock).toHaveBeenCalledTimes(2); + expect(DFSsourceDAGMock).toHaveBeenCalledWith(expect.anything(),['A']); + expect(DFSsourceDAGMock).toHaveBeenLastCalledWith(expect.anything(),['C']); + expect(BFSMock).toHaveBeenCalledTimes(3); + expect(BFSMock).toHaveBeenCalledWith({"A":[],"D":["A"]}, 'D'); + expect(BFSMock).toHaveBeenCalledWith({"C":[],"D":["C"],"B":["C"]}, 'B'); + expect(BFSMock).toHaveBeenLastCalledWith({"C":[],"D":["C"],"B":["C"]}, 'D'); + expect(result).toEqual(pathExpected); + + }); + + test ('case with one start nodes associated with 2 targets, and one start with one target of the first, merge = false', async () => { + // MOCK + graphGDS={ + adjacent:jest.fn((id: string) => { + switch (id) { + case 'A': + return ['D']; + case 'C': + return ['D','B']; + default: + return []; + } + + }), + getEdgeWeight:jest.fn(() => (1)), + nodes: jest.fn(() => (['A', 'B', 'C', 'D'])) + } + + networkToGDSGraphMock.mockImplementation(async ()=>{ + return graphGDS; + }); + // DFS start : A + DFSsourceDAGMock.mockReturnValueOnce(Promise.resolve( + {dfs:[ + 'A', 'D', + ], + graph:graphGDS} + )); + + // DFS start : C + DFSsourceDAGMock.mockReturnValueOnce(Promise.resolve( + {dfs:[ + 'C', 'D','B', + ], + graph:graphGDS} + )); + + // BFS start : D (DFS with A) + BFSMock.mockReturnValueOnce(['D','A' ]); + // BFS start : B (DFS with C) + BFSMock.mockReturnValueOnce([ 'B','C' ]); + // BFS start : D (DFS with C) + BFSMock.mockReturnValueOnce([ 'D','C' ]); + + const pathExpected = {"C":{"nodes":["B","C"],"height":2}}; + + // TEST + const result = await LayoutMainChain.getPathSourcesToTargetNode(network, sources, false ,PathType.ALL); + + // EXPECT + expect(DFSsourceDAGMock).toHaveBeenCalledTimes(2); + expect(DFSsourceDAGMock).toHaveBeenCalledWith(expect.anything(),['A']); + expect(DFSsourceDAGMock).toHaveBeenLastCalledWith(expect.anything(),['C']); + expect(BFSMock).toHaveBeenCalledTimes(3); + expect(BFSMock).toHaveBeenCalledWith({"A":[],"D":["A"]}, 'D'); + expect(BFSMock).toHaveBeenCalledWith({"C":[],"D":["C"],"B":["C"]}, 'B'); + expect(BFSMock).toHaveBeenLastCalledWith({"C":[],"D":["C"],"B":["C"]}, 'D'); + expect(result).toEqual(pathExpected); + + }); + });