From 03d158ba9ca47e0ae2a3a88c86b4dbf124e208c6 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 6 Jul 2018 14:25:56 +0200 Subject: [PATCH] Support classpath resource for custom TestSource in dynamic tests Issue #1178 introduced support for creating a UriSource (specifically a FileSource, DirectorySource, or DefaultUriSource) from a URI supplied for a dynamic container or dynamic test. This commit further extends that feature by introducing support for creating a ClasspathResourceSource from a URI supplied for a dynamic container or dynamic test if the URI contains the "classpath" scheme. Otherwise, the behavior introduced in #1178 is used. Issue: #1467 --- .../release-notes-5.3.0-RC1.adoc | 5 +- .../descriptor/TestFactoryTestDescriptor.java | 14 +- .../TestFactoryTestDescriptorTests.java | 139 ++++++++++++++---- .../descriptor/ClasspathResourceSource.java | 37 +++++ .../support/descriptor/ResourceUtils.java | 48 ++++++ .../engine/support/descriptor/UriSource.java | 16 +- .../ClasspathResourceSourceTests.java | 44 +++++- 7 files changed, 258 insertions(+), 45 deletions(-) create mode 100644 junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/ResourceUtils.java diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.3.0-RC1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.3.0-RC1.adoc index cc2c28975d4f..e4124accce5c 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.3.0-RC1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.3.0-RC1.adoc @@ -23,7 +23,8 @@ on GitHub. ==== New Features and Improvements -* ❓ +* A `ClasspathResourceSource` can now be created from a `URI` via the new `from(URI)` + static factory method if the `URI` uses the `classpath` scheme. [[release-notes-5.3.0-RC1-junit-jupiter]] @@ -41,6 +42,8 @@ on GitHub. * Although it is _highly discouraged_, it is now possible to extend the `{Assertions}` and `{Assumptions}` classes for special use cases. +* A custom test source `URI` for a dynamic container or dynamic test will now be + registered as a `ClasspathResourceSource` if the `URI` uses the `classpath` scheme. * New `TestInstanceFactory` extension API that enables custom creation of test class instances. - See <<../user-guide/index.adoc#extensions-test-instance-factories, Test Instance diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java index 39160afe7984..36633e6b2ad1 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptor.java @@ -11,8 +11,10 @@ package org.junit.jupiter.engine.descriptor; import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.platform.engine.support.descriptor.ClasspathResourceSource.CLASSPATH_SCHEME; import java.lang.reflect.Method; +import java.net.URI; import java.util.Iterator; import java.util.Optional; import java.util.function.Supplier; @@ -28,8 +30,10 @@ import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.util.CollectionUtils; import org.junit.platform.commons.util.PreconditionViolationException; +import org.junit.platform.commons.util.Preconditions; import org.junit.platform.engine.TestSource; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.descriptor.ClasspathResourceSource; import org.junit.platform.engine.support.descriptor.UriSource; /** @@ -121,7 +125,7 @@ static Optional createDynamicDescriptor(JupiterTestDescri UniqueId uniqueId; Supplier descriptorCreator; - Optional customTestSource = node.getTestSourceUri().map(UriSource::from); + Optional customTestSource = node.getTestSourceUri().map(TestFactoryTestDescriptor::fromUri); TestSource source = customTestSource.orElse(defaultTestSource); if (node instanceof DynamicTest) { @@ -143,4 +147,12 @@ static Optional createDynamicDescriptor(JupiterTestDescri return Optional.empty(); } + /** + * @since 5.3 + */ + static TestSource fromUri(URI uri) { + Preconditions.notNull(uri, "URI must not be null"); + return CLASSPATH_SCHEME.equals(uri.getScheme()) ? ClasspathResourceSource.from(uri) : UriSource.from(uri); + } + } diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java index 9292939b4bd3..3d35131c3ebd 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java @@ -10,22 +10,32 @@ package org.junit.jupiter.engine.descriptor; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.File; import java.lang.reflect.Method; +import java.net.URI; import java.util.Optional; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; import org.junit.jupiter.engine.execution.ThrowableCollector; +import org.junit.platform.engine.TestSource; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.descriptor.ClasspathResourceSource; +import org.junit.platform.engine.support.descriptor.DirectorySource; +import org.junit.platform.engine.support.descriptor.FilePosition; +import org.junit.platform.engine.support.descriptor.FileSource; +import org.junit.platform.engine.support.descriptor.UriSource; import org.junit.platform.engine.support.hierarchical.Node; /** @@ -35,48 +45,115 @@ */ class TestFactoryTestDescriptorTests { - private JupiterEngineExecutionContext context; - private ExtensionContext extensionContext; - private TestFactoryTestDescriptor descriptor; - private boolean isClosed; + /** + * @since 5.3 + */ + @Nested + class TestSources { + + @Test + void classpathResourceSourceFromUriWithFilePosition() { + FilePosition position = FilePosition.from(42, 21); + URI uri = URI.create("classpath:/test.js?line=42&column=21"); + TestSource testSource = TestFactoryTestDescriptor.fromUri(uri); + + assertThat(testSource).isInstanceOf(ClasspathResourceSource.class); + ClasspathResourceSource source = (ClasspathResourceSource) testSource; + assertThat(source.getClasspathResourceName()).isEqualTo("test.js"); + assertThat(source.getPosition()).hasValue(position); + } - @BeforeEach - void before() throws Exception { - extensionContext = mock(ExtensionContext.class); - isClosed = false; + @Test + void fileSourceFromUriWithFilePosition() { + File file = new File("src/test/resources/log4j2-test.xml"); + assertThat(file).isFile(); - context = new JupiterEngineExecutionContext(null, null).extend().withThrowableCollector( - new ThrowableCollector()).withExtensionContext(extensionContext).build(); + FilePosition position = FilePosition.from(42, 21); + URI uri = URI.create(file.toURI().toString() + "?line=42&column=21"); + TestSource testSource = TestFactoryTestDescriptor.fromUri(uri); - Method testMethod = CustomStreamTestCase.class.getDeclaredMethod("customStream"); - descriptor = new TestFactoryTestDescriptor(UniqueId.forEngine("engine"), CustomStreamTestCase.class, - testMethod); - when(extensionContext.getTestMethod()).thenReturn(Optional.of(testMethod)); - } + assertThat(testSource).isInstanceOf(FileSource.class); + FileSource source = (FileSource) testSource; + assertThat(source.getFile().getAbsolutePath()).isEqualTo(file.getAbsolutePath()); + assertThat(source.getPosition()).hasValue(position); + } - @Test - void streamsFromTestFactoriesShouldBeClosed() { - Stream dynamicTestStream = Stream.empty(); - prepareMockForTestInstanceWithCustomStream(dynamicTestStream); + @Test + void directorySourceFromUri() { + File file = new File("src/test/resources"); + assertThat(file).isDirectory(); - descriptor.invokeTestMethod(context, mock(Node.DynamicTestExecutor.class)); + URI uri = file.toURI(); + TestSource testSource = TestFactoryTestDescriptor.fromUri(uri); - assertTrue(isClosed); - } + assertThat(testSource).isInstanceOf(DirectorySource.class); + DirectorySource source = (DirectorySource) testSource; + assertThat(source.getFile().getAbsolutePath()).isEqualTo(file.getAbsolutePath()); + } + + @Test + void defaultUriSourceFromUri() { + File file = new File("src/test/resources"); + assertThat(file).isDirectory(); - @Test - void streamsFromTestFactoriesShouldBeClosedWhenTheyThrow() { - Stream integerStream = Stream.of(1, 2); - prepareMockForTestInstanceWithCustomStream(integerStream); + URI uri = URI.create("http://example.com?foo=bar&line=42"); + TestSource testSource = TestFactoryTestDescriptor.fromUri(uri); - descriptor.invokeTestMethod(context, mock(Node.DynamicTestExecutor.class)); + assertThat(testSource).isInstanceOf(UriSource.class); + assertThat(testSource.getClass().getSimpleName()).isEqualTo("DefaultUriSource"); + UriSource source = (UriSource) testSource; + assertThat(source.getUri()).isEqualTo(uri); + } - assertTrue(isClosed); } - private void prepareMockForTestInstanceWithCustomStream(Stream stream) { - Stream mockStream = stream.onClose(() -> isClosed = true); - when(extensionContext.getRequiredTestInstance()).thenReturn(new CustomStreamTestCase(mockStream)); + @Nested + class Streams { + + private JupiterEngineExecutionContext context; + private ExtensionContext extensionContext; + private TestFactoryTestDescriptor descriptor; + private boolean isClosed; + + @BeforeEach + void before() throws Exception { + extensionContext = mock(ExtensionContext.class); + isClosed = false; + + context = new JupiterEngineExecutionContext(null, null).extend().withThrowableCollector( + new ThrowableCollector()).withExtensionContext(extensionContext).build(); + + Method testMethod = CustomStreamTestCase.class.getDeclaredMethod("customStream"); + descriptor = new TestFactoryTestDescriptor(UniqueId.forEngine("engine"), CustomStreamTestCase.class, + testMethod); + when(extensionContext.getTestMethod()).thenReturn(Optional.of(testMethod)); + } + + @Test + void streamsFromTestFactoriesShouldBeClosed() { + Stream dynamicTestStream = Stream.empty(); + prepareMockForTestInstanceWithCustomStream(dynamicTestStream); + + descriptor.invokeTestMethod(context, mock(Node.DynamicTestExecutor.class)); + + assertTrue(isClosed); + } + + @Test + void streamsFromTestFactoriesShouldBeClosedWhenTheyThrow() { + Stream integerStream = Stream.of(1, 2); + prepareMockForTestInstanceWithCustomStream(integerStream); + + descriptor.invokeTestMethod(context, mock(Node.DynamicTestExecutor.class)); + + assertTrue(isClosed); + } + + private void prepareMockForTestInstanceWithCustomStream(Stream stream) { + Stream mockStream = stream.onClose(() -> isClosed = true); + when(extensionContext.getRequiredTestInstance()).thenReturn(new CustomStreamTestCase(mockStream)); + } + } private static class CustomStreamTestCase { diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/ClasspathResourceSource.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/ClasspathResourceSource.java index 5b07db666d4c..3d614c76f2fb 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/ClasspathResourceSource.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/ClasspathResourceSource.java @@ -12,10 +12,12 @@ import static org.apiguardian.api.API.Status.STABLE; +import java.net.URI; import java.util.Objects; import java.util.Optional; import org.apiguardian.api.API; +import org.junit.platform.commons.util.PreconditionViolationException; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.TestSource; @@ -32,6 +34,14 @@ public class ClasspathResourceSource implements TestSource { private static final long serialVersionUID = 1L; + /** + * {@link URI} {@linkplain URI#getScheme() scheme} for classpath + * resources: {@value} + * + * @since 1.3 + */ + public static final String CLASSPATH_SCHEME = "classpath"; + /** * Create a new {@code ClasspathResourceSource} using the supplied classpath * resource name. @@ -70,6 +80,33 @@ public static ClasspathResourceSource from(String classpathResourceName, FilePos return new ClasspathResourceSource(classpathResourceName, filePosition); } + /** + * Create a new {@code ClasspathResourceSource} from the supplied {@link URI}. + * + *

