blob: d702b825878cfa78e371f25ad386ad359d9f7837 [file] [log] [blame]
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
import 'package:analysis_server/src/utilities/source_change_merger.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(SourceChangeMergerTest);
});
}
@reflectiveTest
class SourceChangeMergerTest {
void test_multipleFiles_mergeIndividually() {
var original = [
SourceFileEdit('fileA', -1, edits: [SourceEdit(4, 1, '4')]),
SourceFileEdit('fileB', -1, edits: [SourceEdit(3, 1, '3')]),
SourceFileEdit('fileA', -1, edits: [SourceEdit(3, 1, '3')]),
SourceFileEdit('fileB', -1, edits: [SourceEdit(2, 1, '2')]),
];
var expected = [
SourceFileEdit('fileA', -1, edits: [SourceEdit(3, 2, '34')]),
SourceFileEdit('fileB', -1, edits: [SourceEdit(2, 2, '23')]),
];
var merged = SourceChangeMerger().merge(original);
expect(merged, expected);
}
void test_multipleFiles_noMerge() {
var original = [
SourceFileEdit('fileA', -1, edits: [SourceEdit(2, 1, '2')]),
SourceFileEdit('fileB', -1, edits: [SourceEdit(1, 1, '1')]),
];
var merged = SourceChangeMerger().merge(original);
// The resulting edits should be the same with no merging because they
// are different files.
expect(merged, original);
}
void test_notTouching_singlePass() {
verifyMerge(
start: '0123456789',
expected: '0-12-34-56789',
edits: [
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '-5'),
SourceEdit(3, 1, '-3'),
SourceEdit(1, 1, '-1'),
]),
],
);
}
void test_overlap_secondDeletesFirst_andLeading() {
verifyMerge(
start: '0123456789',
expected: '01236789',
edits: [
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '-5'), // Replace 5 with -5
]),
SourceFileEdit('', -1, edits: [
SourceEdit(4, 3, ''), // Delete 4-5
]),
],
);
}
void test_overlap_secondDeletesFirst_andLeadingAndTrailing() {
verifyMerge(
start: '0123456789',
expected: '0123789',
edits: [
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '-5'), // Replace 5 with -5
]),
SourceFileEdit('', -1, edits: [
SourceEdit(4, 4, ''), // Delete 4-56
]),
],
);
}
void test_overlap_secondDeletesFirst_andTrailing() {
verifyMerge(
start: '0123456789',
expected: '01234789',
edits: [
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '-5'), // Replace 5 with -5
]),
SourceFileEdit('', -1, edits: [
SourceEdit(5, 3, ''), // Delete -56
]),
],
);
}
void test_overlap_secondDeletesFirst_end() {
verifyMerge(
start: '0123456789',
expected: '01234-6789',
edits: [
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '-5'), // Replace 5 with -5
]),
SourceFileEdit('', -1, edits: [
SourceEdit(6, 1, ''), // Delete 5
]),
],
);
}
void test_overlap_secondDeletesFirst_exactly() {
verifyMerge(
start: '0123456789',
expected: '012346789',
edits: [
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '-5'), // Replace 5 with -5
]),
SourceFileEdit('', -1, edits: [
SourceEdit(5, 2, ''), // Delete -5
]),
],
);
}
void test_overlap_secondDeletesFirst_inner() {
verifyMerge(
start: '0123456789',
expected: '01234556789',
edits: [
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '5-5'), // Replace 5 with 5-5
]),
SourceFileEdit('', -1, edits: [
SourceEdit(6, 1, ''), // Delete -
]),
],
);
}
void test_overlap_secondDeletesFirst_start() {
verifyMerge(
start: '0123456789',
expected: '0123456789',
edits: [
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '-5'), // Replace 5 with -5
]),
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, ''), // Delete -
]),
],
);
}
void test_overlap_secondInsertsIntoFirst() {
verifyMerge(
start: '0123456789',
expected: '01234-056789',
edits: [
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '-5'), // Replace 5 with -5
]),
SourceFileEdit('', -1, edits: [
SourceEdit(6, 0, '0'), // Insert 0 between - and 5
]),
],
);
}
void test_overlap_secondInsertsIntoFirst2() {
verifyMerge(
start: 'AAAAAAAAAABBBBBBBBBB',
expected:
'AAAAAAAAAACCCCCCCCCCEEEEEEEEEEGGGGGGGGGGHHHHHHHHHHFFFFFFFFFFDDDDDDDDDDBBBBBBBBBB',
edits: [
SourceFileEdit('', -1, edits: [
// Insert CD between AB
SourceEdit(10, 0, 'CCCCCCCCCCDDDDDDDDDD'),
]),
SourceFileEdit('', -1, edits: [
// Insert EF between CD
SourceEdit(20, 0, 'EEEEEEEEEEFFFFFFFFFF'),
]),
SourceFileEdit('', -1, edits: [
// Insert GH between EF
SourceEdit(30, 0, 'GGGGGGGGGGHHHHHHHHHH'),
]),
],
);
}
void test_overlap_secondReplacesFirst_andLeading() {
verifyMerge(
start: '0123456789',
expected: '012306789',
edits: [
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '-5'), // Replace 5 with -5
]),
SourceFileEdit('', -1, edits: [
SourceEdit(4, 3, '0'), // Replace 4-5 with 0
]),
],
);
}
void test_overlap_secondReplacesFirst_andLeadingAndTrailing() {
verifyMerge(
start: '0123456789',
expected: '01230789',
edits: [
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '-5'), // Replace 5 with -5
]),
SourceFileEdit('', -1, edits: [
SourceEdit(4, 4, '0'), // Replace 4-56 with 0
]),
],
);
}
void test_overlap_secondReplacesFirst_andTrailing() {
verifyMerge(
start: '0123456789',
expected: '012340789',
edits: [
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '-5'), // Replace 5 with -5
]),
SourceFileEdit('', -1, edits: [
SourceEdit(5, 3, '0'), // Replace -56 with 0
]),
],
);
}
void test_overlap_secondReplacesFirst_end() {
verifyMerge(
start: '0123456789',
expected: '01234-06789',
edits: [
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '-5'), // Replace 5 with -5
]),
SourceFileEdit('', -1, edits: [
SourceEdit(6, 1, '0'), // Replace 5 with 0
]),
],
);
}
void test_overlap_secondReplacesFirst_exactly() {
verifyMerge(
start: '0123456789',
expected: '0123406789',
edits: [
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '-5'), // Replace 5 with -5
]),
SourceFileEdit('', -1, edits: [
SourceEdit(5, 2, '0'), // Replace -5 with 0
]),
],
);
}
void test_overlap_secondReplacesFirst_inner() {
verifyMerge(
start: '0123456789',
expected: '012345056789',
edits: [
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '5-5'), // Replace 5 with 5-5
]),
SourceFileEdit('', -1, edits: [
SourceEdit(6, 1, '0'), // Replace - with 0
]),
],
);
}
void test_overlap_secondReplacesFirst_start() {
verifyMerge(
start: '0123456789',
expected: '01234056789',
edits: [
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '-5'), // Replace 5 with -5
]),
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '0'), // Replace - with 0
]),
],
);
}
void test_touching_deletes() {
verifyMerge(
start: '0123456789',
expected: '06789',
edits: [
SourceFileEdit('', -1, edits: [
// Delete 5, 3, 1
SourceEdit(5, 1, ''),
SourceEdit(3, 1, ''),
SourceEdit(1, 1, ''),
]),
SourceFileEdit('', -1, edits: [
// Delete 4, 2
SourceEdit(2, 1, ''),
SourceEdit(1, 1, ''),
]),
],
);
}
void test_touching_inserts() {
verifyMerge(
start: '0123456789',
expected: '0-1-2-3-4-56789',
edits: [
SourceFileEdit('', -1, edits: [
// Insert - before 5, 3, 1
SourceEdit(5, 0, '-'),
SourceEdit(3, 0, '-'),
SourceEdit(1, 0, '-'),
]),
SourceFileEdit('', -1, edits: [
// Insert - before 4, 2
SourceEdit(6, 0, '-'),
SourceEdit(3, 0, '-'),
]),
],
);
}
void test_touching_mixed() {
verifyMerge(
start: '0123456789',
expected: '01234-60789',
edits: [
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, ''), // Delete 5
]),
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '-6'), // Change 6 to -6
]),
SourceFileEdit('', -1, edits: [
SourceEdit(7, 0, '0'), // Insert 0 between -6 and 7
]),
],
);
}
void test_touching_replacements() {
verifyMerge(
start: '0123456789',
expected: '0-1-2-3-4-56789',
edits: [
SourceFileEdit('', -1, edits: [
SourceEdit(5, 1, '-5'), // Replace 5 with -5
SourceEdit(3, 1, '-3'),
SourceEdit(1, 1, '-1'),
]),
SourceFileEdit('', -1, edits: [
SourceEdit(6, 1, '-4'), // 4 is at 6 because of two minuses before it
SourceEdit(3, 1, '-2'), // 2 is at 3...
]),
],
);
}
/// Verifies merged edits for a single file.
void verifyMerge({
required String start,
required String expected,
required List<SourceFileEdit> edits,
}) {
// First, apply edits sequentially to ensure the test is configured
// correctly.
_validateEdits(edits);
_verifyAppliedEdits(
start,
edits,
expected,
'Applying edits sequentially did not produce expected results. '
'This indicates a bug in the test.',
);
// Take a copy of the edits before merging to ensure they are not mutated.
var originalJson = jsonEncode(edits);
var debugBuffer = StringBuffer();
var merged = SourceChangeMerger(debugBuffer: debugBuffer).merge(edits);
// Ensure the merger didn't mutate the originals.
expect(jsonEncode(edits), originalJson);
try {
_validateMergedEdits(merged);
_verifyAppliedEdits(
start,
merged,
expected,
'Applying merged edits did not produce expected results. '
'This indicates a bug in the merger.',
);
} catch (_) {
print(debugBuffer);
rethrow;
}
}
/// Verifies that a [SourceFileEdit] meets some expected criteria:
///
/// - Edits are all for the same file
/// - Edits are ordered from latest offset to earliest
/// - No edits intersect
void _validateEdits(List<SourceFileEdit> edits) {
expect(
edits.map((edit) => edit.file).toSet(),
hasLength(1),
reason: 'All edits should be from the same file',
);
for (var fileEdit in edits) {
var lastOffset = fileEdit.edits.first.offset;
for (var edit in fileEdit.edits.skip(1)) {
expect(
edit.end,
lessThanOrEqualTo(lastOffset),
reason: 'Edits within a SourceFileEdit should be ordered from '
'last (highest offset) to first (lowest offset) and not overlap',
);
lastOffset = edit.offset;
}
}
}
/// Verifies that merged edits meet some expected criteria:
///
/// - Each file appears only once.
/// - Edits are ordered from latest offset to earliest
/// - No edits intersect
/// - No edits touch
void _validateMergedEdits(List<SourceFileEdit> edits) {
expect(
edits.map((edit) => edit.file).toSet(),
hasLength(edits.length),
reason: 'Merged edits should only contain one SourceFileEdit per file',
);
for (var fileEdit in edits) {
var lastOffset = fileEdit.edits.first.offset;
for (var edit in fileEdit.edits.skip(1)) {
expect(
edit.end,
lessThan(lastOffset),
reason: 'Edits within a SourceFileEdit should be ordered from '
'last (highest offset) to first (lowest offset) and not touch or '
'overlap',
);
lastOffset = edit.offset;
}
}
}
/// Verifies that [edits] applied to [content] produce [expected].
void _verifyAppliedEdits(
String content,
List<SourceFileEdit> edits,
String expected,
String reason,
) {
var result = edits.fold(
content,
(content, edit) => SourceEdit.applySequence(content, edit.edits),
);
expect(result, expected, reason: reason);
}
}