MobiusTest/Source/DebugDiff.swift (81 lines of code) (raw):

// Copyright Spotify AB. // SPDX-License-Identifier: Apache-2.0 /// Diff two values by comparing their dumps by line by line. /// - Parameters: /// - lhs: Old value /// - rhs: New value /// - Returns: formatted diff string func dumpDiff<T>(_ lhs: T, _ rhs: T) -> String { let lhsLines = dumpUnwrapped(lhs).lines let rhsLines = dumpUnwrapped(rhs).lines let diffList = diff(lhs: lhsLines, rhs: rhsLines) return diffList.diffString } /// Diff two collections of items by picking the most similar value from `actual` for each of the items /// in `expected`. Matched values are diffed by comparing their dumps line by line. /// /// “Similar” is defined as starting with at least one matching line – in the common case of enums with /// associated types, this means the same case. “Best match” is defined as the one with the smallest number of /// line differences. /// /// - Parameters: /// - expected: Values that are expected to be found in `actual` /// - actual: Values that the `expected` values are diffed against /// - withUnmatchedActual: Whether the unmatched values from `actual` should be included in the diff /// - Returns: formatted diff string func dumpDiffFuzzy<T>(expected: [T], actual: [T], withUnmatchedActual: Bool) -> String where T: Equatable { var actual = actual let diffItem = { (item: T) -> [Difference] in let closestResult = closestDiff( for: item, in: actual, predicate: { $0.first?.isSame ?? false } // Only use diff if first line (typically case name) matches ) if let diffList = closestResult.0, let matchedCandidate = closestResult.1, let matchedIndex = actual.firstIndex(of: matchedCandidate) { actual.remove(at: matchedIndex) return diffList } else { return [Difference.delete(dumpUnwrapped(item).lines)] } } let expectedDifference = expected.flatMap(diffItem) let unmatchedActualDifference = withUnmatchedActual ? actual.map { Difference.insert(dumpUnwrapped($0).lines) } : [] return (expectedDifference + unmatchedActualDifference).diffString } func dumpUnwrapped<T>(_ value: T) -> String { var valueDump: String = "" let mirror = Mirror(reflecting: value) if mirror.displayStyle == .optional, let first = mirror.children.first { dump(first.value, to: &valueDump) } else { dump(value, to: &valueDump) } return valueDump } func closestDiff<T, S: Sequence>( for value: T, in sequence: S, predicate: ([Difference]) -> Bool = { _ in true } ) -> ([Difference]?, T?) where S.Element == T { var closestDiff: [Difference]? var closestDistance = Int.max var closestCandidate: T? let unwrappedValue = dumpUnwrapped(value).lines sequence.forEach { candidate in let unwrappedCandidate = dumpUnwrapped(candidate).lines let diffList = diff(lhs: unwrappedValue, rhs: unwrappedCandidate) let distance = diffList.diffCount if distance < closestDistance && predicate(diffList) { closestDiff = diffList closestDistance = distance closestCandidate = candidate } } return (closestDiff, closestCandidate) } private extension String { var lines: ArraySlice<Substring> { split(separator: "\n")[...] } } private extension Array where Element == Difference { // Return the number of entries that are differences var diffCount: Int { reduce(0) { count, element in switch element { case .insert(let lines), .delete(let lines): return count + lines.count case .same: return count } } } var diffString: String { flatMap { diff in diff.string.map { "\(diff.prefix) \($0)" } } .joined(separator: "\n") } }