From c667c5e4aad5a1faae7de2eef64c0213e24509a0 Mon Sep 17 00:00:00 2001 From: Christian Stein Date: Tue, 22 May 2018 18:30:54 +0200 Subject: [PATCH] Add "TestSource" support to dynamic containers and tests This commit introduces the support to add an instance of `java.net.URI` to a dynamic container or test by adding static factory methods that take a test source uri as a parameter. Prior to this commit the source for dynamic tests was always a `MethodSource` pointing to the `@TestFactory` annotated method. Now, for example, it is possible to refer to external files, that are the underlying sources of generated containers and tests. Addresses part of: #1178 --- .../release-notes/release-notes-5.3.0-M1.adoc | 3 + .../junit/jupiter/api/DynamicContainer.java | 28 ++++++-- .../org/junit/jupiter/api/DynamicNode.java | 22 ++++++- .../org/junit/jupiter/api/DynamicTest.java | 23 ++++++- .../descriptor/TestFactoryTestDescriptor.java | 18 +++-- .../junit/jupiter/api/DynamicTestTests.java | 24 +++++++ .../support/descriptor/DefaultUriSource.java | 65 +++++++++++++++++++ .../support/descriptor/FilePosition.java | 48 +++++++++++++- .../engine/support/descriptor/UriSource.java | 44 +++++++++++++ .../platform/console/ConsoleDetailsTests.java | 39 ++++++++--- .../support/descriptor/FilePositionTests.java | 40 +++++++++++- 11 files changed, 326 insertions(+), 28 deletions(-) create mode 100644 junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/DefaultUriSource.java diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.3.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.3.0-M1.adoc index 3fe8da7b5b0d..0cbe198b3d54 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.3.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.3.0-M1.adoc @@ -65,6 +65,9 @@ on GitHub. _alias_ for `Arguments.of()`. `arguments()` is intended to be used via `import static`. * New `get(index)` Kotlin extension method to make `ArgumentsAccessor` friendlier to use from Kotlin. +* New support for supplying a custom test source `URI` when creating a dynamic container + or test. See factory methods `dynamicContainer(String, URI, ...)` in `DynamicContainer` + and `dynamicTest(String, URI, Executable)` in `DynamicTest` for details. [[release-notes-5.3.0-M1-junit-vintage]] diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicContainer.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicContainer.java index 44dfcb0ea667..7a9136baf918 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicContainer.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicContainer.java @@ -12,6 +12,7 @@ import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import java.net.URI; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -49,7 +50,7 @@ public class DynamicContainer extends DynamicNode { * @see #dynamicContainer(String, Stream) */ public static DynamicContainer dynamicContainer(String displayName, Iterable dynamicNodes) { - return new DynamicContainer(displayName, StreamSupport.stream(dynamicNodes.spliterator(), false)); + return dynamicContainer(displayName, null, StreamSupport.stream(dynamicNodes.spliterator(), false)); } /** @@ -65,13 +66,32 @@ public static DynamicContainer dynamicContainer(String displayName, Iterable dynamicNodes) { - return new DynamicContainer(displayName, dynamicNodes); + return dynamicContainer(displayName, null, dynamicNodes); + } + + /** + * Factory for creating a new {@code DynamicContainer} for the supplied display + * name, the test source uri, and stream of dynamic nodes. + * + *

