diff --git a/build.gradle b/build.gradle index ca9109cb..ca72c721 100644 --- a/build.gradle +++ b/build.gradle @@ -129,6 +129,39 @@ publishing { 'Hamcrest Library', 'A library of Hamcrest matchers - deprecated, please use "hamcrest" instead') } + + def hamcrestJUnit5TestsProject = project(':hamcrest-junit5-tests') + hamcrestJUnit5Tests(MavenPublication) { + from hamcrestJUnit5TestsProject.components.java + artifactId hamcrestJUnit5TestsProject.name + artifact hamcrestJUnit5TestsProject.sourcesJar + artifact hamcrestJUnit5TestsProject.javadocJar + pom pomConfigurationFor( + 'Hamcrest JUnit 5 Tests', + 'A test suite for Hamcrest assumptions using JUnit 5') + } + + def hamcrestJUnit4TestsProject = project(':hamcrest-junit4-tests') + hamcrestJUnit4Tests(MavenPublication) { + from hamcrestJUnit4TestsProject.components.java + artifactId hamcrestJUnit4TestsProject.name + artifact hamcrestJUnit4TestsProject.sourcesJar + artifact hamcrestJUnit4TestsProject.javadocJar + pom pomConfigurationFor( + 'Hamcrest JUnit4 Tests', + 'A test suite for Hamcrest assumptions using JUnit 4') + } + + def hamcrestJUnit4JUnit5TestsProject = project(':hamcrest-junit4-junit5-tests') + hamcrestJUnit4JUnit5Tests(MavenPublication) { + from hamcrestJUnit4JUnit5TestsProject.components.java + artifactId hamcrestJUnit4JUnit5TestsProject.name + artifact hamcrestJUnit4JUnit5TestsProject.sourcesJar + artifact hamcrestJUnit4JUnit5TestsProject.javadocJar + pom pomConfigurationFor( + 'Hamcrest Hybrid JUnit 4/JUnit 5 Tests', + 'A test suite for Hamcrest assumptions using hybrid JUnit 4/JUnit 5') + } } repositories { if (publishToOssrh) { @@ -150,4 +183,7 @@ signing { sign publishing.publications.hamcrest sign publishing.publications.hamcrestCore sign publishing.publications.hamcrestLibrary + sign publishing.publications.hamcrestJUnit5Tests + sign publishing.publications.hamcrestJUnit4Tests + sign publishing.publications.hamcrestJUnit4JUnit5Tests } diff --git a/hamcrest-junit4-junit5-tests/hamcrest-junit4-junit5-tests.gradle b/hamcrest-junit4-junit5-tests/hamcrest-junit4-junit5-tests.gradle new file mode 100644 index 00000000..afc5e845 --- /dev/null +++ b/hamcrest-junit4-junit5-tests/hamcrest-junit4-junit5-tests.gradle @@ -0,0 +1,20 @@ +plugins { + id 'java' +} + +group 'org.hamcrest' +version '2.3-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation project(':hamcrest') + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testImplementation 'org.junit.vintage:junit-vintage-engine:5.8.2' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/hamcrest-junit4-junit5-tests/src/test/java/org/hamcrest/JUnit4MatcherAssumeTest.java b/hamcrest-junit4-junit5-tests/src/test/java/org/hamcrest/JUnit4MatcherAssumeTest.java new file mode 100644 index 00000000..4bc67403 --- /dev/null +++ b/hamcrest-junit4-junit5-tests/src/test/java/org/hamcrest/JUnit4MatcherAssumeTest.java @@ -0,0 +1,54 @@ +package org.hamcrest; + +import org.junit.Test; +import org.junit.AssumptionViolatedException; +import org.opentest4j.TestAbortedException; + +import static org.hamcrest.MatcherAssume.assumeThat; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * Tests compatibility with JUnit 4 and JUnit 5 on the classpath. + * The equivalent test with only JUnit 4 on the classpath is in another module. + */ +public class JUnit4MatcherAssumeTest { + + @Test public void + assumptionFailsWithMessage() { + try { + assumeThat("Custom assumption", "a", startsWith("abc")); + fail("should have failed"); + } + catch (AssumptionViolatedException e) { + assertEquals("Custom assumption: got: \"a\", expected: a string starting with \"abc\"", e.getMessage()); + } + catch (TestAbortedException e) { + throw new AssertionError("Illegal JUnit 5 assumption", e); + } + } + + @Test public void + assumptionFailsWithDefaultMessage() { + try { + assumeThat("a", startsWith("abc")); + fail("should have failed"); + } + catch (AssumptionViolatedException e) { + assertEquals(": got: \"a\", expected: a string starting with \"abc\"", e.getMessage()); + } + catch (TestAbortedException e) { + throw new AssertionError("Illegal JUnit 5 assumption", e); + } + } + + @Test public void + assumptionSucceeds() { + try { + assumeThat("xyz", startsWith("xy")); + } catch (TestAbortedException e) { + throw new AssertionError("Illegal JUnit 5 assumption", e); + } + } +} diff --git a/hamcrest-junit4-junit5-tests/src/test/java/org/hamcrest/JUnit5MatcherAssumeTest.java b/hamcrest-junit4-junit5-tests/src/test/java/org/hamcrest/JUnit5MatcherAssumeTest.java new file mode 100644 index 00000000..cf4929e5 --- /dev/null +++ b/hamcrest-junit4-junit5-tests/src/test/java/org/hamcrest/JUnit5MatcherAssumeTest.java @@ -0,0 +1,62 @@ +package org.hamcrest; + +import org.junit.AssumptionViolatedException; +import org.junit.jupiter.api.Test; +import org.opentest4j.TestAbortedException; + +import static org.hamcrest.MatcherAssume.assumeThat; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests compatibility with JUnit 5 with JUnit 4 and JUnit 5 on the classpath. + * The equivalent test with only JUnit 4 on the classpath is in another module. + */ +class JUnit5MatcherAssumeTest { + + @Test + void + assumptionFailsWithMessage() { + try { + assumeThat("Custom assumption", "a", startsWith("abc")); + fail("should have failed"); + } + catch (TestAbortedException e) { + assertEquals("Assumption failed: Custom assumption", e.getMessage()); + } + catch (AssumptionViolatedException e) { + // If we don't catch JUnit 4 exceptions here, then this test will result in a false positive, or actually + // a false ignored test. + throw new AssertionError("Illegal JUnit 4 assumption", e); + } + } + + @Test void + assumptionFailsWithDefaultMessage() { + try { + assumeThat("a", startsWith("abc")); + fail("should have failed"); + } + catch (TestAbortedException e) { + assertEquals("Assumption failed", e.getMessage()); + } + catch (AssumptionViolatedException e) { + // If we don't catch JUnit 4 exceptions here, then this test will result in a false positive, or actually + // a false ignored test. + throw new AssertionError("Illegal JUnit 4 assumption", e); + } + } + + @Test void + assumptionSucceeds() { + try { + assumeThat("xyz", startsWith("xy")); + } + catch (AssumptionViolatedException e) { + // If we don't catch JUnit 4 exceptions here, then this test will result in a false positive, or actually + // a false ignored test. + throw new AssertionError("Illegal JUnit 4 assumption", e); + } + } +} diff --git a/hamcrest-junit4-junit5-tests/src/test/java/org/hamcrest/MatcherAssumeTest.java b/hamcrest-junit4-junit5-tests/src/test/java/org/hamcrest/MatcherAssumeTest.java new file mode 100644 index 00000000..3c85efe5 --- /dev/null +++ b/hamcrest-junit4-junit5-tests/src/test/java/org/hamcrest/MatcherAssumeTest.java @@ -0,0 +1,39 @@ +package org.hamcrest; + +import org.junit.AssumptionViolatedException; +import org.junit.jupiter.api.Test; +import org.opentest4j.TestAbortedException; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; + +import static java.util.concurrent.Executors.newSingleThreadExecutor; +import static org.hamcrest.MatcherAssume.assumeThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.fail; + +class MatcherAssumeTest { + + @Test + void assumptionFailsWithAssertionErrorWhenNoJUnitInStackTrace() throws Throwable { + // Run the assumption on a separate thread to make sure it has JUnit 4 nor JUnit 5 in its stack trace. + ExecutorService executor = newSingleThreadExecutor(); + try { + try { + executor.submit(new Runnable() { + + @Override + public void run() { + assumeThat(1, is(2)); + } + }).get(); + fail("Expected " + ExecutionException.class); + } catch (ExecutionException expected) { + throw expected.getCause(); + } + } catch (AssertionError expected) { + } catch (TestAbortedException | AssumptionViolatedException e) { + throw new AssertionError(e); + } + } +} \ No newline at end of file diff --git a/hamcrest-junit4-tests/hamcrest-junit4-tests.gradle b/hamcrest-junit4-tests/hamcrest-junit4-tests.gradle new file mode 100644 index 00000000..c1f34af0 --- /dev/null +++ b/hamcrest-junit4-tests/hamcrest-junit4-tests.gradle @@ -0,0 +1,17 @@ +dependencies { + testImplementation project(':hamcrest') + testImplementation(group: 'junit', name: 'junit', version: '4.13.2') { + transitive = false + } +} + +jar { + manifest { + attributes 'Implementation-Title': project.name, + 'Implementation-Vendor': 'hamcrest.org', + 'Implementation-Version': version, + 'Automatic-Module-Name': 'org.hamcrest.junit4-tests' + } +} + +javadoc.title = "Hamcrest JUnit 4 Tests $version" diff --git a/hamcrest-junit4-tests/src/test/java/org/hamcrest/JUnit4MatcherAssumeTest.java b/hamcrest-junit4-tests/src/test/java/org/hamcrest/JUnit4MatcherAssumeTest.java new file mode 100644 index 00000000..a0e0adb4 --- /dev/null +++ b/hamcrest-junit4-tests/src/test/java/org/hamcrest/JUnit4MatcherAssumeTest.java @@ -0,0 +1,43 @@ +package org.hamcrest; + +import org.junit.Test; +import org.junit.AssumptionViolatedException; + +import static org.hamcrest.MatcherAssume.assumeThat; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * Tests compatibility with JUnit 4 with only JUnit 4 on the classpath. + * The equivalent test with JUnit 4 and JUnit 5 on the classpath is in another module. + */ +public class JUnit4MatcherAssumeTest { + + @Test public void + assumptionFailsWithMessage() { + try { + assumeThat("Custom assumption", "a", startsWith("abc")); + fail("should have failed"); + } + catch (AssumptionViolatedException e) { + assertEquals("Custom assumption: got: \"a\", expected: a string starting with \"abc\"", e.getMessage()); + } + } + + @Test public void + assumptionFailsWithDefaultMessage() { + try { + assumeThat("a", startsWith("abc")); + fail("should have failed"); + } + catch (AssumptionViolatedException e) { + assertEquals(": got: \"a\", expected: a string starting with \"abc\"", e.getMessage()); + } + } + + @Test public void + assumptionSucceeds() { + assumeThat("xyz", startsWith("xy")); + } +} diff --git a/hamcrest-junit5-tests/hamcrest-junit5-tests.gradle b/hamcrest-junit5-tests/hamcrest-junit5-tests.gradle new file mode 100644 index 00000000..388b8ab0 --- /dev/null +++ b/hamcrest-junit5-tests/hamcrest-junit5-tests.gradle @@ -0,0 +1,21 @@ +dependencies { + api project(':hamcrest') + api 'org.opentest4j:opentest4j:1.2.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' +} + +jar { + manifest { + attributes 'Implementation-Title': project.name, + 'Implementation-Vendor': 'hamcrest.org', + 'Implementation-Version': version, + 'Automatic-Module-Name': 'org.hamcrest.junit5-tests' + } +} + +test { + useJUnitPlatform() +} + +javadoc.title = "Hamcrest JUnit 5 Tests $version" diff --git a/hamcrest-junit5-tests/src/test/java/org/hamcrest/JUnit5MatcherAssumeTest.java b/hamcrest-junit5-tests/src/test/java/org/hamcrest/JUnit5MatcherAssumeTest.java new file mode 100644 index 00000000..12f5ddb9 --- /dev/null +++ b/hamcrest-junit5-tests/src/test/java/org/hamcrest/JUnit5MatcherAssumeTest.java @@ -0,0 +1,44 @@ +package org.hamcrest; + +import org.junit.jupiter.api.Test; +import org.opentest4j.TestAbortedException; + +import static org.hamcrest.MatcherAssume.assumeThat; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests compatibility with JUnit 5 with only JUnit 5 on the classpath. + * The equivalent test with JUnit 4 and JUnit 5 on the classpath is in another module. + */ +class JUnit5MatcherAssumeTest { + + @Test + void + assumptionFailsWithMessage() { + try { + assumeThat("Custom assumption", "a", startsWith("abc")); + fail("should have failed"); + } + catch (TestAbortedException e) { + assertEquals("Assumption failed: Custom assumption", e.getMessage()); + } + } + + @Test void + assumptionFailsWithDefaultMessage() { + try { + assumeThat("a", startsWith("abc")); + fail("should have failed"); + } + catch (TestAbortedException e) { + assertEquals("Assumption failed", e.getMessage()); + } + } + + @Test void + assumptionSucceeds() { + assumeThat("xyz", startsWith("xy")); + } +} diff --git a/hamcrest/hamcrest.gradle b/hamcrest/hamcrest.gradle index 749b7255..c2b35ccd 100644 --- a/hamcrest/hamcrest.gradle +++ b/hamcrest/hamcrest.gradle @@ -3,7 +3,9 @@ apply plugin: 'osgi' version = rootProject.version dependencies { - testImplementation(group: 'junit', name: 'junit', version: '4.13') { + compileOnly 'org.junit.jupiter:junit-jupiter-api:5.8.2' + compileOnly 'junit:junit:4.13.2' + testImplementation(group: 'junit', name: 'junit', version: '4.13.2') { transitive = false } } diff --git a/hamcrest/src/main/java/org/hamcrest/MatcherAssume.java b/hamcrest/src/main/java/org/hamcrest/MatcherAssume.java new file mode 100644 index 00000000..bcd7fa65 --- /dev/null +++ b/hamcrest/src/main/java/org/hamcrest/MatcherAssume.java @@ -0,0 +1,17 @@ +package org.hamcrest; + +import org.hamcrest.internal.AssumptionProvider; + +public final class MatcherAssume { + + private MatcherAssume() { + } + + public static void assumeThat(T actual, Matcher matcher) { + assumeThat("", actual, matcher); + } + + public static void assumeThat(String message, T actual, Matcher matcher) { + AssumptionProvider.getInstance().assumeThat(message, actual, matcher); + } +} diff --git a/hamcrest/src/main/java/org/hamcrest/internal/AssertionAssumptionProvider.java b/hamcrest/src/main/java/org/hamcrest/internal/AssertionAssumptionProvider.java new file mode 100644 index 00000000..0f4feaf6 --- /dev/null +++ b/hamcrest/src/main/java/org/hamcrest/internal/AssertionAssumptionProvider.java @@ -0,0 +1,24 @@ +package org.hamcrest.internal; + +import org.hamcrest.Matcher; + +/** + * The default (fallback) assumption provider throws {@link AssertionError}, by lack of anything better. + */ +class AssertionAssumptionProvider extends AssumptionProvider { + + @Override + public void assumeThat(String message, T actual, Matcher matcher) { + // Java only knows assertions, not assumptions. + // By lack of anything better, we treat an assumption as an assertion by default. + // Alternatively, we could just do nothing, but that would result in tests failing for the wrong reason, + // because their assumptions would silently fail, which is worse. + if (!matcher.matches(actual)) { + throw new AssertionError(isNotBlank(message) ? "Assumption failed: " + message : "Assumption failed"); + } + } + + private static boolean isNotBlank(String string) { + return string != null && !string.trim().isEmpty(); + } +} diff --git a/hamcrest/src/main/java/org/hamcrest/internal/AssumptionProvider.java b/hamcrest/src/main/java/org/hamcrest/internal/AssumptionProvider.java new file mode 100644 index 00000000..3d9640b7 --- /dev/null +++ b/hamcrest/src/main/java/org/hamcrest/internal/AssumptionProvider.java @@ -0,0 +1,85 @@ +package org.hamcrest.internal; + +import org.hamcrest.Matcher; + +import java.util.ArrayList; +import java.util.List; + +import static java.lang.Thread.currentThread; + +/** + * A facade for assumption failures that uses the Java services framework to let implementations plug in their own + * implementations. + * Java does not define an assumption failure like it does {@link AssertionError} for an assertion failure. + * Therefore, testing frameworks such as JUnit 4 and JUnit 5 (OpenTest4J) define their own. + * This class lets the different test frameworks to plug in their own assumption failures. + */ +public abstract class AssumptionProvider { + + private static final AssumptionProvider INSTANCE = new CompositeAssumptionProvider(loadAssumptionProviders()); + + /** + * Assumes that an object is matching a matcher, throwing on violation of that assumption. + * + * @param message a violation message for when the assumption is violated, which may be {@code null}. + * @param actual an object to match, which may be {@code null}. + * @param matcher a matcher to match an object with, which must not be {@code null}. + * @param an object's generic type. + */ + public abstract void assumeThat(String message, T actual, Matcher matcher); + + /** + * Gets the singleton instance assumption provider, which combines 3 assumption providers: + *
    + *
  1. An optional assumption provider for JUnit 5, only if the runtime classpath contains JUnit 5.
  2. + *
  3. An optional assumption provider for JUnit 4, only if the runtime classpath contains JUnit 4.
  4. + *
  5. A default assumption provider which throws {@link AssertionError} on violation.
  6. + *
+ * + * @return the singleton assumption provider, never {@code null}. + */ + public static AssumptionProvider getInstance() { + return INSTANCE; + } + + private static List loadAssumptionProviders() { + ArrayList providers = new ArrayList<>(3); + try { + providers.add(new JUnit5AssumptionProvider()); + } catch (NoClassDefFoundError ignored) { + // Optional JUnit 5 dependency is not on runtime class path. Continue without it. + } + try { + providers.add(new JUnit4AssumptionProvider()); + } catch (NoClassDefFoundError ignored) { + // Optional JUnit 4 dependency is not on runtime class path. Continue without it. + } + providers.add(new AssertionAssumptionProvider()); + providers.trimToSize(); + return providers; + } + + /** + * Searches the stack trace top-down for an occurrence of a package name prefix to reject or accept. + * If the first hit is to reject, then this method returns {@code false}. + * If the first hit is to accept, then this method returns {@code true}. + * If there is no hit at all, then this method returns {@code false}. + * + * @param packageNamePrefixToReject a package name prefix to reject, which must not be {@code null}. + * @param packageNamePrefixToAccept a package name prefix to accept, which must not be {@code null}. + * @return {@code true} if the stack trace contains an entry matching the package name prefix to accept before it + * contains that to reject, {@code false} otherwise. + */ + protected static boolean stackTraceContains(String packageNamePrefixToReject, String packageNamePrefixToAccept) { + StackTraceElement[] stackTrace = currentThread().getStackTrace(); + for (int i = stackTrace.length; --i > -1; ) { + StackTraceElement stackTraceElement = stackTrace[i]; + if (stackTraceElement.getClassName().startsWith(packageNamePrefixToReject)) { + return false; + } else if (stackTraceElement.getClassName().startsWith(packageNamePrefixToAccept)) { + return true; + } + } + return false; + } +} diff --git a/hamcrest/src/main/java/org/hamcrest/internal/CompositeAssumptionProvider.java b/hamcrest/src/main/java/org/hamcrest/internal/CompositeAssumptionProvider.java new file mode 100644 index 00000000..f09cea7b --- /dev/null +++ b/hamcrest/src/main/java/org/hamcrest/internal/CompositeAssumptionProvider.java @@ -0,0 +1,21 @@ +package org.hamcrest.internal; + +import org.hamcrest.Matcher; + +import java.util.List; + +class CompositeAssumptionProvider extends AssumptionProvider { + + final List providers; + + CompositeAssumptionProvider(List providers) { + this.providers = providers; + } + + @Override + public void assumeThat(String message, T actual, Matcher matcher) { + for (AssumptionProvider provider : providers) { + provider.assumeThat(message, actual, matcher); + } + } +} diff --git a/hamcrest/src/main/java/org/hamcrest/internal/JUnit4AssumptionProvider.java b/hamcrest/src/main/java/org/hamcrest/internal/JUnit4AssumptionProvider.java new file mode 100644 index 00000000..00789821 --- /dev/null +++ b/hamcrest/src/main/java/org/hamcrest/internal/JUnit4AssumptionProvider.java @@ -0,0 +1,22 @@ +package org.hamcrest.internal; + +import org.hamcrest.Matcher; +import org.junit.Assume; + +/** + * This class provides assumption violations compatible with JUnit 4. + */ +class JUnit4AssumptionProvider extends AssumptionProvider { + + static { + // Trigger NoClassDefFoundError when JUnit 4 not on classpath. + Assume.class.getName(); + } + + @Override + public void assumeThat(String message, T actual, Matcher matcher) { + if (stackTraceContains("org.junit.jupiter.", "org.junit.runners.")) { + Assume.assumeThat(message, actual, matcher); + } + } +} diff --git a/hamcrest/src/main/java/org/hamcrest/internal/JUnit5AssumptionProvider.java b/hamcrest/src/main/java/org/hamcrest/internal/JUnit5AssumptionProvider.java new file mode 100644 index 00000000..3e21f4d9 --- /dev/null +++ b/hamcrest/src/main/java/org/hamcrest/internal/JUnit5AssumptionProvider.java @@ -0,0 +1,24 @@ +package org.hamcrest.internal; + +import org.hamcrest.Matcher; +import org.junit.jupiter.api.Assumptions; + +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * This class provides assumption violations compatible with JUnit 5. + */ +class JUnit5AssumptionProvider extends AssumptionProvider { + + static { + // Trigger NoClassDefFoundError when JUnit 5 not on classpath. + Assumptions.class.getName(); + } + + @Override + public void assumeThat(String message, T actual, Matcher matcher) { + if (stackTraceContains("org.junit.runners.", "org.junit.jupiter.")) { + assumeTrue(matcher.matches(actual), message); + } + } +} diff --git a/hamcrest/src/test/java/org/hamcrest/MatcherAssumeTest.java b/hamcrest/src/test/java/org/hamcrest/MatcherAssumeTest.java new file mode 100644 index 00000000..1955ba28 --- /dev/null +++ b/hamcrest/src/test/java/org/hamcrest/MatcherAssumeTest.java @@ -0,0 +1,37 @@ +package org.hamcrest; + +import org.junit.Test; + +import static org.hamcrest.MatcherAssume.assumeThat; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.*; + +public class MatcherAssumeTest { + + @Test public void + assumptionFailsWithMessage() { + try { + assumeThat("Custom assumption", "a", startsWith("abc")); + fail("should have failed"); + } + catch (AssertionError e) { + assertEquals("Assumption failed: Custom assumption", e.getMessage()); + } + } + + @Test public void + assumptionFailsWithDefaultMessage() { + try { + assumeThat("a", startsWith("abc")); + fail("should have failed"); + } + catch (AssertionError e) { + assertEquals("Assumption failed", e.getMessage()); + } + } + + @Test public void + assumptionSucceeds() { + assumeThat("xyz", startsWith("xy")); + } +} diff --git a/settings.gradle b/settings.gradle index b2cd43d7..b5dc4694 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,9 @@ enableFeaturePreview('STABLE_PUBLISHING') include 'hamcrest', + 'hamcrest-junit5-tests', + 'hamcrest-junit4-tests', + 'hamcrest-junit4-junit5-tests', 'hamcrest-core', 'hamcrest-library', 'hamcrest-integration'