The {@link URI#getPath() path} component of the {@code URI} (excluding + * the query) will be used as the classpath resource name. The + * {@linkplain URI#getQuery() query} component of the {@code URI}, if present, + * will be used to retrieve the {@link FilePosition} via + * {@link FilePosition#fromQuery(String)}. + * + * @param uri the {@code URI} for the classpath resource; never {@code null} + * @return a new {@code ClasspathResourceSource}; never {@code null} + * @throws PreconditionViolationException if the supplied {@code URI} is + * {@code null} or if the scheme of the supplied {@code URI} is not equal + * to the {@link #CLASSPATH_SCHEME} + * @since 1.3 + * @see #CLASSPATH_SCHEME + */ + public static ClasspathResourceSource from(URI uri) { + Preconditions.notNull(uri, "URI must not be null"); + Preconditions.condition(CLASSPATH_SCHEME.equals(uri.getScheme()), + () -> "URI [" + uri + "] must have [" + CLASSPATH_SCHEME + "] scheme"); + + String classpathResource = ResourceUtils.stripQueryComponent(uri).getPath(); + FilePosition filePosition = FilePosition.fromQuery(uri.getQuery()).orElse(null); + return ClasspathResourceSource.from(classpathResource, filePosition); + } + private final String classpathResourceName; private final FilePosition filePosition; diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/ResourceUtils.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/ResourceUtils.java new file mode 100644 index 000000000000..1812928eb444 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/ResourceUtils.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015-2018 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 + * + * http://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.platform.engine.support.descriptor; + +import java.net.URI; + +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.commons.util.StringUtils; + +/** + * Collection of static utility methods for working with resources. + * + * @since 1.3 + */ +final class ResourceUtils { + + private ResourceUtils() { + /* no-op */ + } + + /** + * Strip the {@link URI#getQuery() query} component from the supplied + * {@link URI}. + * + * @param uri the {@code URI} from which to strip the query component + * @return a new {@code URI} with the query component removed, or the + * original {@code URI} unmodified if it does not have a query component + */ + static URI stripQueryComponent(URI uri) { + Preconditions.notNull(uri, "URI must not be null"); + + if (StringUtils.isBlank(uri.getQuery())) { + return uri; + } + + String uriAsString = uri.toString(); + return URI.create(uriAsString.substring(0, uriAsString.indexOf('?'))); + } + +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/UriSource.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/UriSource.java index 2c8420cd44fe..2f204b224134 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/UriSource.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/UriSource.java @@ -20,7 +20,6 @@ import org.apiguardian.api.API; import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.util.Preconditions; -import org.junit.platform.commons.util.StringUtils; import org.junit.platform.engine.TestSource; /** @@ -58,22 +57,17 @@ static UriSource from(URI uri) { Preconditions.notNull(uri, "URI must not be null"); try { - URI pathBasedUriWithoutQuery = uri; - String query = uri.getQuery(); - if (StringUtils.isNotBlank(query)) { - String uriAsString = uri.toString(); - pathBasedUriWithoutQuery = URI.create(uriAsString.substring(0, uriAsString.indexOf('?'))); - } - Path path = Paths.get(pathBasedUriWithoutQuery); + URI uriWithoutQuery = ResourceUtils.stripQueryComponent(uri); + Path path = Paths.get(uriWithoutQuery); if (Files.isRegularFile(path)) { - return FileSource.from(path.toFile(), FilePosition.fromQuery(query).orElse(null)); + return FileSource.from(path.toFile(), FilePosition.fromQuery(uri.getQuery()).orElse(null)); } if (Files.isDirectory(path)) { return DirectorySource.from(path.toFile()); } } - catch (RuntimeException e) { - LoggerFactory.getLogger(UriSource.class).debug(e, () -> String.format( + catch (RuntimeException ex) { + LoggerFactory.getLogger(UriSource.class).debug(ex, () -> String.format( "The supplied URI [%s] is not path-based. Falling back to default UriSource implementation.", uri)); } diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/ClasspathResourceSourceTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/ClasspathResourceSourceTests.java index d37e4c1a0e25..6b54acb0c23d 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/ClasspathResourceSourceTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/ClasspathResourceSourceTests.java @@ -12,7 +12,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.platform.engine.support.descriptor.ClasspathResourceSource.CLASSPATH_SCHEME; +import java.net.URI; import java.util.stream.Stream; import org.junit.jupiter.api.Test; @@ -28,6 +30,8 @@ class ClasspathResourceSourceTests extends AbstractTestSourceTests { private static final String FOO_RESOURCE = "test/foo.xml"; private static final String BAR_RESOURCE = "/config/bar.json"; + private static final URI FOO_RESOURCE_URI = URI.create(CLASSPATH_SCHEME + ":/" + FOO_RESOURCE); + @Override Stream createSerializableInstances() { return Stream.of(ClasspathResourceSource.from(FOO_RESOURCE)); @@ -35,15 +39,20 @@ Stream createSerializableInstances() { @Test void preconditions() { - assertThrows(PreconditionViolationException.class, () -> ClasspathResourceSource.from(null)); + assertThrows(PreconditionViolationException.class, () -> ClasspathResourceSource.from((String) null)); assertThrows(PreconditionViolationException.class, () -> ClasspathResourceSource.from("")); assertThrows(PreconditionViolationException.class, () -> ClasspathResourceSource.from(" ")); + + assertThrows(PreconditionViolationException.class, () -> ClasspathResourceSource.from((URI) null)); + assertThrows(PreconditionViolationException.class, + () -> ClasspathResourceSource.from(URI.create("file:/foo.txt"))); } @Test void resourceWithoutPosition() { ClasspathResourceSource source = ClasspathResourceSource.from(FOO_RESOURCE); + assertThat(source).isNotNull(); assertThat(source.getClasspathResourceName()).isEqualTo(FOO_RESOURCE); assertThat(source.getPosition()).isEmpty(); } @@ -52,6 +61,7 @@ void resourceWithoutPosition() { void resourceWithLeadingSlashWithoutPosition() { ClasspathResourceSource source = ClasspathResourceSource.from("/" + FOO_RESOURCE); + assertThat(source).isNotNull(); assertThat(source.getClasspathResourceName()).isEqualTo(FOO_RESOURCE); assertThat(source.getPosition()).isEmpty(); } @@ -61,6 +71,38 @@ void resourceWithPosition() { FilePosition position = FilePosition.from(42, 23); ClasspathResourceSource source = ClasspathResourceSource.from(FOO_RESOURCE, position); + assertThat(source).isNotNull(); + assertThat(source.getClasspathResourceName()).isEqualTo(FOO_RESOURCE); + assertThat(source.getPosition()).hasValue(position); + } + + @Test + void resourcefromUriWithoutPosition() { + ClasspathResourceSource source = ClasspathResourceSource.from(FOO_RESOURCE_URI); + + assertThat(source).isNotNull(); + assertThat(source.getClasspathResourceName()).isEqualTo(FOO_RESOURCE); + assertThat(source.getPosition()).isEmpty(); + } + + @Test + void resourceFromUriWithLineNumber() { + FilePosition position = FilePosition.from(42); + URI uri = URI.create(FOO_RESOURCE_URI + "?line=42"); + ClasspathResourceSource source = ClasspathResourceSource.from(uri); + + assertThat(source).isNotNull(); + assertThat(source.getClasspathResourceName()).isEqualTo(FOO_RESOURCE); + assertThat(source.getPosition()).hasValue(position); + } + + @Test + void resourceFromUriWithLineAndColumnNumbers() { + FilePosition position = FilePosition.from(42, 23); + URI uri = URI.create(FOO_RESOURCE_URI + "?line=42&foo=bar&column=23"); + ClasspathResourceSource source = ClasspathResourceSource.from(uri); + + assertThat(source).isNotNull(); assertThat(source.getClasspathResourceName()).isEqualTo(FOO_RESOURCE); assertThat(source.getPosition()).hasValue(position); }