The stream of dynamic nodes must not contain {@code null} elements. + * + * @param displayName the display name for the dynamic container; never + * {@code null} or blank + * @param testSourceUri the test source uri for the dynamic test; can be {@code null} + * @param dynamicNodes stream of dynamic nodes to execute; + * never {@code null} + * @since 5.3 + * @see #dynamicContainer(String, Iterable) + */ + public static DynamicContainer dynamicContainer(String displayName, URI testSourceUri, + Stream dynamicNodes) { + return new DynamicContainer(displayName, testSourceUri, dynamicNodes); } private final Stream children; - private DynamicContainer(String displayName, Stream children) { - super(displayName); + private DynamicContainer(String displayName, URI testSourceUri, Stream children) { + super(displayName, testSourceUri); Preconditions.notNull(children, "children must not be null"); this.children = children; } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicNode.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicNode.java index e3d44c90552f..cc2a079e29e4 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicNode.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicNode.java @@ -12,6 +12,9 @@ import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import java.net.URI; +import java.util.Optional; + import org.apiguardian.api.API; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.ToStringBuilder; @@ -29,8 +32,12 @@ public abstract class DynamicNode { private final String displayName; - DynamicNode(String displayName) { + /** Custom test source {@link URI} instance associated with this node; potentially {@code null}. */ + private final URI testSourceUri; + + DynamicNode(String displayName, URI testSourceUri) { this.displayName = Preconditions.notBlank(displayName, "displayName must not be null or blank"); + this.testSourceUri = testSourceUri; } /** @@ -40,9 +47,20 @@ public String getDisplayName() { return this.displayName; } + /** + * Get the optional test source of this {@code DynamicNode}. + * @since 5.3 + */ + public Optional getTestSourceURI() { + return Optional.ofNullable(testSourceUri); + } + @Override public String toString() { - return new ToStringBuilder(this).append("displayName", displayName).toString(); + return new ToStringBuilder(this) // + .append("displayName", displayName) // + .append("testSourceUri", testSourceUri) // + .toString(); } } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicTest.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicTest.java index 2f55ed14cec3..26ad0c043071 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicTest.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/DynamicTest.java @@ -14,6 +14,7 @@ import static java.util.Spliterators.spliteratorUnknownSize; import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import java.net.URI; import java.util.Iterator; import java.util.function.Function; import java.util.stream.Stream; @@ -59,7 +60,23 @@ public class DynamicTest extends DynamicNode { * @see #stream(Iterator, Function, ThrowingConsumer) */ public static DynamicTest dynamicTest(String displayName, Executable executable) { - return new DynamicTest(displayName, executable); + return new DynamicTest(displayName, null, executable); + } + + /** + * Factory for creating a new {@code DynamicTest} for the supplied display + * name, the test source uri, and executable code block. + * + * @param displayName the display name for the dynamic test; never + * {@code null} or blank + * @param testSourceUri the test source uri for the dynamic test; can be {@code null} + * @param executable the executable code block for the dynamic test; + * never {@code null} + * @since 5.3 + * @see #stream(Iterator, Function, ThrowingConsumer) + */ + public static DynamicTest dynamicTest(String displayName, URI testSourceUri, Executable executable) { + return new DynamicTest(displayName, testSourceUri, executable); } /** @@ -101,8 +118,8 @@ public static Stream stream(Iterator inputGenerator, private final Executable executable; - private DynamicTest(String displayName, Executable executable) { - super(displayName); + private DynamicTest(String displayName, URI testSourceUri, Executable executable) { + super(displayName, testSourceUri); this.executable = Preconditions.notNull(executable, "executable must not be null"); } 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 38afca423fcf..0fcdda5b4fcf 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 @@ -30,6 +30,7 @@ import org.junit.platform.commons.util.PreconditionViolationException; import org.junit.platform.engine.TestSource; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.descriptor.UriSource; /** * {@link org.junit.platform.engine.TestDescriptor TestDescriptor} for @@ -108,10 +109,18 @@ private Stream toDynamicNodeStream(Object testFactoryMethodResult) } } + private JUnitException invalidReturnTypeException(Throwable cause) { + String message = String.format( + "@TestFactory method [%s] must return a Stream, Collection, Iterable, or Iterator of %s.", + getTestMethod().toGenericString(), DynamicNode.class.getName()); + return new JUnitException(message, cause); + } + static Optional createDynamicDescriptor(JupiterTestDescriptor parent, DynamicNode node, - int index, TestSource source, DynamicDescendantFilter dynamicDescendantFilter) { + int index, TestSource testSource, DynamicDescendantFilter dynamicDescendantFilter) { UniqueId uniqueId; Supplier descriptorCreator; + TestSource source = computeOptionalTestSource(node).orElse(testSource); if (node instanceof DynamicTest) { DynamicTest test = (DynamicTest) node; uniqueId = parent.getUniqueId().append(DYNAMIC_TEST_SEGMENT_TYPE, "#" + index); @@ -131,11 +140,8 @@ static Optional createDynamicDescriptor(JupiterTestDescri return Optional.empty(); } - private JUnitException invalidReturnTypeException(Throwable cause) { - String message = String.format( - "@TestFactory method [%s] must return a Stream, Collection, Iterable, or Iterator of %s.", - getTestMethod().toGenericString(), DynamicNode.class.getName()); - return new JUnitException(message, cause); + static Optional computeOptionalTestSource(DynamicNode node) { + return node.getTestSourceURI().map(UriSource::from); } } diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/DynamicTestTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/DynamicTestTests.java index 169fe1785df9..ed9702463f55 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/DynamicTestTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/DynamicTestTests.java @@ -12,16 +12,19 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.DynamicContainer.dynamicContainer; import static org.junit.jupiter.api.DynamicTest.dynamicTest; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.junit.jupiter.api.function.Executable; import org.junit.platform.commons.support.ReflectionSupport; import org.opentest4j.AssertionFailedError; @@ -30,6 +33,9 @@ */ class DynamicTestTests { + private static final Executable nix = () -> { + }; + private final List assertedValues = new ArrayList<>(); @Test @@ -79,6 +85,24 @@ void reflectiveOperationThrowingInvocationTargetException() { assertThat(t50.getCause()).hasMessage("expected: <1> but was: <50>"); } + @Test + void testSourceUriIsNotPresentByDefault() { + DynamicTest test = dynamicTest("foo", nix); + assertThat(test.getTestSourceURI()).isNotPresent(); + assertThat(dynamicContainer("bar", Stream.of(test)).getTestSourceURI()).isNotPresent(); + } + + @Test + void testSourceUriIsReturnedWhenSupplied() { + URI testSourceUri = URI.create("any://test"); + DynamicTest test = dynamicTest("foo", testSourceUri, nix); + URI containerSourceUri = URI.create("other://container"); + DynamicContainer container = dynamicContainer("bar", containerSourceUri, Stream.of(test)); + + assertThat(test.getTestSourceURI().get()).isSameAs(testSourceUri); + assertThat(container.getTestSourceURI().get()).isSameAs(containerSourceUri); + } + private void assert1Equals48Directly() { Assertions.assertEquals(1, 48); } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/DefaultUriSource.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/DefaultUriSource.java new file mode 100644 index 000000000000..f311e7817be5 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/DefaultUriSource.java @@ -0,0 +1,65 @@ +/* + * 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 static org.apiguardian.api.API.Status.STABLE; + +import java.net.URI; +import java.util.Objects; + +import org.apiguardian.api.API; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.commons.util.ToStringBuilder; + +/** + * Default uri-based test source implementation. + * + * @since 1.3 + */ +@API(status = STABLE, since = "1.3") +class DefaultUriSource implements UriSource { + + private static final long serialVersionUID = 1L; + + private final URI uri; + + DefaultUriSource(URI uri) { + this.uri = Preconditions.notNull(uri, "uri must not be null"); + } + + @Override + public URI getUri() { + return uri; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultUriSource that = (DefaultUriSource) o; + return Objects.equals(this.uri, that.uri); + } + + @Override + public int hashCode() { + return Objects.hash(this.uri); + } + + @Override + public String toString() { + return new ToStringBuilder(this).append("uri", this.uri).toString(); + } + +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/FilePosition.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/FilePosition.java index 488879d6e25a..7f036fe24d2d 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/FilePosition.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/FilePosition.java @@ -17,7 +17,9 @@ import java.util.Optional; 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.commons.util.ToStringBuilder; /** @@ -52,6 +54,50 @@ public static FilePosition from(int line, int column) { return new FilePosition(line, column); } + /** + * Create an optional {@code FilePosition} parsing the supplied + * {@code query} string. + * + *

