import { createSankey } from '~/pipelines/components/dag/drawing_utils'; import { makeLinksFromNodes, filterByAncestors, generateColumnsFromLayersListBare, listByLayers, parseData, removeOrphanNodes, getMaxNodes, } from '~/pipelines/components/parsing_utils'; import { createNodeDict, calculateJobStats, calculateSlowestFiveJobs } from '~/pipelines/utils'; import { mockParsedGraphQLNodes, missingJob } from './components/dag/mock_data'; import { generateResponse, mockPipelineResponse, mockPerformanceInsightsResponse, } from './graph/mock_data'; describe('DAG visualization parsing utilities', () => { const nodeDict = createNodeDict(mockParsedGraphQLNodes); const unfilteredLinks = makeLinksFromNodes(mockParsedGraphQLNodes, nodeDict); const parsed = parseData(mockParsedGraphQLNodes); describe('makeLinksFromNodes', () => { it('returns the expected link structure', () => { expect(unfilteredLinks[0]).toHaveProperty('source', 'build_a'); expect(unfilteredLinks[0]).toHaveProperty('target', 'test_a'); expect(unfilteredLinks[0]).toHaveProperty('value', 10); }); it('does not generate a link for non-existing jobs', () => { const sources = unfilteredLinks.map(({ source }) => source); expect(sources.includes(missingJob)).toBe(false); }); }); describe('filterByAncestors', () => { const allLinks = [ { source: 'job1', target: 'job4' }, { source: 'job1', target: 'job2' }, { source: 'job2', target: 'job4' }, ]; const dedupedLinks = [ { source: 'job1', target: 'job2' }, { source: 'job2', target: 'job4' }, ]; const nodeLookup = { job1: { name: 'job1', }, job2: { name: 'job2', needs: ['job1'], }, job4: { name: 'job4', needs: ['job1', 'job2'], category: 'build', }, }; it('dedupes links', () => { expect(filterByAncestors(allLinks, nodeLookup)).toMatchObject(dedupedLinks); }); }); describe('parseData parent function', () => { it('returns an object containing a list of nodes and links', () => { // an array of nodes exist and the values are defined expect(parsed).toHaveProperty('nodes'); expect(Array.isArray(parsed.nodes)).toBe(true); expect(parsed.nodes.filter(Boolean)).not.toHaveLength(0); // an array of links exist and the values are defined expect(parsed).toHaveProperty('links'); expect(Array.isArray(parsed.links)).toBe(true); expect(parsed.links.filter(Boolean)).not.toHaveLength(0); }); }); describe('removeOrphanNodes', () => { it('removes sankey nodes that have no needs and are not needed', () => { const layoutSettings = { width: 200, height: 200, nodeWidth: 10, nodePadding: 20, paddingForLabels: 100, }; const sankeyLayout = createSankey(layoutSettings)(parsed); const cleanedNodes = removeOrphanNodes(sankeyLayout.nodes); /* These lengths are determined by the mock data. If the data changes, the numbers may also change. */ expect(parsed.nodes).toHaveLength(mockParsedGraphQLNodes.length); expect(cleanedNodes).toHaveLength(12); }); }); describe('getMaxNodes', () => { it('returns the number of nodes in the most populous generation', () => { const layerNodes = [ { layer: 0 }, { layer: 0 }, { layer: 1 }, { layer: 1 }, { layer: 0 }, { layer: 3 }, { layer: 2 }, { layer: 4 }, { layer: 1 }, { layer: 3 }, { layer: 4 }, ]; expect(getMaxNodes(layerNodes)).toBe(3); }); }); describe('generateColumnsFromLayersList', () => { const pipeline = generateResponse(mockPipelineResponse, 'root/fungi-xoxo'); const { pipelineLayers } = listByLayers(pipeline); const columns = generateColumnsFromLayersListBare(pipeline, pipelineLayers); it('returns stage-like objects with default name, id, and status', () => { columns.forEach((col, idx) => { expect(col).toMatchObject({ name: '', status: { action: null }, id: `layer-${idx}`, }); }); }); it('creates groups that match the list created in listByLayers', () => { columns.forEach((col, idx) => { const groupNames = col.groups.map(({ name }) => name); expect(groupNames).toEqual(pipelineLayers[idx]); }); }); it('looks up the correct group object', () => { columns.forEach((col) => { col.groups.forEach((group) => { const groupStage = pipeline.stages.find((el) => el.name === group.stageName); const groupObject = groupStage.groups.find((el) => el.name === group.name); expect(group).toBe(groupObject); }); }); }); /* Just as a fallback in case multiple functions change, so tests pass but the implementation moves away from case. */ it('matches the snapshot', () => { expect(columns).toMatchSnapshot(); }); }); describe('performance insights', () => { const { data: { project: { pipeline: { jobs }, }, }, } = mockPerformanceInsightsResponse; describe('calculateJobStats', () => { const expectedJob = jobs.nodes[0]; it('returns the job that spent this longest time queued', () => { expect(calculateJobStats(jobs, 'queuedDuration')).toEqual(expectedJob); }); it('returns the job that was executed last', () => { expect(calculateJobStats(jobs, 'startedAt')).toEqual(expectedJob); }); }); describe('calculateSlowestFiveJobs', () => { it('returns the slowest five jobs of the pipeline', () => { const expectedJobs = [ jobs.nodes[9], jobs.nodes[1], jobs.nodes[5], jobs.nodes[7], jobs.nodes[8], ]; expect(calculateSlowestFiveJobs(jobs)).toEqual(expectedJobs); }); }); }); });