diff --git a/spock-core/src/main/java/org/spockframework/runtime/FailedStringComparisonRenderer.java b/spock-core/src/main/java/org/spockframework/runtime/FailedStringComparisonRenderer.java
index 2369087027..2a0174342c 100644
--- a/spock-core/src/main/java/org/spockframework/runtime/FailedStringComparisonRenderer.java
+++ b/spock-core/src/main/java/org/spockframework/runtime/FailedStringComparisonRenderer.java
@@ -8,7 +8,10 @@
import java.util.Locale;
public class FailedStringComparisonRenderer implements ExpressionComparisonRenderer {
- public static final long MAX_EDIT_DISTANCE_MEMORY = 50 * 1024;
+ // The edit distance computation needs O(N + M) memory since its linear-space rewrite, so this
+ // limit only bounds the computation effort: MAX cells cost milliseconds and a few MB, large
+ // enough to render diffs for differing sections of ~1000 characters directly.
+ public static final long MAX_EDIT_DISTANCE_MEMORY = 1024 * 1024;
@Override
public String render(ExpressionInfo expr) {
if (!(Boolean.FALSE.equals(expr.getValue()))) return null;
@@ -43,14 +46,14 @@ private String tryReduceStringSizes(String str1, String str2) {
end1++;
end2++;
- if (((long) end1-commonStart) * (end2-commonStart) > MAX_EDIT_DISTANCE_MEMORY) {
+ if (((long) end1 - commonStart) * (end2 - commonStart) > MAX_EDIT_DISTANCE_MEMORY) {
return "false\nStrings too large to calculate edit distance.";
} else {
- // Check if we can add some context
- if (((long) end1 - commonStart + 20) * (end2 - commonStart + 20) < MAX_EDIT_DISTANCE_MEMORY){
- commonStart = Math.max(0, commonStart - 10);
- end1 = Math.min(str1.length(), end1 + 10);
- end2 = Math.min(str2.length(), end2 + 10);
+ // Check if we can add some context around the differing section
+ if (((long) end1 - commonStart + 500) * (end2 - commonStart + 500) < MAX_EDIT_DISTANCE_MEMORY){
+ commonStart = Math.max(0, commonStart - 250);
+ end1 = Math.min(str1.length(), end1 + 250);
+ end2 = Math.min(str2.length(), end2 + 250);
}
return createAndRenderEditDistance(str1, str2, commonStart, end1, end2);
}
diff --git a/spock-core/src/main/java/org/spockframework/runtime/condition/EditDistance.java b/spock-core/src/main/java/org/spockframework/runtime/condition/EditDistance.java
index e499615a3a..f040367356 100644
--- a/spock-core/src/main/java/org/spockframework/runtime/condition/EditDistance.java
+++ b/spock-core/src/main/java/org/spockframework/runtime/condition/EditDistance.java
@@ -4,7 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
+ *
* https://www.apache.org/licenses/LICENSE-2.0
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -14,53 +16,51 @@
package org.spockframework.runtime.condition;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.List;
import static org.spockframework.runtime.condition.EditOperation.Kind.*;
/**
* Calculates Levenshtein distance and corresponding edit path between two character sequences.
- * Inspired from: https://etorreborre.blogspot.com/2008/06/edit-distance-in-scala_245.html
- *
- * Ideas for improvements:
- * - Favor fewer EditOperationS when calculating distance and/or path
- * - Use algorithm with lower time and/or space complexity
- *
- * @author Peter Niederwieser
+ *
+ * Uses a linear-space divide-and-conquer algorithm ("Optimal Alignments in Linear Space",
+ * Eugene W. Myers & Webb Miller, 1988), a.k.a. Hirschberg's technique applied to the
+ * Levenshtein dynamic program: space is O(N + M) and time O(N · M). This replaces the
+ * previous dense distance matrix, which needed O(N · M) space and therefore could
+ * not handle large inputs at all. Common prefixes and suffixes are trimmed away first, so for
+ * near-identical inputs — the typical case in a failed equality condition — only the
+ * small differing middle section is processed.
+ *
+ * The edit path groups consecutive edits into runs and pairs up deletions and insertions into
+ * {@link EditOperation.Kind#SUBSTITUTE} runs, matching the output shape of the previous
+ * matrix-based implementation. The distance is derived from the edit path, so the two are always
+ * consistent.
*/
public class EditDistance {
private final CharSequence seq1;
private final CharSequence seq2;
- private final int[][] matrix;
+ private final int distance;
+ private final List path;
public EditDistance(CharSequence seq1, CharSequence seq2) {
this.seq1 = seq1;
this.seq2 = seq2;
- matrix = new int[seq1.length() + 1][];
- calculateMatrix();
- }
- private void calculateMatrix() {
- for (int i = 0; i < seq1.length() + 1; i++) {
- matrix[i] = new int[seq2.length() + 1];
- for (int j = 0; j < seq2.length() + 1; j++) {
- if (i == 0) matrix[i][j] = j; // j insertions
- else if (j == 0) matrix[i][j] = i; // i deletions
- else matrix[i][j] = min(
- matrix[i][j - 1] + 1, // insertion
- matrix[i - 1][j] + 1, // deletion
- matrix[i - 1][j - 1] + (seq1.charAt(i - 1) == seq2.charAt(j - 1) ? 0 : 1)); // substitution
- }
- }
- }
+ List operations = new ArrayList<>();
+ collectOperations(operations, 0, 0, seq1.length(), seq2.length());
+ path = normalize(operations);
- public int[][] getMatrix() {
- return matrix;
+ int result = 0;
+ for (EditOperation operation : path) {
+ if (operation.getKind() != SKIP) result += operation.getLength();
+ }
+ distance = result;
}
public int getDistance() {
- return matrix[seq1.length()][seq2.length()];
+ return distance;
}
public int getSimilarityInPercent() {
@@ -69,50 +69,185 @@ public int getSimilarityInPercent() {
}
public List calculatePath() {
- LinkedList ops = new LinkedList<>();
- int i = seq1.length();
- int j = seq2.length();
- int dist = matrix[i][j];
-
- while (i > 0 && j > 0 && dist > 0) {
- int ins = matrix[i][j - 1];
- int del = matrix[i - 1][j];
- int sub = matrix[i - 1][j - 1];
-
- if (dist == ins + 1) {
- addOrUpdate(ops, INSERT, 1);
- j--;
- } else if (dist == del + 1) {
- addOrUpdate(ops, DELETE, 1);
- i--;
+ List result = new ArrayList<>(path.size());
+ for (EditOperation operation : path) {
+ result.add(new EditOperation(operation.getKind(), operation.getLength()));
+ }
+ return result;
+ }
+
+ /**
+ * Collects the edit operations for the region {@code [from1, to1) x [from2, to2)} in path order,
+ * splitting the region at an optimal midpoint.
+ */
+ private void collectOperations(List operations, int from1, int from2, int to1, int to2) {
+ int start = from1;
+ while (start < to1 && from2 < to2 && seq1.charAt(start) == seq2.charAt(from2)) {
+ start++;
+ from2++;
+ }
+ addOperation(operations, SKIP, start - from1);
+
+ int end1 = to1;
+ int end2 = to2;
+ while (end1 > start && end2 > from2 && seq1.charAt(end1 - 1) == seq2.charAt(end2 - 1)) {
+ end1--;
+ end2--;
+ }
+
+ int remaining1 = end1 - start;
+ int remaining2 = end2 - from2;
+
+ if (remaining1 == 0) {
+ addOperation(operations, INSERT, remaining2);
+ } else if (remaining2 == 0) {
+ addOperation(operations, DELETE, remaining1);
+ } else if (remaining1 == 1) {
+ // the one remaining seq1 character either matches one seq2 character (move it) or not;
+ // both choices cost the same for every matching position, so the first one is as good as any
+ int match = indexOf(seq2, from2, end2, seq1.charAt(start));
+ if (match >= 0) {
+ addOperation(operations, INSERT, match - from2);
+ addOperation(operations, SKIP, 1);
+ addOperation(operations, INSERT, end2 - match - 1);
} else {
- if (dist == sub) addOrUpdate(ops, SKIP, 1);
- else addOrUpdate(ops, SUBSTITUTE, 1);
- i--; j--;
+ addOperation(operations, SUBSTITUTE, 1);
+ addOperation(operations, INSERT, end2 - from2 - 1);
}
+ } else if (remaining2 == 1) {
+ // the one remaining seq2 character either matches one seq1 character (move it) or not;
+ // deletions are emitted before insertions, like in the remaining1 == 1 case above, so that
+ // neighboring regions normalize into as many substitution runs as possible
+ int match = indexOf(seq1, start, end1, seq2.charAt(from2));
+ if (match >= 0) {
+ addOperation(operations, DELETE, match - start);
+ addOperation(operations, SKIP, 1);
+ addOperation(operations, DELETE, end1 - match - 1);
+ } else {
+ addOperation(operations, DELETE, end1 - start - 1);
+ addOperation(operations, SUBSTITUTE, 1);
+ }
+ } else {
+ // bisect seq1 and find the column an optimal path crosses the bisection row in
+ int mid1 = start + remaining1 / 2;
+ int mid2 = findMidpoint(start, from2, mid1, end1, end2);
- dist = matrix[i][j];
+ collectOperations(operations, start, from2, mid1, mid2);
+ collectOperations(operations, mid1, mid2, end1, end2);
}
- if (i == 0) addOrUpdate(ops, INSERT, j);
- else if (j == 0) addOrUpdate(ops, DELETE, i);
- else addOrUpdate(ops, SKIP, i);
+ if (end1 < to1) addOperation(operations, SKIP, to1 - end1);
+ }
- return ops;
+ private int findMidpoint(int from1, int from2, int mid1, int to1, int to2) {
+ int[] forward = forwardCosts(from1, from2, mid1, to2);
+ int[] backward = backwardCosts(mid1, from2, to1, to2);
+
+ int midpoint = from2;
+ int minCost = Integer.MAX_VALUE;
+ for (int j = 0; j < forward.length; j++) {
+ int cost = forward[j] + backward[j];
+ if (cost < minCost) {
+ minCost = cost;
+ midpoint = from2 + j;
+ }
+ }
+ return midpoint;
}
- private void addOrUpdate(LinkedList ops, EditOperation.Kind kind, int length) {
- if (length == 0) return;
+ /**
+ * Computes the costs of converting {@code seq1[from1..to1)} to the prefixes
+ * {@code seq2[from2..from2 + j)} for all j, using O(length) space.
+ */
+ private int[] forwardCosts(int from1, int from2, int to1, int to2) {
+ int length = to2 - from2;
+ int[] previous = new int[length + 1];
+ int[] current = new int[length + 1];
+ for (int j = 0; j <= length; j++) {
+ previous[j] = j;
+ }
+ for (int i = from1; i < to1; i++) {
+ current[0] = i - from1 + 1;
+ for (int j = 1; j <= length; j++) {
+ current[j] = min(current[j - 1] + 1,
+ previous[j] + 1,
+ previous[j - 1] + (seq1.charAt(i) == seq2.charAt(from2 + j - 1) ? 0 : 1));
+ }
+ int[] swap = previous;
+ previous = current;
+ current = swap;
+ }
+ return previous;
+ }
- if (!ops.isEmpty() && ops.getFirst().getKind() == kind)
- ops.getFirst().incLength(length);
- else
- ops.addFirst(new EditOperation(kind, length));
+ /**
+ * Computes the costs of converting {@code seq1[from1..to1)} to the suffixes
+ * {@code seq2[from2 + j..to2)} for all j, using O(length) space.
+ */
+ private int[] backwardCosts(int from1, int from2, int to1, int to2) {
+ int length = to2 - from2;
+ int[] previous = new int[length + 1];
+ int[] current = new int[length + 1];
+ for (int j = 0; j <= length; j++) {
+ previous[j] = length - j;
+ }
+ for (int i = to1 - 1; i >= from1; i--) {
+ current[length] = to1 - i;
+ for (int j = length - 1; j >= 0; j--) {
+ current[j] = min(current[j + 1] + 1,
+ previous[j] + 1,
+ previous[j + 1] + (seq1.charAt(i) == seq2.charAt(from2 + j) ? 0 : 1));
+ }
+ int[] swap = previous;
+ previous = current;
+ current = swap;
+ }
+ return previous;
}
private static int min(int a, int b, int c) {
return Math.min(a, Math.min(b, c));
}
-}
+ private static int indexOf(CharSequence seq, int from, int to, char c) {
+ for (int i = from; i < to; i++) {
+ if (seq.charAt(i) == c) return i;
+ }
+ return -1;
+ }
+ private static void addOperation(List operations, EditOperation.Kind kind, int length) {
+ if (length == 0) return;
+
+ if (!operations.isEmpty() && operations.get(operations.size() - 1).getKind() == kind) {
+ operations.get(operations.size() - 1).incLength(length);
+ } else {
+ operations.add(new EditOperation(kind, length));
+ }
+ }
+
+ /**
+ * Pairs up adjacent deletions and insertions into {@link EditOperation.Kind#SUBSTITUTE} runs,
+ * which is how the previous matrix-based implementation rendered fully-differing regions.
+ */
+ private static List normalize(List operations) {
+ List result = new ArrayList<>(operations.size());
+ for (int i = 0; i < operations.size(); i++) {
+ EditOperation operation = operations.get(i);
+ EditOperation.Kind kind = operation.getKind();
+ boolean deletionFirst = kind == DELETE;
+ if ((deletionFirst || kind == INSERT) && i + 1 < operations.size()
+ && operations.get(i + 1).getKind() == (deletionFirst ? INSERT : DELETE)) {
+ EditOperation deletion = deletionFirst ? operation : operations.get(++i);
+ EditOperation insertion = deletionFirst ? operations.get(++i) : operation;
+ int substituted = Math.min(deletion.getLength(), insertion.getLength());
+ addOperation(result, DELETE, deletion.getLength() - substituted);
+ addOperation(result, SUBSTITUTE, substituted);
+ addOperation(result, INSERT, insertion.getLength() - substituted);
+ } else {
+ addOperation(result, kind, operation.getLength());
+ }
+ }
+ return result;
+ }
+}
diff --git a/spock-specs/src/test/groovy/org/spockframework/runtime/condition/EditDistanceCrossCheckSpec.groovy b/spock-specs/src/test/groovy/org/spockframework/runtime/condition/EditDistanceCrossCheckSpec.groovy
new file mode 100644
index 0000000000..da1356354d
--- /dev/null
+++ b/spock-specs/src/test/groovy/org/spockframework/runtime/condition/EditDistanceCrossCheckSpec.groovy
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.spockframework.runtime.condition
+
+import spock.lang.Specification
+
+import java.util.Random
+
+/**
+ * Cross-checks {@link EditDistance} against a straightforward dynamic-programming Levenshtein
+ * implementation on many random inputs. The binary alphabet maximizes ties between equally
+ * expensive alignments, i.e. the degenerate cases a divide-and-conquer diff algorithm has to
+ * survive.
+ */
+class EditDistanceCrossCheckSpec extends Specification {
+ private static final char[] ALPHABET = 'ab'.toCharArray()
+
+ def "path cost matches Levenshtein distance for random inputs (seed #@seed)"() {
+ given:
+ Random random = new Random(seed)
+
+ expect: "for every pair, path cost and rendered alignment match the reference distance"
+ 200.times {
+ String str1 = randomString(random, random.nextInt(13))
+ String str2 = randomString(random, random.nextInt(13))
+ def dist = new EditDistance(str1, str2)
+ int expectedDistance = levenshtein(str1, str2)
+
+ assert pathCost(dist.calculatePath()) == expectedDistance
+ assert dist.getDistance() == expectedDistance
+ assert rendersToOriginalStrings(str1, str2, dist.calculatePath())
+ }
+
+ where:
+ seed << (0..4)
+ }
+
+ def "structured edge cases match Levenshtein distance"() {
+ given:
+ List> cases = [
+ ["", ""], ["", "a"], ["a", ""], ["a", "a"], ["a", "b"],
+ ["ab", "ba"], ["abc", "cba"], ["aa", "aa"], ["aa", "aaa"], ["aaa", "a"],
+ ["abab", "baba"], ["aaaa", "aa"], ["ab", "baab"], ["aabb", "bbaa"],
+ ["aaaaaaaaaa", "bbbbbbbbbb"], ["aaaaaaaaaa", "aaaaabaaaa"],
+ ["ab" * 10, "ba" * 10], ["mississippi", "mispissippi"]
+ ]
+
+ expect:
+ cases.each { pair ->
+ def dist = new EditDistance(pair[0], pair[1])
+ assert dist.getDistance() == levenshtein(pair[0], pair[1])
+ assert pathCost(dist.calculatePath()) == levenshtein(pair[0], pair[1])
+ }
+ }
+
+ private static int pathCost(List path) {
+ path.sum(0) { it.kind == EditOperation.Kind.SKIP ? 0 : it.length }
+ }
+
+ private static boolean rendersToOriginalStrings(String str1, String str2, List path) {
+ def renderer = new EditPathRenderer()
+ String[] lines = renderer.render(str1, str2, path).split('\n', -1)
+ stripRendering(lines[0]) == str1 && stripRendering(lines[1]) == str2
+ }
+
+ private static String stripRendering(String line) {
+ line.replace('-', '').replace('(', '').replace(')', '').replace('~', '')
+ }
+
+ private static String randomString(Random random, int length) {
+ StringBuilder result = new StringBuilder(length)
+ length.times { result.append(ALPHABET[random.nextInt(ALPHABET.length)]) }
+ result.toString()
+ }
+
+ private static int levenshtein(String str1, String str2) {
+ int[] prev = new int[str2.length() + 1]
+ int[] cur = new int[str2.length() + 1]
+ for (int j = 0; j <= str2.length(); j++) {
+ prev[j] = j
+ }
+ for (int i = 1; i <= str1.length(); i++) {
+ cur[0] = i
+ for (int j = 1; j <= str2.length(); j++) {
+ cur[j] = Math.min(prev[j] + 1,
+ Math.min(cur[j - 1] + 1, prev[j - 1] + (str1.charAt(i - 1) == str2.charAt(j - 1) ? 0 : 1)))
+ }
+ int[] swap = prev
+ prev = cur
+ cur = swap
+ }
+ prev[str2.length()]
+ }
+}
diff --git a/spock-specs/src/test/groovy/org/spockframework/runtime/condition/EditDistanceLargeInputSpec.groovy b/spock-specs/src/test/groovy/org/spockframework/runtime/condition/EditDistanceLargeInputSpec.groovy
new file mode 100644
index 0000000000..3d1880e94b
--- /dev/null
+++ b/spock-specs/src/test/groovy/org/spockframework/runtime/condition/EditDistanceLargeInputSpec.groovy
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.spockframework.runtime.condition
+
+import spock.lang.Shared
+import spock.lang.Specification
+
+import java.util.Random
+
+import static org.spockframework.runtime.condition.EditOperation.Kind.SUBSTITUTE
+
+/**
+ * Verifies that {@link EditDistance} handles large inputs, which requires an algorithm with a better
+ * time and space complexity than the dense-matrix Levenshtein it replaces: for two 10,000-character
+ * near-identical strings the old implementation allocated more than 400 MB for the distance matrix
+ * and could die with {@link OutOfMemoryError} before returning anything.
+ */
+class EditDistanceLargeInputSpec extends Specification {
+ private static final int LENGTH = 10_000
+ // 'z' is excluded here so that substituting it below always changes exactly one character
+ private static final char[] ALPHABET = 'abcdefghijklmnopqrstuv'.toCharArray()
+
+ @Shared
+ String large1 = randomString(LENGTH)
+
+ @Shared
+ String large2 = substituteOneChar(large1)
+
+ def "distance between large near-identical strings is computed"() {
+ expect:
+ new EditDistance(large1, large2).getDistance() == 1
+ }
+
+ def "edit path between large near-identical strings has exactly one substitution"() {
+ expect:
+ new EditDistance(large1, large2).calculatePath() == [
+ new EditOperation(EditOperation.Kind.SKIP, LENGTH.intdiv(2)),
+ new EditOperation(SUBSTITUTE, 1),
+ new EditOperation(EditOperation.Kind.SKIP, LENGTH.intdiv(2) - 1)
+ ]
+ }
+
+ def "rendering the edit path of large near-identical strings works"() {
+ given:
+ def rendered = new EditPathRenderer().render(large1, large2, new EditDistance(large1, large2).calculatePath())
+
+ expect:
+ rendered.split('\n').length == 2
+ // the one differing region is delimited by '(' and ')' on each of the two lines
+ rendered.count('(') == 2
+ rendered.contains("(z)")
+ }
+
+ def "completely different large strings are handled"() {
+ given:
+ String s1 = 'a' * LENGTH
+ String s2 = randomString(LENGTH)
+
+ when:
+ def dist = new EditDistance(s1, s2)
+
+ then:
+ dist.getDistance() >= LENGTH / 2
+
+ and: "the path cost equals the distance"
+ dist.calculatePath().sum(0, { op -> op.getKind() == EditOperation.Kind.SKIP ? 0 : op.getLength() }) ==
+ dist.getDistance()
+ }
+
+ private static String randomString(int length) {
+ Random random = new Random(0)
+ StringBuilder result = new StringBuilder(length)
+ length.times { result.append(ALPHABET[random.nextInt(ALPHABET.length)]) }
+ result.toString()
+ }
+
+ private static String substituteOneChar(String str) {
+ char[] chars = str.toCharArray()
+ chars[chars.length.intdiv(2)] = 'z' // not in ALPHABET, so exactly one character changes
+ new String(chars)
+ }
+}
diff --git a/spock-specs/src/test/groovy/org/spockframework/runtime/condition/EditDistanceSpec.groovy b/spock-specs/src/test/groovy/org/spockframework/runtime/condition/EditDistanceSpec.groovy
index c36b216593..3239e7d9a8 100644
--- a/spock-specs/src/test/groovy/org/spockframework/runtime/condition/EditDistanceSpec.groovy
+++ b/spock-specs/src/test/groovy/org/spockframework/runtime/condition/EditDistanceSpec.groovy
@@ -26,54 +26,6 @@ import static org.spockframework.runtime.condition.EditOperation.Kind.*
class EditDistanceSpec extends Specification {
@Shared chars = ('a'..'z') + ('A'..'Z') + ('0'..'9') + [' '] * 10
- def "matrix for 'sitting' and 'kitten'"() {
- def matrix = new EditDistance("sitting", "kitten").matrix
-
- expect:
- matrix.size() == 8
- matrix[0] == [0, 1, 2, 3, 4, 5, 6]
- matrix[1] == [1, 1, 2, 3, 4, 5, 6]
- matrix[2] == [2, 2, 1, 2, 3, 4, 5]
- matrix[3] == [3, 3, 2, 1, 2, 3, 4]
- matrix[4] == [4, 4, 3, 2, 1, 2, 3]
- matrix[5] == [5, 5, 4, 3, 2, 2, 3]
- matrix[6] == [6, 6, 5, 4, 3, 3, 2]
- matrix[7] == [7, 7, 6, 5, 4, 4, 3]
- }
-
- def "matrix for 'Sunday' and 'Saturday'"() {
- def matrix = new EditDistance("Sunday", "Saturday").matrix
-
- expect:
- matrix.size() == 7
- matrix[0] == [0, 1, 2, 3, 4, 5, 6, 7, 8]
- matrix[1] == [1, 0, 1, 2, 3, 4, 5, 6, 7]
- matrix[2] == [2, 1, 1, 2, 2, 3, 4, 5, 6]
- matrix[3] == [3, 2, 2, 2, 3, 3, 4, 5, 6]
- matrix[4] == [4, 3, 3, 3, 3, 4, 3, 4, 5]
- matrix[5] == [5, 4, 3, 4, 4, 4, 4, 3, 4]
- matrix[6] == [6, 5, 4, 4, 5, 5, 5, 4, 3]
- }
-
- def "matrix for 'levenshtein' and 'meilenstein'"() {
- def matrix = new EditDistance("levenshtein", "meilenstein").matrix
-
- expect:
- matrix.size() == 12
- matrix[0] == [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
- matrix[1] == [ 1, 1, 2, 3, 3, 4, 5, 6, 7, 8, 9, 10]
- matrix[2] == [ 2, 2, 1, 2, 3, 3, 4, 5, 6, 7, 8, 9]
- matrix[3] == [ 3, 3, 2, 2, 3, 4, 4, 5, 6, 7, 8, 9]
- matrix[4] == [ 4, 4, 3, 3, 3, 3, 4, 5, 6, 6, 7, 8]
- matrix[5] == [ 5, 5, 4, 4, 4, 4, 3, 4, 5, 6, 7, 7]
- matrix[6] == [ 6, 6, 5, 5, 5, 5, 4, 3, 4, 5, 6, 7]
- matrix[7] == [ 7, 7, 6, 6, 6, 6, 5, 4, 4, 5, 6, 7]
- matrix[8] == [ 8, 8, 7, 7, 7, 7, 6, 5, 4, 5, 6, 7]
- matrix[9] == [ 9, 9, 8, 8, 8, 7, 7, 6, 5, 4, 5, 6]
- matrix[10] == [10, 10, 9, 8, 9, 8, 8, 7, 6, 5, 4, 5]
- matrix[11] == [11, 11, 10, 9, 9, 9, 8, 8, 7, 6, 5, 4]
- }
-
def "path from 'sitting' to 'kitten'"() {
def path = new EditDistance("sitting", "kitten").calculatePath()
@@ -135,6 +87,19 @@ class EditDistanceSpec extends Specification {
str2 = editedString(str1)
}
+ def "calculated paths can be modified independently"() {
+ given:
+ def dist = new EditDistance("sitting", "kitten")
+ def expected = dist.calculatePath()
+
+ when:
+ dist.calculatePath().clear()
+ dist.calculatePath().first().incLength(1)
+
+ then:
+ dist.calculatePath() == expected
+ }
+
def computeDistance(List operations) {
operations.sum 0, { it.getKind() == EditOperation.Kind.SKIP ? 0 : it.getLength() }
}
diff --git a/spock-specs/src/test/groovy/org/spockframework/runtime/condition/EditPathRendererSpec.groovy b/spock-specs/src/test/groovy/org/spockframework/runtime/condition/EditPathRendererSpec.groovy
index 0413045714..f2a6eee623 100644
--- a/spock-specs/src/test/groovy/org/spockframework/runtime/condition/EditPathRendererSpec.groovy
+++ b/spock-specs/src/test/groovy/org/spockframework/runtime/condition/EditPathRendererSpec.groovy
@@ -67,10 +67,13 @@ class EditPathRendererSpec extends Specification {
expect:
renderer.render(str, seq, dist.calculatePath()) == "$out1\n$out2"
+ // the last case pins one specific alignment; several alignments with the same minimal
+ // edit distance exist and the current algorithm pairs up "row"/"-ur" instead of
+ // "-"/"u" and "ow"/"--"
where:
str = "the quick brown"
seq << ["${"the"} quark ${"burn"}", new StringBuilder("the quark burn"), CharBuffer.wrap("the quark burn".toCharArray())]
- out1 = "the qu(ic)k b(-)r(ow)n"
- out2 = "the qu(ar)k b(u)r(--)n"
+ out1 = "the qu(ic)k b(row)n"
+ out2 = "the qu(ar)k b(-ur)n"
}
}
diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/condition/StringComparisonRendering.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/condition/StringComparisonRendering.groovy
index 28750ca1db..30fafb3df7 100644
--- a/spock-specs/src/test/groovy/org/spockframework/smoke/condition/StringComparisonRendering.groovy
+++ b/spock-specs/src/test/groovy/org/spockframework/smoke/condition/StringComparisonRendering.groovy
@@ -93,7 +93,7 @@ null == "foo"
renderedConditionContains({
assert a == b
},
- "1 difference (90% similarity) (comparing subset start: 0, end1: 11, end2: 11)",
+ "1 difference (99% similarity) (comparing subset start: 0, end1: 251, end2: 251)",
"(a)aaaaaaaaaa",
"(b)aaaaaaaaaa"
)
@@ -110,7 +110,7 @@ null == "foo"
renderedConditionContains({
assert a == b
},
- "1 difference (95% similarity) (comparing subset start: 12789, end1: 12811, end2: 12811)",
+ "1 difference (99% similarity) (comparing subset start: 261893, end1: 262395, end2: 262395)",
"aaaaaaaaaaa(a)aaaaaaaaaa",
"aaaaaaaaaaa(b)aaaaaaaaaa"
)
@@ -127,7 +127,7 @@ null == "foo"
renderedConditionContains({
assert a == b
},
- "1 difference (91% similarity) (comparing subset start: 25588, end1: 25600, end2: 25600)",
+ "1 difference (99% similarity) (comparing subset start: 524036, end1: 524288, end2: 524288)",
"aaaaaaaaaaa(a)",
"aaaaaaaaaaa(b)")
}
@@ -208,8 +208,8 @@ null == "foo"
|
false
4 differences (20% similarity)
- (foo)\\n(-~)
- (bar)\\n(\\n)
+ (foo-~)\\n
+ (bar\\n)\\n
''', {
assert """foo
""" == """bar
@@ -290,8 +290,8 @@ $b
|
false
7 differences (95% similarity)
- Lorem ipsum\\n(\\n)Lorem ipsum (-)dolor sit amet, (-)consetetur sadipscing elitr, sed (-)diam nonumy eirmod tempor (-)invidunt ut labore et (-)dolore magna aliquyam erat, sed diam voluptua.(-~)
- Lorem ipsum\\n(-~)Lorem ipsum ( )dolor sit amet, ( )consetetur sadipscing elitr, sed ( )diam nonumy eirmod tempor ( )invidunt ut labore et ( )dolore magna aliquyam erat, sed diam voluptua.(\\n)
+ Lorem ipsum\\n(\\n)Lorem ipsum(-) dolor sit amet,(-) consetetur sadipscing elitr, sed(-) diam nonumy eirmod tempor (-)invidunt ut labore et(-) dolore magna aliquyam erat, sed diam voluptua.(-~)
+ Lorem ipsum\\n(-~)Lorem ipsum( ) dolor sit amet,( ) consetetur sadipscing elitr, sed( ) diam nonumy eirmod tempor ( )invidunt ut labore et( ) dolore magna aliquyam erat, sed diam voluptua.(\\n)
''', {
assert """\
Lorem ipsum
@@ -336,4 +336,38 @@ dolore magna aliquyam erat, sed diam voluptua.\
}
return sb
}
+
+ def "large near-identical strings render a full inline diff instead of giving up"() {
+ given:
+ String a = largeStringBuilder("the quick brown fox ", 6000)
+ String b = a[0..<-1] + "z"
+
+ expect:
+ renderedConditionContains({
+ assert a == b
+ }, "1 difference (99% similarity)", "(z)")
+ }
+
+ def "huge completely-different strings still fall back to the no-diff message"() {
+ given:
+ int length = 4 * Math.sqrt(FailedStringComparisonRenderer.MAX_EDIT_DISTANCE_MEMORY)
+ String a = largeStringBuilder("a", length)
+ String b = largeStringBuilder("b", length)
+
+ expect:
+ renderedConditionContains({
+ assert a == b
+ }, "false", "Strings too large to calculate edit distance.")
+ }
+
+ def "gstring comparison of large strings renders a real diff"() {
+ given:
+ String a = largeStringBuilder("hello world ", 6000)
+ GString b = "j${5}${a[1..-1]}"
+
+ expect:
+ renderedConditionContains({
+ assert a == b
+ }, "2 differences (99% similarity)", "(j5)")
+ }
}
diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/mock/TooFewInvocations.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/TooFewInvocations.groovy
index d64a64fb0c..42f324a123 100644
--- a/spock-specs/src/test/groovy/org/spockframework/smoke/mock/TooFewInvocations.groovy
+++ b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/TooFewInvocations.groovy
@@ -66,7 +66,7 @@ methodName == "add"
remove false
6 differences (0% similarity)
(remove)
- (add---)
+ (---add)
1 * list2.add(2)
instance == target
@@ -93,7 +93,7 @@ methodName == "add"
size false
4 differences (0% similarity)
(size)
- (add-)
+ (-add)
''')
@@ -163,7 +163,7 @@ methodName == "add"
remove false
6 differences (0% similarity)
(remove)
- (add---)
+ (---add)
One or more arguments(s) didn't match:
0: argument == expected