Examples of valid {@code query} strings: + *

+ * + * @param query the query string; may be {@code null} + * @since 1.3 + */ + public static Optional fromQuery(String query) { + FilePosition result = null; + Integer line = null; + Integer column = null; + if (StringUtils.isNotBlank(query)) { + try { + for (String pair : query.split("&")) { + String[] data = pair.split("="); + if (data.length == 2) { + String key = data[0]; + int value = Integer.valueOf(data[1]); + if (key.equals("line")) { + line = value; + } + if (key.equals("column")) { + column = value; + } + } + } + } + catch (IllegalArgumentException e) { + LoggerFactory.getLogger(FilePosition.class).debug(e, () -> "parsing query failed: " + query); + // fall-through and continue + } + if (line != null) { + result = column == null ? new FilePosition(line) : new FilePosition(line, column); + } + } + return Optional.ofNullable(result); + } + private final int line; private final Integer column; @@ -65,7 +111,7 @@ private FilePosition(int line, int column) { Preconditions.condition(line > 0, "line number must be greater than zero"); Preconditions.condition(column > 0, "column number must be greater than zero"); this.line = line; - this.column = Integer.valueOf(column); + this.column = column; } /** 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 eacf59675aa5..481b1bd7e468 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 @@ -13,8 +13,14 @@ import static org.apiguardian.api.API.Status.STABLE; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; 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; /** @@ -33,4 +39,42 @@ public interface UriSource extends TestSource { */ URI getUri(); + /** + * Create a new {@code UriSource} using the supplied {@code uri}. + *

+ * This implementation tries resolve the {@code uri} to local file + * system path-based source first. If that fails for any reason, an + * instance of a simple default uri source class storing the supplied + * {@code uri} as-is is returned. + * + * @param uri the uri instance; must not be {@code null} + * @return a uri source instance + * @since 1.3 + * @see org.junit.platform.engine.support.descriptor.FileSource + * @see org.junit.platform.engine.support.descriptor.DirectorySource + */ + static UriSource from(final URI uri) { + Preconditions.notNull(uri, "uri must not be null"); + try { + URI pathBasedUri = uri; + String query = pathBasedUri.getQuery(); + if (StringUtils.isNotBlank(query)) { + String s = pathBasedUri.toString(); + pathBasedUri = URI.create(s.substring(0, s.indexOf('?'))); + } + Path path = Paths.get(pathBasedUri); + if (Files.isRegularFile(path)) { + return FileSource.from(path.toFile(), FilePosition.fromQuery(query).orElse(null)); + } + if (Files.isDirectory(path)) { + return DirectorySource.from(path.toFile()); + } + } + catch (IllegalArgumentException e) { + LoggerFactory.getLogger(UriSource.class).debug(e, () -> "uri not path-based: " + uri); + } + // store uri as-is + return new DefaultUriSource(uri); + } + } diff --git a/platform-tests/src/test/java/org/junit/platform/console/ConsoleDetailsTests.java b/platform-tests/src/test/java/org/junit/platform/console/ConsoleDetailsTests.java index 361659108a3f..a6396b31032a 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/ConsoleDetailsTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/ConsoleDetailsTests.java @@ -16,10 +16,15 @@ import static org.junit.jupiter.api.Assertions.assertLinesMatch; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.api.DynamicContainer.dynamicContainer; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; import static org.junit.platform.commons.util.ReflectionUtils.findMethods; import static org.junit.platform.commons.util.ReflectionUtils.getFullyQualifiedMethodName; +import java.io.File; import java.lang.reflect.Method; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; @@ -33,7 +38,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.DynamicContainer; import org.junit.jupiter.api.DynamicNode; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; @@ -95,11 +99,14 @@ private List scanContainerClassAndCreateDynamicTests(Class conta String displayName = methodName + "() " + theme.name(); String dirName = "console/details/" + containerName.toLowerCase(); String outName = caption + ".out.txt"; - tests.add(DynamicTest.dynamicTest(displayName, new Runner(dirName, outName, args))); + Runner runner = new Runner(dirName, outName, args); + URI source = toUri(dirName, outName).orElse(null); + tests.add(dynamicTest(displayName, source, runner)); } } } - map.forEach((details, tests) -> nodes.add(DynamicContainer.dynamicContainer(details.name(), tests))); + URI source = new File("src/test/resources/console/details").toURI(); + map.forEach((details, tests) -> nodes.add(dynamicContainer(details.name(), source, tests.stream()))); return nodes; } @@ -194,22 +201,20 @@ public void execute() throws Throwable { ConsoleLauncherWrapper wrapper = new ConsoleLauncherWrapper(); ConsoleLauncherWrapperResult result = wrapper.execute(Optional.empty(), args); - String resourceName = dirName + "/" + outName; - Optional optionalUrl = Optional.ofNullable(getClass().getClassLoader().getResource(resourceName)); - if (!optionalUrl.isPresent()) { + Optional optionalUri = toUri(dirName, outName); + if (!optionalUri.isPresent()) { if (Boolean.getBoolean("org.junit.platform.console.ConsoleDetailsTests.writeResultOut")) { // do not use Files.createTempDirectory(prefix) as we want one folder for one container Path temp = Paths.get(System.getProperty("java.io.tmpdir"), dirName.replace('/', '-')); Files.createDirectories(temp); Path path = Files.write(temp.resolve(outName), result.out.getBytes(UTF_8)); assumeTrue(false, - format("resource `%s` not found\nwrote console stdout to: %s", resourceName, path)); + format("resource `%s` not found\nwrote console stdout to: %s/%s", dirName, outName, path)); } - fail("could not load resource named `" + resourceName + "`"); + fail("could not load resource named `" + dirName + "/" + outName + "`"); } - URL url = optionalUrl.orElseThrow(AssertionError::new); - Path path = Paths.get(url.toURI()); + Path path = Paths.get(optionalUri.get()); assumeTrue(Files.exists(path), "path does not exist: " + path); assumeTrue(Files.isReadable(path), "can not read: " + path); @@ -220,4 +225,18 @@ public void execute() throws Throwable { } } + static Optional toUri(String dirName, String outName) { + String resourceName = dirName + "/" + outName; + URL url = ConsoleDetailsTests.class.getClassLoader().getResource(resourceName); + if (url == null) { + return Optional.empty(); + } + try { + return Optional.of(url.toURI()); + } + catch (URISyntaxException e) { + return Optional.empty(); + } + } + } diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/FilePositionTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/FilePositionTests.java index 97c2fdbef595..2e844681c525 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/FilePositionTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/descriptor/FilePositionTests.java @@ -11,10 +11,17 @@ package org.junit.platform.engine.support.descriptor; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.Optional; +import java.util.stream.Stream; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.platform.commons.util.PreconditionViolationException; /** @@ -34,7 +41,7 @@ void preconditions() { @Test @DisplayName("create FilePosition from factory method with line number") - void filePositionFromLine() throws Exception { + void filePositionFromLine() { FilePosition filePosition = FilePosition.from(42); assertThat(filePosition.getLine()).isEqualTo(42); @@ -43,13 +50,42 @@ void filePositionFromLine() throws Exception { @Test @DisplayName("create FilePosition from factory method with line number and column number") - void filePositionFromLineAndColumn() throws Exception { + void filePositionFromLineAndColumn() { FilePosition filePosition = FilePosition.from(42, 99); assertThat(filePosition.getLine()).isEqualTo(42); assertThat(filePosition.getColumn()).contains(99); } + @ParameterizedTest + @MethodSource + void filePositionFromQuery(String query, int expectedLine, int expectedColumn) { + Optional optionalFilePosition = FilePosition.fromQuery(query); + if (optionalFilePosition.isPresent()) { + FilePosition filePosition = optionalFilePosition.get(); + + assertThat(filePosition.getLine()).isEqualTo(expectedLine); + assertThat(filePosition.getColumn().orElse(-1)).isEqualTo(expectedColumn); + return; + } + + assertEquals(-1, expectedLine); + assertEquals(-1, expectedColumn); + } + + @SuppressWarnings("unused") + static Stream filePositionFromQuery() { + return Stream.of( // + Arguments.of(null, -1, -1), // + Arguments.of("?!", -1, -1), // + Arguments.of("line=ZZ", -1, -1), // + Arguments.of("line=42", 42, -1), // + Arguments.of("line=42&line=24", 24, -1), // + Arguments.of("line=42&column=99", 42, 99), // + Arguments.of("line=42&column=ZZ", 42, -1) // + ); + } + @Test @DisplayName("equals() and hashCode() with column number cached by Integer.valueOf()") void equalsAndHashCode() {