Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assertions.assertIterableEquals overflows the stack #2157

Closed
1 task done
sormuras opened this issue Jan 20, 2020 · 8 comments · Fixed by #2158
Closed
1 task done

Assertions.assertIterableEquals overflows the stack #2157

sormuras opened this issue Jan 20, 2020 · 8 comments · Fixed by #2158

Comments

@sormuras
Copy link
Member

sormuras commented Jan 20, 2020

Steps to reproduce

The following code:

var expected = List.of(Path.of("1"));
var actual = List.of(Path.of("1"));
System.out.println(expected);
System.out.println(actual);
assertIterableEquals(expected, actual);

Yields:

[1]
[1]
java.lang.StackOverflowError
	at java.base/java.util.ArrayList.grow(ArrayList.java:243)
	at java.base/java.util.ArrayList.add(ArrayList.java:453)
	at java.base/java.util.ArrayList.add(ArrayList.java:466)
	at java.base/sun.nio.fs.WindowsPath.initOffsets(WindowsPath.java:649)
	at java.base/sun.nio.fs.WindowsPath.getNameCount(WindowsPath.java:660)
	at java.base/java.nio.file.Path$1.hasNext(Path.java:923)
	at org.junit.jupiter.api.AssertIterableEquals.assertIterableEquals(AssertIterableEquals.java:62)
	at org.junit.jupiter.api.AssertIterableEquals.assertIterableElementsEqual(AssertIterableEquals.java:82)
	at org.junit.jupiter.api.AssertIterableEquals.assertIterableEquals(AssertIterableEquals.java:72)
	at org.junit.jupiter.api.AssertIterableEquals.assertIterableElementsEqual(AssertIterableEquals.java:82)
	at org.junit.jupiter.api.AssertIterableEquals.assertIterableEquals(AssertIterableEquals.java:72)
	[...]

Deliverables

  • Provide fix and test for comparing Iterable<? extends Iterable<?>> instances.
@sormuras sormuras added this to the 5.6 GA milestone Jan 20, 2020
@sormuras sormuras self-assigned this Jan 20, 2020
sormuras added a commit that referenced this issue Jan 20, 2020
Prior to this commit an iterable of iterables (here instances
of java.nio.file.Path) passed to method `assertIterableEquals`
in class `Assertions` of the JUnit Jupiter API yielded a
`StackOverflowError`.

This commit prevent the eternal while-loop by leveraging the
`equals()`-implementation of the non-null expected element.

Fixes #2157
sormuras added a commit that referenced this issue Jan 20, 2020
Prior to this commit an iterable of iterables (here instances
of java.nio.file.Path) passed to method `assertIterableEquals`
in class `Assertions` of the JUnit Jupiter API yielded a
`StackOverflowError`.

This commit prevent the eternal while-loop by leveraging the
`equals()`-implementation of the non-null expected element.

Fixes #2157
sormuras added a commit that referenced this issue Jan 20, 2020
@jbduncan
Copy link
Contributor

Oh wow, that's a really insidious bug! Thank you very much for finding and fixing it @sormuras. 👏

@jbduncan
Copy link
Contributor

jbduncan commented Jan 21, 2020

I wonder if this bug could have been caught earlier with mutation testing. 🤔

@sormuras
Copy link
Member Author

I wonder if this bug could have been caught earlier with mutation testing. 🤔

Interesting thought. Perhaps also a candidate for property-based testing ... @jlink?

@jlink
Copy link
Contributor

jlink commented Jan 23, 2020

@sormuras Can you point me to the test? I'll make it a simple property and see if it catches the bug.

@sormuras
Copy link
Member Author

sormuras commented Jan 23, 2020

@Test
// https://github.com/junit-team/junit5/issues/2157
void assertIterableEqualsWithListOfPath() {
var expected = listOf(Path.of("1"));
var actual = listOf(Path.of("1"));
assertDoesNotThrow(() -> assertIterableEquals(expected, actual));
}

And the one below -- which is expected to fail. With and with the fix.

Edit: Revert this if-clause

https://github.com/junit-team/junit5/blob/master/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertIterableEquals.java#L67-L69

back to

			if (expectedElement == actualElement) {
				continue;
			}

This will show the StackOverflowError again.

@jlink
Copy link
Contributor

jlink commented Jan 23, 2020

I could detect the bug with the following rather straightforward property:

@Property
<T> void assertingEqualityOfTwoListsShouldNeverThrowException(@ForAll List<T> expected, @ForAll List<T> actual) {
	try {
		assertIterableEquals(expected, actual);
	} catch (AssertionError ignore) {}
}

I had to register a default arbitrary provider for java.nio.Path because there is currently none in jqwik. But that could be fixable, of course. I'm wondering, however, what's so special about Path.

There's an additional drawback: Shrinking does not produce something meaningful in this setup. That means that the falsifying sample is very large and hard to identify.

@marcphilipp
Copy link
Member

The special thing about path is that its iterator returns paths whose iterator will return instances that are equal to themselves. For instance, the following Groovy snippet

import java.nio.file.Paths
def path = Paths.get("bar");
println path.iterator().next().equals(path)
println path.iterator().next().is(path)

prints

true
false

The code did not account for this kind of recursiveness.

@jlink
Copy link
Contributor

jlink commented Jan 25, 2020

That's some strange kind of behaviour indeed. Nothing in Iterator's contract that would exclude that, though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants