File size: 6,872 Bytes
f0634fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
// test/differential.test.ts
//
// Differential suite: every sample in test/fixtures/differential/ and every
// official tree-sitter-bash 0.25.0 corpus case in test/fixtures/corpus/ is
// parsed by BOTH this package and the reference (web-tree-sitter + the
// official wasm build) and the normalized trees are compared byte-for-byte.
//
//   - @match samples must produce a tree identical to the reference;
//   - @known-diff samples must (a) still differ from the reference and
//     (b) match the stored dump of our parser exactly — so a documented
//     deviation cannot silently drift, and a fixed deviation fails loudly
//     ("remove it from the known list").
//
// The known-difference registry (test/helpers/known-differences.ts) is the
// single source of truth and is cross-checked against README.md's Known
// differences section here.

import { readFileSync, readdirSync } from 'node:fs';
import path from 'node:path';

import { beforeAll, describe, expect, it } from 'vitest';

import { compareSource, diffDumps, ourDump, parseCorpusFile, parseFixtureFile, referenceDump } from './helpers/differential';
import type { FixtureSample } from './helpers/differential';
import { KNOWN_DIFFERENCES, isKnownDifferenceId } from './helpers/known-differences';

const PKG_ROOT = path.resolve(import.meta.dirname, '..');
const DIFF_DIR = path.join(PKG_ROOT, 'test/fixtures/differential');
const CORPUS_DIR = path.join(PKG_ROOT, 'test/fixtures/corpus');

interface LocatedSample extends FixtureSample {
  file: string;
}

function loadDifferentialFixtures(): LocatedSample[] {
  const out: LocatedSample[] = [];
  for (const file of readdirSync(DIFF_DIR).filter((f) => f.endsWith('.txt')).toSorted()) {
    const content = readFileSync(path.join(DIFF_DIR, file), 'utf8');
    for (const sample of parseFixtureFile(`${DIFF_DIR}/${file}`, content)) {
      out.push({ ...sample, file });
    }
  }
  return out;
}

function loadCorpusKnownDiffs(): Map<string, LocatedSample> {
  const file = 'known-diffs.txt';
  const content = readFileSync(path.join(CORPUS_DIR, file), 'utf8');
  const map = new Map<string, LocatedSample>();
  for (const sample of parseFixtureFile(`${CORPUS_DIR}/${file}`, content)) {
    // description: "corpus <file>::<case name>"
    const key = sample.description.replace(/^corpus /, '');
    map.set(key, { ...sample, file });
  }
  return map;
}

function label(sample: LocatedSample): string {
  return `${sample.file}:${sample.line} ${sample.kind === 'match' ? 'match' : `known-diff ${sample.id}`} — ${sample.description.slice(0, 60)}`;
}

const fixtures = loadDifferentialFixtures();
const corpusKnownDiffs = loadCorpusKnownDiffs();
const corpusCases = readdirSync(CORPUS_DIR)
  .filter((f) => f.endsWith('.txt') && f !== 'known-diffs.txt')
  .toSorted()
  .flatMap((file) => parseCorpusFile(readFileSync(path.join(CORPUS_DIR, file), 'utf8')).map((c) => ({ ...c, file })));

beforeAll(async () => {
  // Fails with a descriptive error when the wasm reference cannot be loaded.
  await compareSource('echo sanity');
});

describe('differential fixtures', () => {
  for (const sample of fixtures) registerFixtureTest(sample);
});

/** Register one fixture test (lookup table instead of conditionals — the
 *  vitest lint rules reject conditionals around `it`). */
function registerFixtureTest(sample: LocatedSample): void {
  const register: Record<FixtureSample['kind'], () => void> = {
    match: () => {
      it(label(sample), async () => {
        const cmp = await compareSource(sample.source);
        expect(cmp.diff).toBe('');
      });
    },
    'known-diff': () => {
      it(label(sample), async () => {
        const [ours, reference] = [ourDump(sample.source), await referenceDump(sample.source)];
        expect(
          ours !== reference,
          'this sample now matches the reference — remove it from the known-difference list (fixture + registry + README)',
        ).toBe(true);
        expect(ours === sample.expectedOurs, `deviation shape drifted:\n${diffDumps(ours, sample.expectedOurs!)}`).toBe(
          true,
        );
      });
    },
  };
  register[sample.kind]();
}

describe('official tree-sitter-bash 0.25.0 corpus', () => {
  for (const corpusCase of corpusCases) registerCorpusTest(corpusCase);
});

/** Register one corpus case test. */
function registerCorpusTest(corpusCase: { file: string; name: string; input: string }): void {
  const key = `${corpusCase.file}::${corpusCase.name}`;
  const known = corpusKnownDiffs.get(key) ?? null;
  const register: Record<'match' | 'known-diff', () => void> = {
    match: () => {
      it(`${key} (match)`, async () => {
        const cmp = await compareSource(corpusCase.input);
        expect(cmp.diff).toBe('');
      });
    },
    'known-diff': () => {
      it(`${key} (known-diff ${known!.id})`, async () => {
        const [ours, reference] = [ourDump(corpusCase.input), await referenceDump(corpusCase.input)];
        expect(
          ours !== reference,
          'this corpus case now matches the reference — remove it from known-diffs.txt, the registry and the README',
        ).toBe(true);
        expect(ours === known!.expectedOurs, `deviation shape drifted:\n${diffDumps(ours, known!.expectedOurs!)}`).toBe(
          true,
        );
      });
    },
  };
  register[known === null ? 'match' : 'known-diff']();
}

describe('known-difference registry consistency', () => {
  const usedIds = new Set<string>();
  for (const sample of fixtures) {
    if (sample.id !== undefined) usedIds.add(sample.id);
  }
  for (const sample of corpusKnownDiffs.values()) {
    if (sample.id !== undefined) usedIds.add(sample.id);
  }

  it('every @known-diff id used in fixtures exists in the registry', () => {
    for (const id of usedIds) {
      expect(isKnownDifferenceId(id), `fixture uses unregistered id '${id}'`).toBe(true);
    }
  });

  it('every registry id is exercised by at least one fixture or corpus case', () => {
    for (const entry of KNOWN_DIFFERENCES) {
      expect(usedIds.has(entry.id), `registry id '${entry.id}' has no fixture sample`).toBe(true);
    }
  });

  it('every registry entry is anchored in README.md Known differences', () => {
    const readme = readFileSync(path.join(PKG_ROOT, 'README.md'), 'utf8');
    const sectionMatch = /## Known differences[^\n]*\n([\s\S]*?)(?=\n## |\s*$)/.exec(readme);
    expect(sectionMatch, 'README.md has no "## Known differences" section').not.toBeNull();
    // Whitespace-normalized so anchors may span line breaks in the README.
    const section = sectionMatch![1]!.replaceAll(/\s+/g, ' ');
    for (const entry of KNOWN_DIFFERENCES) {
      expect(
        section.includes(entry.readmeAnchor.replaceAll(/\s+/g, ' ')),
        `README.md Known differences does not contain the anchor for '${entry.id}': ${entry.readmeAnchor}`,
      ).toBe(true);
    }
  });
});