Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/test/node/diffing/fixtures.test.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import assert from 'assert';
7
import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';
8
import { join, resolve } from '../../../../base/common/path.js';
9
import { setUnexpectedErrorHandler } from '../../../../base/common/errors.js';
10
import { FileAccess } from '../../../../base/common/network.js';
11
import { DetailedLineRangeMapping, RangeMapping } from '../../../common/diff/rangeMapping.js';
12
import { LegacyLinesDiffComputer } from '../../../common/diff/legacyLinesDiffComputer.js';
13
import { DefaultLinesDiffComputer } from '../../../common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.js';
14
import { Range } from '../../../common/core/range.js';
15
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
16
import { TextReplacement, TextEdit } from '../../../common/core/edits/textEdit.js';
17
import { AbstractText, ArrayText } from '../../../common/core/text/abstractText.js';
18
import { LinesDiff } from '../../../common/diff/linesDiffComputer.js';
19
20
suite('diffing fixtures', () => {
21
ensureNoDisposablesAreLeakedInTestSuite();
22
23
setup(() => {
24
setUnexpectedErrorHandler(e => {
25
throw e;
26
});
27
});
28
29
30
const fixturesOutDir = FileAccess.asFileUri('vs/editor/test/node/diffing/fixtures').fsPath;
31
// We want the dir in src, so we can directly update the source files if they disagree and create invalid files to capture the previous state.
32
// This makes it very easy to update the fixtures.
33
const fixturesSrcDir = resolve(fixturesOutDir).replaceAll('\\', '/').replace('/out/vs/editor/', '/src/vs/editor/');
34
const folders = readdirSync(fixturesSrcDir);
35
36
function runTest(folder: string, diffingAlgoName: 'legacy' | 'advanced') {
37
const folderPath = join(fixturesSrcDir, folder);
38
const files = readdirSync(folderPath);
39
40
const firstFileName = files.find(f => f.startsWith('1.'))!;
41
const secondFileName = files.find(f => f.startsWith('2.'))!;
42
43
const firstContent = readFileSync(join(folderPath, firstFileName), 'utf8').replaceAll('\r\n', '\n').replaceAll('\r', '\n');
44
const firstContentLines = firstContent.split(/\n/);
45
const secondContent = readFileSync(join(folderPath, secondFileName), 'utf8').replaceAll('\r\n', '\n').replaceAll('\r', '\n');
46
const secondContentLines = secondContent.split(/\n/);
47
48
const diffingAlgo = diffingAlgoName === 'legacy' ? new LegacyLinesDiffComputer() : new DefaultLinesDiffComputer();
49
50
const ignoreTrimWhitespace = folder.indexOf('trimws') >= 0;
51
const diff = diffingAlgo.computeDiff(firstContentLines, secondContentLines, { ignoreTrimWhitespace, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: true });
52
53
if (diffingAlgoName === 'advanced' && !ignoreTrimWhitespace) {
54
assertDiffCorrectness(diff, firstContentLines, secondContentLines);
55
}
56
57
function getDiffs(changes: readonly DetailedLineRangeMapping[]): IDetailedDiff[] {
58
for (const c of changes) {
59
RangeMapping.assertSorted(c.innerChanges ?? []);
60
}
61
62
return changes.map<IDetailedDiff>(c => ({
63
originalRange: c.original.toString(),
64
modifiedRange: c.modified.toString(),
65
innerChanges: c.innerChanges?.map<IDiff>(c => ({
66
originalRange: formatRange(c.originalRange, firstContentLines),
67
modifiedRange: formatRange(c.modifiedRange, secondContentLines),
68
})) || null
69
}));
70
}
71
72
function formatRange(range: Range, lines: string[]): string {
73
const toLastChar = range.endColumn === lines[range.endLineNumber - 1].length + 1;
74
75
return '[' + range.startLineNumber + ',' + range.startColumn + ' -> ' + range.endLineNumber + ',' + range.endColumn + (toLastChar ? ' EOL' : '') + ']';
76
}
77
78
const actualDiffingResult: DiffingResult = {
79
original: { content: firstContent, fileName: `./${firstFileName}` },
80
modified: { content: secondContent, fileName: `./${secondFileName}` },
81
diffs: getDiffs(diff.changes),
82
moves: diff.moves.map(v => ({
83
originalRange: v.lineRangeMapping.original.toString(),
84
modifiedRange: v.lineRangeMapping.modified.toString(),
85
changes: getDiffs(v.changes),
86
}))
87
};
88
if (actualDiffingResult.moves?.length === 0) {
89
delete actualDiffingResult.moves;
90
}
91
92
const expectedFilePath = join(folderPath, `${diffingAlgoName}.expected.diff.json`);
93
const invalidFilePath = join(folderPath, `${diffingAlgoName}.invalid.diff.json`);
94
95
const actualJsonStr = JSON.stringify(actualDiffingResult, null, '\t');
96
97
if (!existsSync(expectedFilePath)) {
98
// New test, create expected file
99
writeFileSync(expectedFilePath, actualJsonStr);
100
// Create invalid file so that this test fails on a re-run
101
writeFileSync(invalidFilePath, '');
102
throw new Error('No expected file! Expected and invalid files were written. Delete the invalid file to make the test pass.');
103
} if (existsSync(invalidFilePath)) {
104
const invalidJsonStr = readFileSync(invalidFilePath, 'utf8');
105
if (invalidJsonStr === '') {
106
// Update expected file
107
writeFileSync(expectedFilePath, actualJsonStr);
108
throw new Error(`Delete the invalid ${invalidFilePath} file to make the test pass.`);
109
} else {
110
const expectedFileDiffResult: DiffingResult = JSON.parse(invalidJsonStr);
111
try {
112
assert.deepStrictEqual(actualDiffingResult, expectedFileDiffResult);
113
} catch (e) {
114
writeFileSync(expectedFilePath, actualJsonStr);
115
throw e;
116
}
117
// Test succeeded with the invalid file, restore expected file from invalid
118
writeFileSync(expectedFilePath, invalidJsonStr);
119
rmSync(invalidFilePath);
120
}
121
} else {
122
const expectedJsonStr = readFileSync(expectedFilePath, 'utf8');
123
const expectedFileDiffResult: DiffingResult = JSON.parse(expectedJsonStr);
124
try {
125
assert.deepStrictEqual(actualDiffingResult, expectedFileDiffResult);
126
} catch (e) {
127
// Backup expected file
128
writeFileSync(invalidFilePath, expectedJsonStr);
129
// Update expected file
130
writeFileSync(expectedFilePath, actualJsonStr);
131
throw e;
132
}
133
}
134
}
135
136
test(`test`, () => {
137
runTest('invalid-diff-trimws', 'advanced');
138
});
139
140
for (const folder of folders) {
141
for (const diffingAlgoName of ['legacy', 'advanced'] as const) {
142
test(`${folder}-${diffingAlgoName}`, () => {
143
runTest(folder, diffingAlgoName);
144
});
145
}
146
}
147
});
148
149
interface DiffingResult {
150
original: { content: string; fileName: string };
151
modified: { content: string; fileName: string };
152
153
diffs: IDetailedDiff[];
154
moves?: IMoveInfo[];
155
}
156
157
interface IDetailedDiff {
158
originalRange: string; // [startLineNumber, endLineNumberExclusive)
159
modifiedRange: string; // [startLineNumber, endLineNumberExclusive)
160
innerChanges: IDiff[] | null;
161
}
162
163
interface IDiff {
164
originalRange: string; // [1,18 -> 1,19]
165
modifiedRange: string; // [1,18 -> 1,19]
166
}
167
168
interface IMoveInfo {
169
originalRange: string; // [startLineNumber, endLineNumberExclusive)
170
modifiedRange: string; // [startLineNumber, endLineNumberExclusive)
171
172
changes: IDetailedDiff[];
173
}
174
175
function assertDiffCorrectness(diff: LinesDiff, original: string[], modified: string[]) {
176
const allInnerChanges = diff.changes.flatMap(c => c.innerChanges!);
177
const edit = rangeMappingsToTextEdit(allInnerChanges, new ArrayText(modified));
178
const result = edit.normalize().apply(new ArrayText(original));
179
180
assert.deepStrictEqual(result, modified.join('\n'));
181
}
182
183
function rangeMappingsToTextEdit(rangeMappings: readonly RangeMapping[], modified: AbstractText): TextEdit {
184
return new TextEdit(rangeMappings.map(m => {
185
return new TextReplacement(
186
m.originalRange,
187
modified.getValueOfRange(m.modifiedRange)
188
);
189
}));
190
}
191
192