teableio/teable

View on GitHub
apps/nestjs-backend/src/features/calculation/utils/dfs.spec.ts

Summary

Maintainability
A
0 mins
Test Coverage
import type { IGraphItem } from './dfs';
import {
  buildAdjacencyMap,
  buildCompressedAdjacencyMap,
  hasCycle,
  pruneGraph,
  topoOrderWithDepends,
  topoOrderWithStart,
  topologicalSort,
} from './dfs';

describe('Graph Processing Functions', () => {
  describe('buildAdjacencyMap', () => {
    it('should create an adjacency map from a graph', () => {
      const graph = [
        { fromFieldId: 'a', toFieldId: 'b' },
        { fromFieldId: 'b', toFieldId: 'c' },
      ];
      const expected = {
        a: ['b'],
        b: ['c'],
      };
      expect(buildAdjacencyMap(graph)).toEqual(expected);
    });

    it('should handle graphs with multiple edges from a single node', () => {
      const graph = [
        { fromFieldId: 'a', toFieldId: 'b' },
        { fromFieldId: 'a', toFieldId: 'c' },
      ];
      const expected = {
        a: ['b', 'c'],
      };
      expect(buildAdjacencyMap(graph)).toEqual(expected);
    });

    it('should return an empty object for an empty graph', () => {
      expect(buildAdjacencyMap([])).toEqual({});
    });
  });

  describe('buildCompressedAdjacencyMap', () => {
    it('should compress a graph based on linkIdSet', () => {
      const graph = [
        { fromFieldId: 'id1', toFieldId: 'id2' },
        { fromFieldId: 'id2', toFieldId: 'id3' },
        { fromFieldId: 'id2', toFieldId: 'id4' },
        { fromFieldId: 'id3', toFieldId: 'id5' },
      ];
      const linkIdSet = new Set(['id2', 'id4', 'id5']);
      const expected = {
        id1: ['id2'],
        id2: ['id5', 'id4'],
        id3: ['id5'],
      };
      expect(buildCompressedAdjacencyMap(graph, linkIdSet)).toEqual(expected);
    });

    it('should compress a graph based on linkIdSet with multiple entry', () => {
      const graph = [
        { fromFieldId: 'id1', toFieldId: 'id2' },
        { fromFieldId: 'id2', toFieldId: 'id3' },
        { fromFieldId: 'id2', toFieldId: 'id4' },
      ];
      const linkIdSet = new Set(['id3', 'id4']);
      const expected = {
        id1: ['id3', 'id4'],
        id2: ['id3', 'id4'],
      };
      expect(buildCompressedAdjacencyMap(graph, linkIdSet)).toEqual(expected);
    });

    it('should handle empty linkIdSet', () => {
      const graph = [
        { fromFieldId: 'id1', toFieldId: 'id2' },
        { fromFieldId: 'id2', toFieldId: 'id3' },
      ];
      expect(buildCompressedAdjacencyMap(graph, new Set())).toEqual({});
    });

    it('should handle graphs with no valid paths', () => {
      const graph = [
        { fromFieldId: 'id1', toFieldId: 'id2' },
        { fromFieldId: 'id2', toFieldId: 'id3' },
      ];
      const linkIdSet = new Set(['id4']);
      expect(buildCompressedAdjacencyMap(graph, linkIdSet)).toEqual({});
    });
  });

  describe('buildCompressedAdjacencyMap with unordered graph', () => {
    it('should handle graphs with unordered edges', () => {
      const graph = [
        { fromFieldId: 'id3', toFieldId: 'id5' },
        { fromFieldId: 'id1', toFieldId: 'id2' },
        { fromFieldId: 'id2', toFieldId: 'id4' },
        { fromFieldId: 'id2', toFieldId: 'id3' },
      ];
      const linkIdSet = new Set(['id2', 'id4', 'id5']);
      const expected = {
        id1: ['id2'],
        id2: ['id4', 'id5'],
        id3: ['id5'],
      };
      expect(buildCompressedAdjacencyMap(graph, linkIdSet)).toEqual(expected);
    });
  });

  describe('topologicalSort', () => {
    it('should perform a basic topological sort', () => {
      const graph: IGraphItem[] = [
        { fromFieldId: 'a', toFieldId: 'b' },
        { fromFieldId: 'b', toFieldId: 'c' },
      ];
      expect(topologicalSort(graph)).toEqual(['a', 'b', 'c']);
    });

    it('should perform a branched topological sort', () => {
      const graph: IGraphItem[] = [
        { fromFieldId: 'a', toFieldId: 'b' },
        { fromFieldId: 'a', toFieldId: 'c' },
        { fromFieldId: 'b', toFieldId: 'c' },
        { fromFieldId: 'b', toFieldId: 'd' },
      ];
      expect(topologicalSort(graph)).toEqual(['a', 'b', 'd', 'c']);
    });

    it('should handle an empty graph', () => {
      const graph: IGraphItem[] = [];
      expect(topologicalSort(graph)).toEqual([]);
    });

    it('should handle a graph with a single circular node', () => {
      const graph: IGraphItem[] = [{ fromFieldId: 'a', toFieldId: 'a' }];
      expect(() => topologicalSort(graph)).toThrowError();
    });

    it('should handle graphs with circular dependencies', () => {
      const graph: IGraphItem[] = [
        { fromFieldId: 'a', toFieldId: 'b' },
        { fromFieldId: 'b', toFieldId: 'a' },
      ];
      expect(() => topologicalSort(graph)).toThrowError();
    });
  });

  describe('topoOrderWithDepends', () => {
    it('should return an empty array for an empty graph', () => {
      const result = topoOrderWithDepends('anyNodeId', []);
      expect(result).toEqual([
        {
          id: 'anyNodeId',
          dependencies: [],
        },
      ]);
    });

    it('should handle circular single node graph correctly', () => {
      const graph: IGraphItem[] = [{ fromFieldId: '1', toFieldId: '1' }];
      expect(() => topoOrderWithDepends('1', graph)).toThrowError();
    });

    it('should handle circular node graph correctly', () => {
      const graph: IGraphItem[] = [
        { fromFieldId: '1', toFieldId: '2' },
        { fromFieldId: '2', toFieldId: '1' },
      ];
      expect(() => topoOrderWithDepends('1', graph)).toThrowError();
    });

    it('should return correct order for a normal DAG', () => {
      const graph: IGraphItem[] = [
        { fromFieldId: '1', toFieldId: '2' },
        { fromFieldId: '2', toFieldId: '3' },
      ];
      const result = topoOrderWithDepends('1', graph);
      expect(result).toEqual([
        { id: '1', dependencies: [] },
        { id: '2', dependencies: ['1'] },
        { id: '3', dependencies: ['2'] },
      ]);
    });

    it('should return correct order for a complex DAG', () => {
      const graph: IGraphItem[] = [
        { fromFieldId: '1', toFieldId: '2' },
        { fromFieldId: '2', toFieldId: '3' },
        { fromFieldId: '1', toFieldId: '3' },
        { fromFieldId: '3', toFieldId: '4' },
      ];
      const result = topoOrderWithDepends('1', graph);
      expect(result).toEqual([
        { id: '1', dependencies: [] },
        { id: '2', dependencies: ['1'] },
        { id: '3', dependencies: ['2', '1'] },
        { id: '4', dependencies: ['3'] },
      ]);
    });
  });

  describe('hasCycle', () => {
    it('should return false for an empty graph', () => {
      expect(hasCycle([])).toBe(false);
    });

    it('should return true for a single node graph link to self', () => {
      const graph = [{ fromFieldId: '1', toFieldId: '1' }];
      expect(hasCycle(graph)).toBe(true);
    });

    it('should return false for a normal DAG without cycles', () => {
      const graph = [
        { fromFieldId: '1', toFieldId: '2' },
        { fromFieldId: '2', toFieldId: '3' },
      ];
      expect(hasCycle(graph)).toBe(false);
    });

    it('should return true for a graph with a cycle', () => {
      const graph = [
        { fromFieldId: '1', toFieldId: '2' },
        { fromFieldId: '2', toFieldId: '3' },
        { fromFieldId: '3', toFieldId: '1' }, // creates a cycle
      ];
      expect(hasCycle(graph)).toBe(true);
    });
  });

  describe('topoOrderWithStart', () => {
    it('should return correct order for a normal DAG', () => {
      const graph: IGraphItem[] = [
        { fromFieldId: '1', toFieldId: '2' },
        { fromFieldId: '2', toFieldId: '3' },
      ];
      const result = topoOrderWithStart('1', graph);
      expect(result).toEqual(['1', '2', '3']);
    });

    it('should return correct order for a complex DAG', () => {
      const graph: IGraphItem[] = [
        { fromFieldId: '1', toFieldId: '2' },
        { fromFieldId: '2', toFieldId: '3' },
        { fromFieldId: '1', toFieldId: '3' },
        { fromFieldId: '3', toFieldId: '4' },
      ];
      const result = topoOrderWithStart('1', graph);
      expect(result).toEqual(['1', '2', '3', '4']);
    });
  });

  describe('pruneGraph', () => {
    test('returns an empty array for an empty graph', () => {
      expect(pruneGraph('A', [])).toEqual([]);
    });

    test('returns correct graph for a single-node graph', () => {
      const graph: IGraphItem[] = [{ fromFieldId: 'A', toFieldId: 'B' }];
      expect(pruneGraph('A', graph)).toEqual(graph);
    });

    test('returns correct graph for a tow-node graph', () => {
      const graph: IGraphItem[] = [
        { fromFieldId: 'A', toFieldId: 'C' },
        { fromFieldId: 'B', toFieldId: 'C' },
      ];
      expect(pruneGraph('C', graph)).toEqual(graph);
    });

    test('returns correct graph for a multi-node graph', () => {
      const graph: IGraphItem[] = [
        { fromFieldId: 'A', toFieldId: 'B' },
        { fromFieldId: 'B', toFieldId: 'C' },
        { fromFieldId: 'C', toFieldId: 'D' },
        { fromFieldId: 'E', toFieldId: 'F' },
      ];
      const expectedResult: IGraphItem[] = [
        { fromFieldId: 'A', toFieldId: 'B' },
        { fromFieldId: 'B', toFieldId: 'C' },
        { fromFieldId: 'C', toFieldId: 'D' },
      ];
      expect(pruneGraph('A', graph)).toEqual(expectedResult);
    });

    test('returns an empty array for a graph with unrelated node', () => {
      const graph: IGraphItem[] = [
        { fromFieldId: 'B', toFieldId: 'C' },
        { fromFieldId: 'C', toFieldId: 'D' },
      ];
      expect(pruneGraph('A', graph)).toEqual([]);
    });
  });
});