Skip to content

Commit

Permalink
Introduce rootCause() condition for use with the EngineTestKit
Browse files Browse the repository at this point in the history
Closes #3839
  • Loading branch information
sbrannen committed Jun 1, 2024
1 parent ca61b12 commit 9b7957a
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ repository on GitHub.
[[release-notes-5.11.0-M3-junit-platform-new-features-and-improvements]]
==== New Features and Improvements

* ❓
* New `rootCause()` condition in `TestExecutionResultConditions` that matches if an
exception's _root_ cause matches all supplied conditions, for use with the
`EngineTestKit`.


[[release-notes-5.11.0-M3-junit-jupiter]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import static java.util.function.Predicate.isEqual;
import static java.util.stream.Collectors.toList;
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.apiguardian.api.API.Status.MAINTAINED;
import static org.junit.platform.commons.util.FunctionUtils.where;

Expand All @@ -22,6 +23,7 @@
import org.apiguardian.api.API;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.Condition;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.TestExecutionResult.Status;

Expand Down Expand Up @@ -69,6 +71,9 @@ public static Condition<TestExecutionResult> throwable(Condition<Throwable>... c
* Create a new {@link Condition} that matches if and only if a
* {@link Throwable}'s {@linkplain Throwable#getCause() cause} matches all
* supplied conditions.
*
* @see #rootCause(Condition...)
* @see #suppressed(int, Condition...)
*/
@SafeVarargs
@SuppressWarnings("varargs")
Expand All @@ -80,10 +85,33 @@ public static Condition<Throwable> cause(Condition<Throwable>... conditions) {
return Assertions.allOf(list);
}

/**
* Create a new {@link Condition} that matches if and only if a
* {@link Throwable}'s root {@linkplain Throwable#getCause() cause} matches
* all supplied conditions.
*
* @since 1.11
* @see #cause(Condition...)
* @see #suppressed(int, Condition...)
*/
@API(status = EXPERIMENTAL, since = "1.11")
@SafeVarargs
@SuppressWarnings("varargs")
public static Condition<Throwable> rootCause(Condition<Throwable>... conditions) {
List<Condition<Throwable>> list = Arrays.stream(conditions)//
.map(TestExecutionResultConditions::rootCause)//
.collect(toList());

return Assertions.allOf(list);
}

/**
* Create a new {@link Condition} that matches if and only if a
* {@link Throwable}'s {@linkplain Throwable#getSuppressed() suppressed
* throwable} at the supplied index matches all supplied conditions.
*
* @see #cause(Condition...)
* @see #rootCause(Condition...)
*/
@SafeVarargs
@SuppressWarnings("varargs")
Expand Down Expand Up @@ -135,6 +163,27 @@ private static Condition<Throwable> cause(Condition<Throwable> condition) {
condition);
}

private static Condition<Throwable> rootCause(Condition<Throwable> condition) {
Predicate<Throwable> predicate = throwable -> {
Preconditions.notNull(throwable, "Throwable must not be null");
Throwable cause = Preconditions.notNull(throwable.getCause(), "Throwable does not have a cause");
return condition.matches(getRootCause(cause));
};
return new Condition<>(predicate, "throwable root cause matches %s", condition);
}

/**
* Get the root cause of the supplied {@link Throwable}, or the supplied
* {@link Throwable} if it has no cause.
*/
private static Throwable getRootCause(Throwable throwable) {
Throwable cause = throwable.getCause();
if (cause == null) {
return throwable;
}
return getRootCause(cause);
}

private static Condition<Throwable> suppressed(int index, Condition<Throwable> condition) {
return new Condition<>(
throwable -> throwable.getSuppressed().length > index
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.platform.testkit.engine;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import java.util.function.Predicate;

import org.assertj.core.api.Condition;
import org.junit.jupiter.api.Test;
import org.junit.platform.commons.PreconditionViolationException;

/**
* Tests for {@link TestExecutionResultConditions}.
*
* @since 1.11
*/
class TestExecutionResultConditionsTests {

private static final String EXPECTED = "expected";

private static final String UNEXPECTED = "unexpected";

private static final Predicate<Throwable> messageEqualsExpected = //
throwable -> EXPECTED.equals(throwable.getMessage());

private static final Condition<Throwable> expectedMessageCondition = //
new Condition<>(messageEqualsExpected, "message matches %s", EXPECTED);

private static final Condition<Throwable> rootCauseCondition = //
TestExecutionResultConditions.rootCause(expectedMessageCondition);

@Test
void rootCauseFailsForNullThrowable() {
assertThatExceptionOfType(PreconditionViolationException.class)//
.isThrownBy(() -> rootCauseCondition.matches(null))//
.withMessage("Throwable must not be null");
}

@Test
void rootCauseFailsForThrowableWithoutCause() {
Throwable throwable = new Throwable();

assertThatExceptionOfType(PreconditionViolationException.class)//
.isThrownBy(() -> rootCauseCondition.matches(throwable))//
.withMessage("Throwable does not have a cause");
}

@Test
void rootCauseMatchesForDirectCauseWithExpectedMessage() {
RuntimeException cause = new RuntimeException(EXPECTED);
Throwable throwable = new Throwable(cause);

assertThat(rootCauseCondition.matches(throwable)).isTrue();
}

@Test
void rootCauseDoesNotMatchForDirectCauseWithDifferentMessage() {
RuntimeException cause = new RuntimeException(UNEXPECTED);
Throwable throwable = new Throwable(cause);

assertThat(rootCauseCondition.matches(throwable)).isFalse();
}

@Test
void rootCauseMatchesForRootCauseWithExpectedMessage() {
RuntimeException rootCause = new RuntimeException(EXPECTED);
RuntimeException intermediateCause = new RuntimeException("intermediate cause", rootCause);
Throwable throwable = new Throwable(intermediateCause);

assertThat(rootCauseCondition.matches(throwable)).isTrue();
}

@Test
void rootCauseDoesNotMatchForRootCauseWithDifferentMessage() {
RuntimeException rootCause = new RuntimeException(UNEXPECTED);
RuntimeException intermediateCause = new RuntimeException("intermediate cause", rootCause);
Throwable throwable = new Throwable(intermediateCause);

assertThat(rootCauseCondition.matches(throwable)).isFalse();
}

}

2 comments on commit 9b7957a

@marcphilipp
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@sbrannen
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks 🙇

Please sign in to comment.