mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
734 lines
25 KiB
Swift
734 lines
25 KiB
Swift
// RUN: %target-run-simple-swift
|
|
// REQUIRES: executable_test
|
|
|
|
import Swift
|
|
import StdlibUnittest
|
|
|
|
let suite = TestSuite("Diffing")
|
|
|
|
// This availability test has to be this awkward because of
|
|
// rdar://problem/48450376 - Availability checks don't apply to top-level code
|
|
if #available(SwiftStdlib 5.1, *) {
|
|
|
|
suite.test("Diffing empty collections") {
|
|
let a = [Int]()
|
|
let b = [Int]()
|
|
let diff = b.difference(from: a)
|
|
expectEqual(diff, a.difference(from: a))
|
|
expectTrue(diff.isEmpty)
|
|
}
|
|
|
|
suite.test("Basic diffing algorithm validators") {
|
|
let expectedChanges: [(
|
|
source: [String],
|
|
target: [String],
|
|
changes: [CollectionDifference<String>.Change],
|
|
line: UInt
|
|
)] = [
|
|
(source:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne"],
|
|
target:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne"],
|
|
changes: [],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne"],
|
|
target:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Presents",
|
|
"New Years", "Champagne"],
|
|
changes: [
|
|
.remove(offset: 5, element: "Lights", associatedWith: nil)
|
|
],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne"],
|
|
target:
|
|
["Hannukah", "Menorah", "Dreidel", "Gelt",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne"],
|
|
changes: [
|
|
.insert(offset: 3, element: "Gelt", associatedWith: nil)
|
|
],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne"],
|
|
target:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Presents", "Tree", "Lights",
|
|
"New Years", "Champagne"],
|
|
changes: [
|
|
.remove(offset: 6, element: "Presents", associatedWith: 4),
|
|
.insert(offset: 4, element: "Presents", associatedWith: 6)
|
|
],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne"],
|
|
target:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Lights", "Presents", "Tree",
|
|
"New Years", "Champagne"],
|
|
changes: [
|
|
.remove(offset: 4, element: "Tree", associatedWith: 6),
|
|
.insert(offset: 6, element: "Tree", associatedWith: 4)
|
|
],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne"],
|
|
target:
|
|
["Hannukah", "Menorah", "Dreidel", "Presents",
|
|
"Xmas", "Tree", "Lights",
|
|
"New Years", "Champagne"],
|
|
changes: [
|
|
.remove(offset: 6, element: "Presents", associatedWith: 3),
|
|
.insert(offset: 3, element: "Presents", associatedWith: 6)
|
|
],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne"],
|
|
target:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights",
|
|
"New Years", "Champagne", "Presents"],
|
|
changes: [
|
|
.remove(offset: 6, element: "Presents", associatedWith: 8),
|
|
.insert(offset: 8, element: "Presents", associatedWith: 6)
|
|
],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne"],
|
|
target:
|
|
["Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne"],
|
|
changes: [
|
|
.remove(offset: 2, element: "Dreidel", associatedWith: nil),
|
|
.remove(offset: 1, element: "Menorah", associatedWith: nil),
|
|
.remove(offset: 0, element: "Hannukah", associatedWith: nil)
|
|
],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights", "Presents"],
|
|
target:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne"],
|
|
changes: [
|
|
.insert(offset: 7, element: "New Years", associatedWith: nil),
|
|
.insert(offset: 8, element: "Champagne", associatedWith: nil)
|
|
],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne"],
|
|
target:
|
|
["New Years", "Champagne",
|
|
"Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights", "Presents"],
|
|
changes: [
|
|
.remove(offset: 8, element: "Champagne", associatedWith: 1),
|
|
.remove(offset: 7, element: "New Years", associatedWith: 0),
|
|
.insert(offset: 0, element: "New Years", associatedWith: 7),
|
|
.insert(offset: 1, element: "Champagne", associatedWith: 8)
|
|
],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne"],
|
|
target:
|
|
["Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne",
|
|
"Hannukah", "Menorah", "Dreidel"],
|
|
changes: [
|
|
.remove(offset: 2, element: "Dreidel", associatedWith: 8),
|
|
.remove(offset: 1, element: "Menorah", associatedWith: 7),
|
|
.remove(offset: 0, element: "Hannukah", associatedWith: 6),
|
|
.insert(offset: 6, element: "Hannukah", associatedWith: 0),
|
|
.insert(offset: 7, element: "Menorah", associatedWith: 1),
|
|
.insert(offset: 8, element: "Dreidel", associatedWith: 2)
|
|
],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah", "Dreidel", "Presents",
|
|
"Xmas", "Tree", "Lights",
|
|
"New Years", "Champagne"],
|
|
target:
|
|
["Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne"],
|
|
changes: [
|
|
.remove(offset: 3, element: "Presents", associatedWith: 3),
|
|
.remove(offset: 2, element: "Dreidel", associatedWith: nil),
|
|
.remove(offset: 1, element: "Menorah", associatedWith: nil),
|
|
.remove(offset: 0, element: "Hannukah", associatedWith: nil),
|
|
.insert(offset: 3, element: "Presents", associatedWith: 3)
|
|
],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights", "Presents"],
|
|
target:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Presents",
|
|
"New Years", "Champagne", "Lights"],
|
|
changes: [
|
|
.remove(offset: 5, element: "Lights", associatedWith: 8),
|
|
.insert(offset: 6, element: "New Years", associatedWith: nil),
|
|
.insert(offset: 7, element: "Champagne", associatedWith: nil),
|
|
.insert(offset: 8, element: "Lights", associatedWith: 5)
|
|
],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne"],
|
|
target:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years"],
|
|
changes: [
|
|
.remove(offset: 8, element: "Champagne", associatedWith: nil)
|
|
],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah", "Dreidel", "Presents",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne", "Presents"],
|
|
target:
|
|
["Hannukah", "Menorah", "Dreidel", "Presents",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne", "Presents"],
|
|
changes: [],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah", "Dreidel", "Presents",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne", "Presents"],
|
|
target:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights",
|
|
"New Years", "Champagne", "Presents"],
|
|
changes: [
|
|
.remove(offset: 7, element: "Presents", associatedWith: nil),
|
|
.remove(offset: 3, element: "Presents", associatedWith: nil)
|
|
],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights",
|
|
"New Years", "Champagne", "Presents"],
|
|
target:
|
|
["Hannukah", "Menorah", "Dreidel", "Presents",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne", "Presents"],
|
|
changes: [
|
|
.insert(offset: 3, element: "Presents", associatedWith: nil),
|
|
.insert(offset: 7, element: "Presents", associatedWith: nil)
|
|
],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah", "Dreidel", "Presents",
|
|
"Xmas", "Tree", "Lights",
|
|
"New Years", "Champagne", "Presents"],
|
|
target:
|
|
["Hannukah", "Menorah", "Dreidel",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne", "Presents"],
|
|
changes: [
|
|
.remove(offset: 3, element: "Presents", associatedWith: 6),
|
|
.insert(offset: 6, element: "Presents", associatedWith: 3)
|
|
],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne",
|
|
"Hannukah", "Dreidel"],
|
|
target:
|
|
["Hannukah", "Menorah",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne",
|
|
"Hannukah", "Dreidel"],
|
|
changes: [],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne",
|
|
"Hannukah", "Dreidel"],
|
|
target:
|
|
["Hannukah", "Menorah",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne"],
|
|
changes: [
|
|
.remove(offset: 9, element: "Dreidel", associatedWith: nil),
|
|
.remove(offset: 8, element: "Hannukah", associatedWith: nil)
|
|
],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne"],
|
|
target:
|
|
["Hannukah", "Menorah",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne",
|
|
"Hannukah", "Dreidel"],
|
|
changes: [
|
|
.insert(offset: 8, element: "Hannukah", associatedWith: nil),
|
|
.insert(offset: 9, element: "Dreidel", associatedWith: nil)
|
|
],
|
|
line: #line),
|
|
|
|
(source:
|
|
["Hannukah", "Menorah",
|
|
"Xmas", "Tree", "Lights", "Presents",
|
|
"New Years", "Champagne",
|
|
"Hannukah", "Dreidel"],
|
|
target:
|
|
["Xmas", "Tree", "Lights", "Presents",
|
|
"Hannukah", "Menorah",
|
|
"New Years", "Champagne",
|
|
"Hannukah", "Dreidel"],
|
|
changes: [
|
|
.remove(offset: 1, element: "Menorah", associatedWith: 5),
|
|
.remove(offset: 0, element: "Hannukah", associatedWith: 4),
|
|
.insert(offset: 4, element: "Hannukah", associatedWith: 0),
|
|
.insert(offset: 5, element: "Menorah", associatedWith: 1)
|
|
],
|
|
line: #line),
|
|
]
|
|
|
|
for (source, target, expected, line) in expectedChanges {
|
|
let actual = target.difference(from: source).inferringMoves()
|
|
expectEqual(CollectionDifference(expected), actual, "failed test at line \(line)")
|
|
}
|
|
}
|
|
|
|
suite.test("Empty diffs have sane behaviour") {
|
|
guard let diff = CollectionDifference<String>([]) else {
|
|
expectUnreachable()
|
|
return
|
|
}
|
|
expectEqual(0, diff.insertions.count)
|
|
expectEqual(0, diff.removals.count)
|
|
expectEqual(true, diff.isEmpty)
|
|
|
|
var c = 0
|
|
diff.forEach({ _ in c += 1 })
|
|
expectEqual(0, c)
|
|
}
|
|
|
|
suite.test("Happy path tests for the change validator") {
|
|
// Base case: one insert and one remove with legal offsets
|
|
expectNotNil(CollectionDifference<Int>.init([
|
|
.insert(offset: 0, element: 0, associatedWith: nil),
|
|
.remove(offset: 0, element: 0, associatedWith: nil)
|
|
]))
|
|
|
|
// Code coverage:
|
|
// • non-first change .remove has legal associated offset
|
|
// • non-first change .insert has legal associated offset
|
|
expectNotNil(CollectionDifference<Int>.init([
|
|
.remove(offset: 1, element: 0, associatedWith: 0),
|
|
.remove(offset: 0, element: 0, associatedWith: 1),
|
|
.insert(offset: 0, element: 0, associatedWith: 1),
|
|
.insert(offset: 1, element: 0, associatedWith: 0)
|
|
]))
|
|
}
|
|
|
|
suite.test("Exhaustive edge case tests for the change validator") {
|
|
// Base case: two inserts sharing the same offset
|
|
expectNil(CollectionDifference<Int>.init([
|
|
.insert(offset: 0, element: 0, associatedWith: nil),
|
|
.insert(offset: 0, element: 0, associatedWith: nil)
|
|
]))
|
|
|
|
// Base case: two removes sharing the same offset
|
|
expectNil(CollectionDifference<Int>.init([
|
|
.remove(offset: 0, element: 0, associatedWith: nil),
|
|
.remove(offset: 0, element: 0, associatedWith: nil)
|
|
]))
|
|
|
|
// Base case: illegal insertion offset
|
|
expectNil(CollectionDifference<Int>.init([
|
|
.insert(offset: -1, element: 0, associatedWith: nil)
|
|
]))
|
|
|
|
// Base case: illegal remove offset
|
|
expectNil(CollectionDifference<Int>.init([
|
|
.remove(offset: -1, element: 0, associatedWith: nil)
|
|
]))
|
|
|
|
// Base case: two inserts sharing same associated offset
|
|
expectNil(CollectionDifference<Int>.init([
|
|
.insert(offset: 0, element: 0, associatedWith: 0),
|
|
.insert(offset: 1, element: 0, associatedWith: 0)
|
|
]))
|
|
|
|
// Base case: two removes sharing same associated offset
|
|
expectNil(CollectionDifference<Int>.init([
|
|
.remove(offset: 0, element: 0, associatedWith: 0),
|
|
.remove(offset: 1, element: 0, associatedWith: 0)
|
|
]))
|
|
|
|
// Base case: insert with illegal associated offset
|
|
expectNil(CollectionDifference<Int>.init([
|
|
.insert(offset: 0, element: 0, associatedWith: -1)
|
|
]))
|
|
|
|
// Base case: remove with illegal associated offset
|
|
expectNil(CollectionDifference<Int>.init([
|
|
.remove(offset: 1, element: 0, associatedWith: -1)
|
|
]))
|
|
|
|
// Code coverage: non-first change has illegal offset
|
|
expectNil(CollectionDifference<Int>.init([
|
|
.remove(offset: 0, element: 0, associatedWith: nil),
|
|
.insert(offset: -1, element: 0, associatedWith: nil)
|
|
]))
|
|
|
|
// Code coverage: non-first change has illegal associated offset
|
|
expectNil(CollectionDifference<Int>.init([
|
|
.remove(offset: 0, element: 0, associatedWith: nil),
|
|
.insert(offset: 0, element: 0, associatedWith: -1)
|
|
]))
|
|
}
|
|
|
|
suite.test("Enumeration order is safe") {
|
|
let safelyOrderedChanges: [CollectionDifference<Int>.Change] = [
|
|
.remove(offset: 2, element: 0, associatedWith: nil),
|
|
.remove(offset: 1, element: 0, associatedWith: 0),
|
|
.remove(offset: 0, element: 0, associatedWith: 1),
|
|
.insert(offset: 0, element: 0, associatedWith: 1),
|
|
.insert(offset: 1, element: 0, associatedWith: 0),
|
|
.insert(offset: 2, element: 0, associatedWith: nil),
|
|
]
|
|
let diff = CollectionDifference<Int>.init(safelyOrderedChanges)!
|
|
var enumerationOrderedChanges = [CollectionDifference<Int>.Change]()
|
|
diff.forEach { c in
|
|
enumerationOrderedChanges.append(c)
|
|
}
|
|
expectEqual(enumerationOrderedChanges, safelyOrderedChanges)
|
|
}
|
|
|
|
suite.test("Change validator rejects bad associations") {
|
|
// .remove(1) → .insert(1)
|
|
// ↑ ↓
|
|
// .insert(0) ← .remove(0)
|
|
expectNil(CollectionDifference<Int>.init([
|
|
.remove(offset: 1, element: 0, associatedWith: 1),
|
|
.remove(offset: 0, element: 0, associatedWith: 0),
|
|
.insert(offset: 0, element: 0, associatedWith: 1),
|
|
.insert(offset: 1, element: 0, associatedWith: 0)
|
|
]))
|
|
|
|
// Coverage: duplicate remove offsets both with assocs
|
|
expectNil(CollectionDifference<Int>.init([
|
|
.remove(offset: 0, element: 0, associatedWith: 1),
|
|
.remove(offset: 0, element: 0, associatedWith: 0),
|
|
]))
|
|
|
|
// Coverage: duplicate insert assocs
|
|
expectNil(CollectionDifference<Int>.init([
|
|
.insert(offset: 0, element: 0, associatedWith: 1),
|
|
.insert(offset: 1, element: 0, associatedWith: 1),
|
|
]))
|
|
}
|
|
|
|
// Full-coverage test for CollectionDifference.Change.==()
|
|
suite.test("Exhaustive testing for equatable conformance") {
|
|
// Differs by type:
|
|
expectNotEqual(
|
|
CollectionDifference<Int>.Change.insert(offset: 0, element: 0, associatedWith: 0),
|
|
CollectionDifference<Int>.Change.remove(offset: 0, element: 0, associatedWith: 0)
|
|
)
|
|
|
|
// Differs by type in the other direction:
|
|
expectNotEqual(
|
|
CollectionDifference<Int>.Change.remove(offset: 0, element: 0, associatedWith: 0),
|
|
CollectionDifference<Int>.Change.insert(offset: 0, element: 0, associatedWith: 0)
|
|
)
|
|
|
|
// Insert differs by offset
|
|
expectNotEqual(
|
|
CollectionDifference<Int>.Change.insert(offset: 0, element: 0, associatedWith: 0),
|
|
CollectionDifference<Int>.Change.insert(offset: 1, element: 0, associatedWith: 0)
|
|
)
|
|
|
|
// Insert differs by element
|
|
expectNotEqual(
|
|
CollectionDifference<Int>.Change.insert(offset: 0, element: 0, associatedWith: 0),
|
|
CollectionDifference<Int>.Change.insert(offset: 0, element: 1, associatedWith: 0)
|
|
)
|
|
|
|
// Insert differs by association
|
|
expectNotEqual(
|
|
CollectionDifference<Int>.Change.insert(offset: 0, element: 0, associatedWith: 0),
|
|
CollectionDifference<Int>.Change.insert(offset: 0, element: 0, associatedWith: 1)
|
|
)
|
|
|
|
// Remove differs by offset
|
|
expectNotEqual(
|
|
CollectionDifference<Int>.Change.remove(offset: 0, element: 0, associatedWith: 0),
|
|
CollectionDifference<Int>.Change.remove(offset: 1, element: 0, associatedWith: 0)
|
|
)
|
|
|
|
// Remove differs by element
|
|
expectNotEqual(
|
|
CollectionDifference<Int>.Change.remove(offset: 0, element: 0, associatedWith: 0),
|
|
CollectionDifference<Int>.Change.remove(offset: 0, element: 1, associatedWith: 0)
|
|
)
|
|
|
|
// Remove differs by association
|
|
expectNotEqual(
|
|
CollectionDifference<Int>.Change.remove(offset: 0, element: 0, associatedWith: 0),
|
|
CollectionDifference<Int>.Change.remove(offset: 0, element: 0, associatedWith: 1)
|
|
)
|
|
}
|
|
|
|
suite.test("Compile-time test of hashable conformance") {
|
|
let _ = Set<CollectionDifference<String>>();
|
|
}
|
|
|
|
suite.test("Move inference") {
|
|
let n = CollectionDifference<String>.init([
|
|
.insert(offset: 3, element: "Sike", associatedWith: nil),
|
|
.insert(offset: 4, element: "Sike", associatedWith: nil),
|
|
.insert(offset: 2, element: "Hello", associatedWith: nil),
|
|
.remove(offset: 6, element: "Hello", associatedWith: nil),
|
|
.remove(offset: 8, element: "Goodbye", associatedWith: nil),
|
|
.remove(offset: 9, element: "Sike", associatedWith: nil),
|
|
])
|
|
let w = CollectionDifference<String>.init([
|
|
.insert(offset: 3, element: "Sike", associatedWith: nil),
|
|
.insert(offset: 4, element: "Sike", associatedWith: nil),
|
|
.insert(offset: 2, element: "Hello", associatedWith: 6),
|
|
.remove(offset: 6, element: "Hello", associatedWith: 2),
|
|
.remove(offset: 8, element: "Goodbye", associatedWith: nil),
|
|
.remove(offset: 9, element: "Sike", associatedWith: nil),
|
|
])
|
|
expectEqual(w, n?.inferringMoves())
|
|
}
|
|
|
|
suite.test("Three way merge") {
|
|
let baseLines = ["Is", "it", "time", "already?"]
|
|
let theirLines = ["Hi", "there", "is", "it", "time", "already?"]
|
|
let myLines = ["Is", "it", "review", "time", "already?"]
|
|
|
|
// Create a difference from base to theirs
|
|
let diff = theirLines.difference(from: baseLines)
|
|
|
|
// Apply it to mine, if possible
|
|
guard let patchedLines = myLines.applying(diff) else {
|
|
print("Merge conflict applying patch, manual merge required")
|
|
return
|
|
}
|
|
|
|
// Reassemble the result
|
|
expectEqual(patchedLines, ["Hi", "there", "is", "it", "review", "time", "already?"])
|
|
// print(patched)
|
|
}
|
|
|
|
suite.test("Diff reversal demo code") {
|
|
let diff = CollectionDifference<Int>([])!
|
|
let _ = CollectionDifference<Int>(
|
|
diff.map({(change) -> CollectionDifference<Int>.Change in
|
|
switch change {
|
|
case .insert(offset: let o, element: let e, associatedWith: let a):
|
|
return .remove(offset: o, element: e, associatedWith: a)
|
|
case .remove(offset: let o, element: let e, associatedWith: let a):
|
|
return .insert(offset: o, element: e, associatedWith: a)
|
|
}
|
|
})
|
|
)!
|
|
}
|
|
|
|
suite.test("Naive application by enumeration") {
|
|
var arr = ["Is", "it", "time", "already?"]
|
|
let theirLines = ["Hi", "there", "is", "it", "time", "already?"]
|
|
|
|
// Create a difference from base to theirs
|
|
let diff = theirLines.difference(from: arr)
|
|
|
|
for c in diff {
|
|
switch c {
|
|
case .remove(offset: let o, element: _, associatedWith: _):
|
|
arr.remove(at: o)
|
|
case .insert(offset: let o, element: let e, associatedWith: _):
|
|
arr.insert(e, at: o)
|
|
}
|
|
}
|
|
|
|
expectEqual(theirLines, arr)
|
|
}
|
|
|
|
suite.test("Fast applicator boundary conditions") {
|
|
let a = [1, 2, 3, 4, 5, 6, 7, 8]
|
|
for removeMiddle in [false, true] {
|
|
for insertMiddle in [false, true] {
|
|
for removeLast in [false, true] {
|
|
for insertLast in [false, true] {
|
|
for removeFirst in [false, true] {
|
|
for insertFirst in [false, true] {
|
|
var b = a
|
|
|
|
// Prepare b
|
|
if removeMiddle { b.remove(at: 4) }
|
|
if insertMiddle { b.insert(10, at: 4) }
|
|
if removeLast { b.removeLast() }
|
|
if insertLast { b.append(11) }
|
|
if removeFirst { b.removeFirst() }
|
|
if insertFirst { b.insert(12, at: 0) }
|
|
|
|
// Generate diff
|
|
let diff = b.difference(from: a)
|
|
|
|
// Validate application
|
|
expectEqual(b, a.applying(diff)!)
|
|
expectEqual(a, b.applying(diff.inverse())!)
|
|
}}}}}}
|
|
}
|
|
}
|
|
|
|
if #available(SwiftStdlib 5.2, *) {
|
|
|
|
suite.test("Fast applicator error condition") {
|
|
let bear = "bear"
|
|
let bird = "bird"
|
|
let bat = "bat"
|
|
|
|
let diff = bird.difference(from: bear)
|
|
|
|
expectEqual(nil, bat.applying(diff))
|
|
}
|
|
|
|
suite.test("Fast applicator boundary condition remove last element") {
|
|
let base = [1, 2, 3]
|
|
|
|
expectEqual([1, 2], base.applying(CollectionDifference<Int>([.remove(offset: base.count - 1, element: 3, associatedWith: nil)])!))
|
|
}
|
|
|
|
suite.test("Fast applicator boundary condition append element") {
|
|
let base = [1, 2, 3]
|
|
|
|
expectEqual([1, 2, 3, 4], base.applying(CollectionDifference<Int>([.insert(offset: base.count, element: 4, associatedWith: nil)])!))
|
|
}
|
|
|
|
suite.test("Fast applicator error boundary condition remove at endIndex") {
|
|
let base = [1, 2, 3]
|
|
|
|
expectEqual(nil, base.applying(CollectionDifference<Int>([.remove(offset: base.count, element: 4, associatedWith: nil)])!))
|
|
}
|
|
|
|
suite.test("Fast applicator error boundary condition insert beyond end") {
|
|
let base = [1, 2, 3]
|
|
|
|
expectEqual(nil, base.applying(CollectionDifference<Int>([.insert(offset: base.count + 1, element: 5, associatedWith: nil)])!))
|
|
}
|
|
|
|
suite.test("Fast applicator boundary condition replace tail") {
|
|
let base = [1, 2, 3]
|
|
|
|
expectEqual([1, 2, 4], base.applying(CollectionDifference<Int>([
|
|
.remove(offset: base.count - 1, element: 3, associatedWith: nil),
|
|
.insert(offset: base.count - 1, element: 4, associatedWith: nil)
|
|
])!))
|
|
}
|
|
|
|
suite.test("Fast applicator error boundary condition insert beyond end after tail removal") {
|
|
let base = [1, 2, 3]
|
|
|
|
expectEqual(nil, base.applying(CollectionDifference<Int>([
|
|
.remove(offset: base.count - 1, element: 3, associatedWith: nil),
|
|
.insert(offset: base.count, element: 4, associatedWith: nil)
|
|
])!))
|
|
|
|
}
|
|
|
|
suite.test("Fast applicator error boundary condition insert beyond end after non-tail removal") {
|
|
let base = [1, 2, 3]
|
|
|
|
expectEqual(nil, base.applying(CollectionDifference<Int>([
|
|
.remove(offset: base.count - 2, element: 2, associatedWith: nil),
|
|
.insert(offset: base.count, element: 4, associatedWith: nil)
|
|
])!))
|
|
}
|
|
}
|
|
|
|
if #available(SwiftStdlib 5.1, *) {
|
|
|
|
suite.test("Fast applicator fuzzer") {
|
|
func makeArray() -> [Int] {
|
|
var arr = [Int]()
|
|
for _ in 0..<Int.random(in: 0..<10) {
|
|
arr.append(Int.random(in: 0..<20))
|
|
}
|
|
return arr
|
|
}
|
|
for _ in 0..<1000 {
|
|
let a = makeArray()
|
|
let b = makeArray()
|
|
let d = b.difference(from: a)
|
|
let applied = a.applying(d)
|
|
expectNotNil(applied)
|
|
if let applied = applied {
|
|
expectEqual(b, applied)
|
|
expectEqual(a, applied.applying(d.inverse()))
|
|
if (b != applied) {
|
|
print("""
|
|
// repro:
|
|
let a = \(a)
|
|
let b = \(b)
|
|
let d = b.difference(from: a)
|
|
expectEqual(b, a.applying(d))
|
|
expectEqual(a, applied.applying(d.inverse()))
|
|
""")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
runAllTests()
|