mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
222 lines
9.4 KiB
TypeScript
222 lines
9.4 KiB
TypeScript
/**
|
|
* COBOL parsing-coverage regression tests (F17-F23 from issue #1925).
|
|
*
|
|
* Each finding has its own fixture file and exact-count assertions.
|
|
* These tests must FAIL on main and PASS on the fix branch.
|
|
*/
|
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
import path from 'path';
|
|
import {
|
|
FIXTURES,
|
|
getRelationships,
|
|
getNodesByLabel,
|
|
runPipelineFromRepo,
|
|
type PipelineResult,
|
|
} from './helpers.js';
|
|
|
|
const COVERAGE_FIXTURE = path.join(FIXTURES, 'cobol-parsing-coverage');
|
|
|
|
describe('COBOL parsing coverage (F17-F23)', () => {
|
|
let result: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
result = await runPipelineFromRepo(COVERAGE_FIXTURE, () => {}, {
|
|
skipGraphPhases: true,
|
|
});
|
|
}, 60000);
|
|
|
|
// =====================================================================
|
|
// F17: Digit-leading paragraph names
|
|
// =====================================================================
|
|
describe('F17 — digit-leading paragraph names', () => {
|
|
const DIGITLEAD_FUNCS = ['1000-MAIN', '2000-READ-FILE', '2100-PROCESS', '9000-EXIT'];
|
|
|
|
it('captures digit-leading paragraphs as Function nodes', () => {
|
|
const funcs = getNodesByLabel(result, 'Function');
|
|
for (const name of DIGITLEAD_FUNCS) {
|
|
expect(funcs).toContain(name);
|
|
}
|
|
});
|
|
|
|
it('PERFORM 2000-READ-FILE resolves to correct paragraph', () => {
|
|
const perfCalls = getRelationships(result, 'CALLS').filter(
|
|
(e) => e.rel.reason === 'cobol-perform',
|
|
);
|
|
const targets = perfCalls.map((e) => e.target);
|
|
// DIGITLEAD: PERFORM 2000-READ-FILE (twice: bare + THRU first-target)
|
|
expect(targets.filter((t) => t === '2000-READ-FILE').length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('PERFORM THRU with digit-leading targets emits both edges', () => {
|
|
const perfThruEdges = getRelationships(result, 'CALLS').filter(
|
|
(e) => e.rel.reason === 'cobol-perform-thru',
|
|
);
|
|
const thruTargets = perfThruEdges.map((e) => e.target);
|
|
// DIGITLEAD: PERFORM 2000-READ-FILE THRU 2100-PROCESS
|
|
expect(thruTargets).toContain('2100-PROCESS');
|
|
// PERFTIMS: PERFORM 2000-PROCESS THRU 2100-CLEANUP
|
|
expect(thruTargets).toContain('2100-CLEANUP');
|
|
});
|
|
|
|
it('GO TO 9000-EXIT resolves to digit-leading target', () => {
|
|
const gotoCalls = getRelationships(result, 'CALLS').filter(
|
|
(e) => e.rel.reason === 'cobol-goto',
|
|
);
|
|
const gotoTargets = gotoCalls.map((e) => e.target);
|
|
expect(gotoTargets).toContain('9000-EXIT');
|
|
expect(gotoTargets).toContain('9000-END');
|
|
});
|
|
|
|
// INPUT/OUTPUT PROCEDURE (SORT/MERGE): The regex patterns at
|
|
// L1605-1610 use the same [A-Z0-9] character class as all other
|
|
// F17 patterns — verified via code audit that the capture groups
|
|
// accept digit-leading paragraph names identically to RE_PROC_PARAGRAPH.
|
|
// Dedicated SORT/INPUT PROCEDURE fixture requires file declarations
|
|
// (SELECT/ASSIGN) which adds complexity beyond this findings scope.
|
|
// Deferred to future work; existing regex coverage is confirmed.
|
|
});
|
|
|
|
// =====================================================================
|
|
// F18: MOVE source subscript
|
|
// =====================================================================
|
|
describe('F18 — MOVE with subscripts', () => {
|
|
it('emits cobol-move-read ACCESSES edges', () => {
|
|
const readEdges = getRelationships(result, 'ACCESSES').filter(
|
|
(e) => e.rel.reason === 'cobol-move-read',
|
|
);
|
|
expect(readEdges.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('emits cobol-move-write ACCESSES edges', () => {
|
|
const writeEdges = getRelationships(result, 'ACCESSES').filter(
|
|
(e) => e.rel.reason === 'cobol-move-write',
|
|
);
|
|
expect(writeEdges.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
// =====================================================================
|
|
// F19: Multi-table SQL FROM
|
|
// =====================================================================
|
|
describe('F19 — multi-table SQL FROM', () => {
|
|
it('captures all tables from comma-separated FROM clauses', () => {
|
|
const sqlAccesses = getRelationships(result, 'ACCESSES').filter(
|
|
(e) => e.rel.reason === 'sql-select',
|
|
);
|
|
// MULTISQL has:
|
|
// FROM CUSTOMER C, ACCOUNT A → CUSTOMER, ACCOUNT (2 tables)
|
|
// FROM CUSTOMER, ACCOUNT → CUSTOMER, ACCOUNT (2 tables)
|
|
// FROM INVENTORY → INVENTORY (1 table)
|
|
// Total: 5 table references across 3 SQL blocks
|
|
expect(sqlAccesses.length).toBe(5);
|
|
const targets = sqlAccesses.map((e) => e.target);
|
|
expect(targets).toContain('Record:<db>:CUSTOMER');
|
|
expect(targets).toContain('Record:<db>:ACCOUNT');
|
|
expect(targets).toContain('Record:<db>:INVENTORY');
|
|
});
|
|
});
|
|
|
|
// =====================================================================
|
|
// F20: All 5 arithmetic verbs
|
|
// =====================================================================
|
|
describe('F20 — arithmetic verb ACCESSES edges', () => {
|
|
it('ADD A TO B produces both edges', () => {
|
|
const arithRead = getRelationships(result, 'ACCESSES').filter(
|
|
(e) => e.rel.reason === 'cobol-arithmetic-read',
|
|
);
|
|
const arithWrite = getRelationships(result, 'ACCESSES').filter(
|
|
(e) => e.rel.reason === 'cobol-arithmetic-write',
|
|
);
|
|
// ARITHOPS fixture:
|
|
// ADD WS-A TO WS-B → read(WS-A) write(WS-B)
|
|
// SUBTRACT WS-A FROM WS-C → read(WS-A) write(WS-C)
|
|
// MULTIPLY WS-A BY WS-B → read(WS-A) write(WS-B)
|
|
// DIVIDE WS-A INTO WS-D → read(WS-A) write(WS-D)
|
|
// COMPUTE WS-RESULT = ... → read(WS-A, WS-B) write(WS-RESULT)
|
|
// Total: 6+ reads, 5 writes
|
|
expect(arithRead.length).toBeGreaterThanOrEqual(6);
|
|
expect(arithWrite.length).toBeGreaterThanOrEqual(5);
|
|
});
|
|
});
|
|
|
|
// =====================================================================
|
|
// F21: Free-format column detection
|
|
// =====================================================================
|
|
describe('F21 — free-format PROGRAM-ID column', () => {
|
|
it('produces Module nodes for both fixtures', () => {
|
|
const modules = getNodesByLabel(result, 'Module');
|
|
expect(modules).toContain('FREEFMT');
|
|
expect(modules).toContain('OFFSETPGM');
|
|
});
|
|
});
|
|
|
|
// =====================================================================
|
|
// F22: File-size guard — edge case verification
|
|
// =====================================================================
|
|
describe('F22 — file-size guard', () => {
|
|
// When threshold is below file sizes, files are skipped (no Module nodes).
|
|
// When threshold is above, files process normally.
|
|
let skipResult: PipelineResult;
|
|
|
|
beforeAll(async () => {
|
|
process.env.GITNEXUS_MAX_COBOL_FILE_SIZE_BYTES = '100';
|
|
skipResult = await runPipelineFromRepo(COVERAGE_FIXTURE, () => {}, {
|
|
skipGraphPhases: true,
|
|
});
|
|
delete process.env.GITNEXUS_MAX_COBOL_FILE_SIZE_BYTES;
|
|
}, 60000);
|
|
|
|
it('file above threshold is skipped — zero Module nodes', () => {
|
|
// With threshold=100, all fixture files (203-906 bytes) are over the limit.
|
|
// The guard calls logger.warn with the file path and size — visible in test stderr.
|
|
const modules = getNodesByLabel(skipResult, 'Module');
|
|
expect(modules.length).toBe(0);
|
|
});
|
|
|
|
it('file near threshold (below limit) processes normally', async () => {
|
|
// Set threshold to 10MB — well above all fixture file sizes
|
|
process.env.GITNEXUS_MAX_COBOL_FILE_SIZE_BYTES = String(10 * 1024 * 1024);
|
|
const norResult = await runPipelineFromRepo(COVERAGE_FIXTURE, () => {}, {
|
|
skipGraphPhases: true,
|
|
});
|
|
delete process.env.GITNEXUS_MAX_COBOL_FILE_SIZE_BYTES;
|
|
const modules = getNodesByLabel(norResult, 'Module');
|
|
expect(modules.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
// =====================================================================
|
|
// F23: PERFORM TIMES + THRU on digit-leading targets
|
|
// =====================================================================
|
|
describe('F23 — PERFORM TIMES and THRU', () => {
|
|
it('PERFORM 2000-PROCESS THRU 2100-CLEANUP resolves THRU target', () => {
|
|
const perfThruEdges = getRelationships(result, 'CALLS').filter(
|
|
(e) => e.rel.reason === 'cobol-perform-thru',
|
|
);
|
|
const thruTargets = perfThruEdges.map((e) => e.target);
|
|
expect(thruTargets).toContain('2100-CLEANUP');
|
|
});
|
|
|
|
it('PERFORM with VARYING does NOT create spurious CALLS edge', () => {
|
|
// PERFTIMS has PERFORM VARYING WS-COUNT FROM 1 BY 1...
|
|
// VARYING is in PERFORM_KEYWORD_SKIP, so it should be skipped.
|
|
// Verify there are no spurious perform edges with target "VARYING"
|
|
const perfEdges = getRelationships(result, 'CALLS').filter(
|
|
(e) => e.rel.reason === 'cobol-perform',
|
|
);
|
|
const targets = perfEdges.map((e) => e.target);
|
|
expect(targets).not.toContain('VARYING');
|
|
expect(targets).not.toContain('WS-COUNT');
|
|
});
|
|
|
|
it('total CALLS count stays reasonable (TIMES with count is real)', () => {
|
|
// PERFTIMS has: 2 perform (2000-PROCESS + 2000-PROCESS THRU first-target)
|
|
// + 2 perform for count TIMES (2000-PROCESS 3 TIMES + WS-COUNT TIMES)
|
|
// + 1 perform-thru + 1 goto = 6 CALLS
|
|
// DIGITLEAD has: 2 perform + 1 perform-thru + 1 goto = 4 CALLS
|
|
// Total: 10 across both fixtures
|
|
const calls = getRelationships(result, 'CALLS');
|
|
expect(calls.length).toBe(10);
|
|
});
|
|
});
|
|
});
|