Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/java/groovy/lang/Tuple.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public Tuple<E> subTuple(int fromIndex, int toIndex) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Tuple that)) return false;
if (!(o instanceof List<?> that)) return false;

int size = size();
if (size != that.size()) return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,14 @@ public static boolean compareEqual(Object left, Object right) {
if (left == right) return true;
if (left == null) return right instanceof NullObject;
if (right == null) return left instanceof NullObject;
// An object that is both Comparable and a List (e.g. Tuple) must use
// list equality here, consistent with how a plain (non-Comparable)
// List is compared, rather than taking the Comparable/compareTo
// short-circuit below; otherwise `tuple == [..]` would be asymmetric
// with `[..] == tuple` and inconsistent with Tuple.equals(List).
if (left instanceof List && right instanceof List) {
return DefaultGroovyMethods.equals((List) left, (List) right);
}
if (left instanceof Comparable) {
return compareToWithEqualityCheck(left, right, true) == 0;
}
Expand Down
37 changes: 37 additions & 0 deletions src/test/groovy/groovy/lang/TupleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import groovy.test.GroovyTestCase;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
Expand Down Expand Up @@ -293,4 +294,40 @@ public void testGroovyStyleAccessor() {
assert false : e.getMessage();
}
}

// Regression coverage for making Tuple equality symmetric with List
// (broaden Tuple.equals from `instanceof Tuple` to `instanceof List`,
// keeping Groovy's number-aware element comparison consistent with
// DefaultGroovyMethods.equals(List, List)).
public void testTupleEqualsBehavesLikeList() throws Exception {
Tuple2<Integer, Integer> t = new Tuple2<>(1, 2);
List<Integer> list = Arrays.asList(1, 2);

// .equals() symmetric in both operand orders
assertEquals("Tuple2.equals(List)", t, list);
assertEquals("List.equals(Tuple2)", list, t);
// hashCode is already the List formula, so it stays consistent
assertEquals("hashCode parity with List", t.hashCode(), list.hashCode());

// Groovy == in both operand orders
assertScript(
"def t = new Tuple2(1, 2)\n" +
"assert t == [1, 2]\n" +
"assert [1, 2] == t\n" +
// number-aware elements, consistent with equals(List, List)
"assert t == [1L, 2L]\n" +
"assert [1L, 2L] == t\n");

// number-aware elements via .equals(), consistent with equals(List, List)
assertEquals("number-aware elements", t, Arrays.asList(1L, 2L));

// unchanged: Tuple vs Tuple
assertEquals("Tuple vs Tuple", new Tuple2<>(1, 2), new Tuple2<>(1, 2));
// empty tuple vs empty list
assertEquals("empty Tuple vs empty List", tuple(), Arrays.asList());

// guardrails: differing size / content are not equal
assertNotEquals(t, Arrays.asList(1, 2, 3), "size mismatch");
assertNotEquals(t, Arrays.asList(1, 9), "content mismatch");
}
}
Loading