mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* feat: configure eslint with unused import removal Add ESLint v9 (flat config) for code quality: - eslint-plugin-unused-imports for auto-removing dead imports - @typescript-eslint for TypeScript-aware linting - eslint-plugin-react-hooks for React hooks rules - eslint-config-prettier to avoid formatting conflicts - lint-staged runs eslint --fix before prettier on .ts/.tsx - CI lint job added to ci-quality.yml * refactor: remove unused imports via eslint --fix Auto-fixed by eslint-plugin-unused-imports. No logic changes. * chore: add eslint fix commit to .git-blame-ignore-revs
333 lines
12 KiB
TypeScript
333 lines
12 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { parseJcl } from '../../src/core/ingestion/cobol/jcl-parser.js';
|
|
|
|
describe('parseJcl', () => {
|
|
// ── JOB statements ──────────────────────────────────────────────────
|
|
|
|
describe('JOB statements', () => {
|
|
it('extracts job name', () => {
|
|
const jcl = `//MYJOB JOB (ACCT),'MY JOB'`;
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
expect(r.jobs).toHaveLength(1);
|
|
expect(r.jobs[0].name).toBe('MYJOB');
|
|
expect(r.jobs[0].line).toBe(1);
|
|
});
|
|
|
|
it('extracts CLASS and MSGCLASS parameters', () => {
|
|
const jcl = `//PAYJOB JOB (ACCT),'PAYROLL',CLASS=A,MSGCLASS=X`;
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
expect(r.jobs).toHaveLength(1);
|
|
expect(r.jobs[0].name).toBe('PAYJOB');
|
|
expect(r.jobs[0].class).toBe('A');
|
|
expect(r.jobs[0].msgclass).toBe('X');
|
|
});
|
|
|
|
it('handles job with no CLASS or MSGCLASS', () => {
|
|
const jcl = `//BAREJOB JOB (ACCT),'BARE'`;
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
expect(r.jobs).toHaveLength(1);
|
|
expect(r.jobs[0].name).toBe('BAREJOB');
|
|
expect(r.jobs[0].class).toBeUndefined();
|
|
expect(r.jobs[0].msgclass).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ── EXEC statements ─────────────────────────────────────────────────
|
|
|
|
describe('EXEC statements', () => {
|
|
it('extracts step with PGM=program', () => {
|
|
const jcl = ['//MYJOB JOB (ACCT)', '//STEP1 EXEC PGM=IEFBR14'].join('\n');
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
expect(r.steps).toHaveLength(1);
|
|
expect(r.steps[0].name).toBe('STEP1');
|
|
expect(r.steps[0].program).toBe('IEFBR14');
|
|
expect(r.steps[0].proc).toBeUndefined();
|
|
});
|
|
|
|
it('extracts step with proc name (no PGM= keyword)', () => {
|
|
const jcl = ['//MYJOB JOB (ACCT)', '//STEP1 EXEC MYPROC'].join('\n');
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
expect(r.steps).toHaveLength(1);
|
|
expect(r.steps[0].name).toBe('STEP1');
|
|
expect(r.steps[0].program).toBeUndefined();
|
|
expect(r.steps[0].proc).toBe('MYPROC');
|
|
});
|
|
|
|
it('associates step with current job', () => {
|
|
const jcl = [
|
|
'//JOB1 JOB (ACCT)',
|
|
'//STEPA EXEC PGM=PROG1',
|
|
'//JOB2 JOB (ACCT)',
|
|
'//STEPB EXEC PGM=PROG2',
|
|
].join('\n');
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
expect(r.steps).toHaveLength(2);
|
|
expect(r.steps[0].jobName).toBe('JOB1');
|
|
expect(r.steps[1].jobName).toBe('JOB2');
|
|
});
|
|
});
|
|
|
|
// ── DD statements ───────────────────────────────────────────────────
|
|
|
|
describe('DD statements', () => {
|
|
it('extracts DD name and dataset (DSN=)', () => {
|
|
const jcl = [
|
|
'//MYJOB JOB (ACCT)',
|
|
'//STEP1 EXEC PGM=IEFBR14',
|
|
'//INPUT DD DSN=MY.DATA.SET,DISP=SHR',
|
|
].join('\n');
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
expect(r.ddStatements).toHaveLength(1);
|
|
expect(r.ddStatements[0].ddName).toBe('INPUT');
|
|
expect(r.ddStatements[0].dataset).toBe('MY.DATA.SET');
|
|
});
|
|
|
|
it('extracts DISP parameter', () => {
|
|
const jcl = [
|
|
'//MYJOB JOB (ACCT)',
|
|
'//STEP1 EXEC PGM=IEFBR14',
|
|
'//OUTPUT DD DSN=MY.OUT,DISP=(NEW,CATLG,DELETE)',
|
|
].join('\n');
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
expect(r.ddStatements).toHaveLength(1);
|
|
expect(r.ddStatements[0].disp).toBe('NEW');
|
|
});
|
|
|
|
it('associates DD with current step', () => {
|
|
const jcl = [
|
|
'//MYJOB JOB (ACCT)',
|
|
'//STEP1 EXEC PGM=PROG1',
|
|
'//DD1 DD DSN=DS1,DISP=SHR',
|
|
'//STEP2 EXEC PGM=PROG2',
|
|
'//DD2 DD DSN=DS2,DISP=SHR',
|
|
].join('\n');
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
expect(r.ddStatements).toHaveLength(2);
|
|
expect(r.ddStatements[0].stepName).toBe('STEP1');
|
|
expect(r.ddStatements[1].stepName).toBe('STEP2');
|
|
});
|
|
});
|
|
|
|
// ── PROC definitions ────────────────────────────────────────────────
|
|
|
|
describe('PROC definitions', () => {
|
|
it('extracts in-stream PROC with name', () => {
|
|
const jcl = ['//MYPROC PROC', '//STEP1 EXEC PGM=IEFBR14', '// PEND'].join('\n');
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
expect(r.procs).toHaveLength(1);
|
|
expect(r.procs[0].name).toBe('MYPROC');
|
|
expect(r.procs[0].isInStream).toBe(true);
|
|
});
|
|
|
|
it('handles PROC/PEND pairs', () => {
|
|
const jcl = [
|
|
'//PROC1 PROC',
|
|
'//S1 EXEC PGM=PROG1',
|
|
'// PEND',
|
|
'//PROC2 PROC',
|
|
'//S2 EXEC PGM=PROG2',
|
|
'// PEND',
|
|
].join('\n');
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
expect(r.procs).toHaveLength(2);
|
|
expect(r.procs[0].name).toBe('PROC1');
|
|
expect(r.procs[1].name).toBe('PROC2');
|
|
});
|
|
});
|
|
|
|
// ── INCLUDE / SET ───────────────────────────────────────────────────
|
|
|
|
describe('INCLUDE and SET', () => {
|
|
it('extracts INCLUDE MEMBER=name', () => {
|
|
const jcl = `// INCLUDE MEMBER=MYINCL`;
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
expect(r.includes).toHaveLength(1);
|
|
expect(r.includes[0].member).toBe('MYINCL');
|
|
expect(r.includes[0].line).toBe(1);
|
|
});
|
|
|
|
it('extracts SET variable=value', () => {
|
|
const jcl = `// SET ENV=PROD`;
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
expect(r.sets).toHaveLength(1);
|
|
expect(r.sets[0].variable).toBe('ENV');
|
|
expect(r.sets[0].value).toBe('PROD');
|
|
});
|
|
});
|
|
|
|
// ── Conditionals ────────────────────────────────────────────────────
|
|
|
|
describe('Conditionals', () => {
|
|
it('extracts IF condition THEN', () => {
|
|
const jcl = `// IF STEP1.RC = 0 THEN`;
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
expect(r.conditionals).toHaveLength(1);
|
|
expect(r.conditionals[0].type).toBe('IF');
|
|
expect(r.conditionals[0].condition).toBe('STEP1.RC = 0');
|
|
});
|
|
|
|
it('extracts ELSE and ENDIF', () => {
|
|
const jcl = [
|
|
'// IF STEP1.RC = 0 THEN',
|
|
'//GOOD EXEC PGM=GOODPGM',
|
|
'// ELSE',
|
|
'//BAD EXEC PGM=BADPGM',
|
|
'// ENDIF',
|
|
].join('\n');
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
expect(r.conditionals).toHaveLength(3);
|
|
expect(r.conditionals[0].type).toBe('IF');
|
|
expect(r.conditionals[1].type).toBe('ELSE');
|
|
expect(r.conditionals[1].condition).toBeUndefined();
|
|
expect(r.conditionals[2].type).toBe('ENDIF');
|
|
expect(r.conditionals[2].condition).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ── JCLLIB ──────────────────────────────────────────────────────────
|
|
|
|
describe('JCLLIB', () => {
|
|
it('extracts JCLLIB ORDER=(lib1,lib2)', () => {
|
|
const jcl = `// JCLLIB ORDER=(SYS1.PROCLIB,USER.PROCLIB)`;
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
expect(r.jcllib).toHaveLength(1);
|
|
expect(r.jcllib[0].order).toEqual(['SYS1.PROCLIB', 'USER.PROCLIB']);
|
|
expect(r.jcllib[0].line).toBe(1);
|
|
});
|
|
});
|
|
|
|
// ── Continuation lines ──────────────────────────────────────────────
|
|
|
|
describe('Continuation lines', () => {
|
|
it('joins continuation lines (col 72 non-blank + next line starts with //)', () => {
|
|
// Build a DD line that is exactly 72 chars with non-blank at col 72 (index 71).
|
|
// The continuation line provides the DISP parameter.
|
|
// "//DD1 DD DSN=MY.VERY.LONG.DATASET.NAME.THAT.KEEPS.GOING," is 60 chars.
|
|
// Pad to 71 then add non-blank at col 72.
|
|
const base = '//DD1 DD DSN=MY.VERY.LONG.DATASET.NAME.THAT.KEEPS.GOING,';
|
|
const padding = ' '.repeat(71 - base.length);
|
|
const line1 = base + padding + 'X'; // col 72 is 'X' (non-blank) -> continuation
|
|
const line2 = '// DISP=SHR';
|
|
const jcl = ['//MYJOB JOB (ACCT)', '//STEP1 EXEC PGM=IEFBR14', line1, line2].join('\n');
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
// The continuation should join the DD line so both DSN and DISP are parsed
|
|
expect(r.ddStatements).toHaveLength(1);
|
|
expect(r.ddStatements[0].ddName).toBe('DD1');
|
|
expect(r.ddStatements[0].dataset).toBe('MY.VERY.LONG.DATASET.NAME.THAT.KEEPS.GOING');
|
|
expect(r.ddStatements[0].disp).toBe('SHR');
|
|
});
|
|
});
|
|
|
|
// ── Edge cases ──────────────────────────────────────────────────────
|
|
|
|
describe('Edge cases', () => {
|
|
it('skips JCL comments (//*)', () => {
|
|
const jcl = ['//* This is a comment', '//MYJOB JOB (ACCT)', '//* Another comment'].join(
|
|
'\n',
|
|
);
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
expect(r.jobs).toHaveLength(1);
|
|
expect(r.jobs[0].name).toBe('MYJOB');
|
|
});
|
|
|
|
it('skips non-JCL lines', () => {
|
|
const jcl = [
|
|
'This is not a JCL line',
|
|
'//MYJOB JOB (ACCT)',
|
|
' Some data',
|
|
'//STEP1 EXEC PGM=IEFBR14',
|
|
].join('\n');
|
|
const r = parseJcl(jcl, 'test.jcl');
|
|
expect(r.jobs).toHaveLength(1);
|
|
expect(r.steps).toHaveLength(1);
|
|
});
|
|
|
|
it('empty input returns empty results', () => {
|
|
const r = parseJcl('', 'test.jcl');
|
|
expect(r.jobs).toEqual([]);
|
|
expect(r.steps).toEqual([]);
|
|
expect(r.ddStatements).toEqual([]);
|
|
expect(r.procs).toEqual([]);
|
|
expect(r.includes).toEqual([]);
|
|
expect(r.sets).toEqual([]);
|
|
expect(r.jcllib).toEqual([]);
|
|
expect(r.conditionals).toEqual([]);
|
|
});
|
|
|
|
it('complete JCL job with multiple steps and DDs', () => {
|
|
const jcl = [
|
|
'//* Complete payroll job',
|
|
"//PAYJOB JOB (ACCT123),'PAYROLL RUN',CLASS=A,MSGCLASS=X",
|
|
'// JCLLIB ORDER=(PAY.PROCLIB,SYS1.PROCLIB)',
|
|
'// SET ENV=PROD',
|
|
'// INCLUDE MEMBER=STDPARMS',
|
|
'//*',
|
|
'// IF 1 = 1 THEN',
|
|
'//STEP01 EXEC PGM=PAYEXT',
|
|
'//INPUT DD DSN=PAY.MASTER,DISP=SHR',
|
|
'//OUTPUT DD DSN=PAY.EXTRACT,DISP=(NEW,CATLG,DELETE)',
|
|
'//SYSPRINT DD SYSOUT=*',
|
|
'//*',
|
|
'//STEP02 EXEC PAYCALC',
|
|
'//INFILE DD DSN=PAY.EXTRACT,DISP=SHR',
|
|
'// ELSE',
|
|
'//STEP03 EXEC PGM=IEFBR14',
|
|
'// ENDIF',
|
|
].join('\n');
|
|
const r = parseJcl(jcl, 'payroll.jcl');
|
|
|
|
// Jobs
|
|
expect(r.jobs).toHaveLength(1);
|
|
expect(r.jobs[0]).toEqual({
|
|
name: 'PAYJOB',
|
|
line: 2,
|
|
class: 'A',
|
|
msgclass: 'X',
|
|
});
|
|
|
|
// JCLLIB
|
|
expect(r.jcllib).toHaveLength(1);
|
|
expect(r.jcllib[0].order).toEqual(['PAY.PROCLIB', 'SYS1.PROCLIB']);
|
|
|
|
// SET
|
|
expect(r.sets).toHaveLength(1);
|
|
expect(r.sets[0]).toEqual({ variable: 'ENV', value: 'PROD', line: 4 });
|
|
|
|
// INCLUDE
|
|
expect(r.includes).toHaveLength(1);
|
|
expect(r.includes[0].member).toBe('STDPARMS');
|
|
|
|
// Conditionals
|
|
expect(r.conditionals).toHaveLength(3);
|
|
expect(r.conditionals[0].type).toBe('IF');
|
|
expect(r.conditionals[1].type).toBe('ELSE');
|
|
expect(r.conditionals[2].type).toBe('ENDIF');
|
|
|
|
// Steps
|
|
expect(r.steps).toHaveLength(3);
|
|
expect(r.steps[0]).toMatchObject({ name: 'STEP01', program: 'PAYEXT', jobName: 'PAYJOB' });
|
|
expect(r.steps[1]).toMatchObject({ name: 'STEP02', proc: 'PAYCALC', jobName: 'PAYJOB' });
|
|
expect(r.steps[2]).toMatchObject({ name: 'STEP03', program: 'IEFBR14', jobName: 'PAYJOB' });
|
|
|
|
// DD statements
|
|
expect(r.ddStatements).toHaveLength(4);
|
|
expect(r.ddStatements[0]).toMatchObject({
|
|
ddName: 'INPUT',
|
|
stepName: 'STEP01',
|
|
dataset: 'PAY.MASTER',
|
|
disp: 'SHR',
|
|
});
|
|
expect(r.ddStatements[1]).toMatchObject({
|
|
ddName: 'OUTPUT',
|
|
stepName: 'STEP01',
|
|
disp: 'NEW',
|
|
});
|
|
expect(r.ddStatements[2]).toMatchObject({ ddName: 'SYSPRINT', stepName: 'STEP01' });
|
|
expect(r.ddStatements[3]).toMatchObject({
|
|
ddName: 'INFILE',
|
|
stepName: 'STEP02',
|
|
dataset: 'PAY.EXTRACT',
|
|
});
|
|
});
|
|
});
|
|
});
|