Path: blob/main/src/vs/editor/test/node/diffing/fixtures.test.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs';7import { join, resolve } from '../../../../base/common/path.js';8import { setUnexpectedErrorHandler } from '../../../../base/common/errors.js';9import { FileAccess } from '../../../../base/common/network.js';10import { DetailedLineRangeMapping, RangeMapping } from '../../../common/diff/rangeMapping.js';11import { LegacyLinesDiffComputer } from '../../../common/diff/legacyLinesDiffComputer.js';12import { DefaultLinesDiffComputer } from '../../../common/diff/defaultLinesDiffComputer/defaultLinesDiffComputer.js';13import { Range } from '../../../common/core/range.js';14import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';15import { TextReplacement, TextEdit } from '../../../common/core/edits/textEdit.js';16import { AbstractText, ArrayText } from '../../../common/core/text/abstractText.js';17import { LinesDiff } from '../../../common/diff/linesDiffComputer.js';1819suite('diffing fixtures', () => {20ensureNoDisposablesAreLeakedInTestSuite();2122setup(() => {23setUnexpectedErrorHandler(e => {24throw e;25});26});272829const fixturesOutDir = FileAccess.asFileUri('vs/editor/test/node/diffing/fixtures').fsPath;30// 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.31// This makes it very easy to update the fixtures.32const fixturesSrcDir = resolve(fixturesOutDir).replaceAll('\\', '/').replace('/out/vs/editor/', '/src/vs/editor/');33const folders = readdirSync(fixturesSrcDir);3435function runTest(folder: string, diffingAlgoName: 'legacy' | 'advanced') {36const folderPath = join(fixturesSrcDir, folder);37const files = readdirSync(folderPath);3839const firstFileName = files.find(f => f.startsWith('1.'))!;40const secondFileName = files.find(f => f.startsWith('2.'))!;4142const firstContent = readFileSync(join(folderPath, firstFileName), 'utf8').replaceAll('\r\n', '\n').replaceAll('\r', '\n');43const firstContentLines = firstContent.split(/\n/);44const secondContent = readFileSync(join(folderPath, secondFileName), 'utf8').replaceAll('\r\n', '\n').replaceAll('\r', '\n');45const secondContentLines = secondContent.split(/\n/);4647const diffingAlgo = diffingAlgoName === 'legacy' ? new LegacyLinesDiffComputer() : new DefaultLinesDiffComputer();4849const ignoreTrimWhitespace = folder.indexOf('trimws') >= 0;50const diff = diffingAlgo.computeDiff(firstContentLines, secondContentLines, { ignoreTrimWhitespace, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: true });5152if (diffingAlgoName === 'advanced' && !ignoreTrimWhitespace) {53assertDiffCorrectness(diff, firstContentLines, secondContentLines);54}5556function getDiffs(changes: readonly DetailedLineRangeMapping[]): IDetailedDiff[] {57for (const c of changes) {58RangeMapping.assertSorted(c.innerChanges ?? []);59}6061return changes.map<IDetailedDiff>(c => ({62originalRange: c.original.toString(),63modifiedRange: c.modified.toString(),64innerChanges: c.innerChanges?.map<IDiff>(c => ({65originalRange: formatRange(c.originalRange, firstContentLines),66modifiedRange: formatRange(c.modifiedRange, secondContentLines),67})) || null68}));69}7071function formatRange(range: Range, lines: string[]): string {72const toLastChar = range.endColumn === lines[range.endLineNumber - 1].length + 1;7374return '[' + range.startLineNumber + ',' + range.startColumn + ' -> ' + range.endLineNumber + ',' + range.endColumn + (toLastChar ? ' EOL' : '') + ']';75}7677const actualDiffingResult: DiffingResult = {78original: { content: firstContent, fileName: `./${firstFileName}` },79modified: { content: secondContent, fileName: `./${secondFileName}` },80diffs: getDiffs(diff.changes),81moves: diff.moves.map(v => ({82originalRange: v.lineRangeMapping.original.toString(),83modifiedRange: v.lineRangeMapping.modified.toString(),84changes: getDiffs(v.changes),85}))86};87if (actualDiffingResult.moves?.length === 0) {88delete actualDiffingResult.moves;89}9091const expectedFilePath = join(folderPath, `${diffingAlgoName}.expected.diff.json`);92const invalidFilePath = join(folderPath, `${diffingAlgoName}.invalid.diff.json`);9394const actualJsonStr = JSON.stringify(actualDiffingResult, null, '\t');9596if (!existsSync(expectedFilePath)) {97// New test, create expected file98writeFileSync(expectedFilePath, actualJsonStr);99// Create invalid file so that this test fails on a re-run100writeFileSync(invalidFilePath, '');101throw new Error('No expected file! Expected and invalid files were written. Delete the invalid file to make the test pass.');102} if (existsSync(invalidFilePath)) {103const invalidJsonStr = readFileSync(invalidFilePath, 'utf8');104if (invalidJsonStr === '') {105// Update expected file106writeFileSync(expectedFilePath, actualJsonStr);107throw new Error(`Delete the invalid ${invalidFilePath} file to make the test pass.`);108} else {109const expectedFileDiffResult: DiffingResult = JSON.parse(invalidJsonStr);110try {111assert.deepStrictEqual(actualDiffingResult, expectedFileDiffResult);112} catch (e) {113writeFileSync(expectedFilePath, actualJsonStr);114throw e;115}116// Test succeeded with the invalid file, restore expected file from invalid117writeFileSync(expectedFilePath, invalidJsonStr);118rmSync(invalidFilePath);119}120} else {121const expectedJsonStr = readFileSync(expectedFilePath, 'utf8');122const expectedFileDiffResult: DiffingResult = JSON.parse(expectedJsonStr);123try {124assert.deepStrictEqual(actualDiffingResult, expectedFileDiffResult);125} catch (e) {126// Backup expected file127writeFileSync(invalidFilePath, expectedJsonStr);128// Update expected file129writeFileSync(expectedFilePath, actualJsonStr);130throw e;131}132}133}134135test(`test`, () => {136runTest('invalid-diff-trimws', 'advanced');137});138139for (const folder of folders) {140for (const diffingAlgoName of ['legacy', 'advanced'] as const) {141test(`${folder}-${diffingAlgoName}`, () => {142runTest(folder, diffingAlgoName);143});144}145}146});147148interface DiffingResult {149original: { content: string; fileName: string };150modified: { content: string; fileName: string };151152diffs: IDetailedDiff[];153moves?: IMoveInfo[];154}155156interface IDetailedDiff {157originalRange: string; // [startLineNumber, endLineNumberExclusive)158modifiedRange: string; // [startLineNumber, endLineNumberExclusive)159innerChanges: IDiff[] | null;160}161162interface IDiff {163originalRange: string; // [1,18 -> 1,19]164modifiedRange: string; // [1,18 -> 1,19]165}166167interface IMoveInfo {168originalRange: string; // [startLineNumber, endLineNumberExclusive)169modifiedRange: string; // [startLineNumber, endLineNumberExclusive)170171changes: IDetailedDiff[];172}173174function assertDiffCorrectness(diff: LinesDiff, original: string[], modified: string[]) {175const allInnerChanges = diff.changes.flatMap(c => c.innerChanges!);176const edit = rangeMappingsToTextEdit(allInnerChanges, new ArrayText(modified));177const result = edit.normalize().apply(new ArrayText(original));178179assert.deepStrictEqual(result, modified.join('\n'));180}181182function rangeMappingsToTextEdit(rangeMappings: readonly RangeMapping[], modified: AbstractText): TextEdit {183return new TextEdit(rangeMappings.map(m => {184return new TextReplacement(185m.originalRange,186modified.getValueOfRange(m.modifiedRange)187);188}));189}190191192