Repository: junit-team/junit5 (language: java)

Issue:
Add support for passing line and column number to ConsoleLauncher via `--select-file` and `--select-resource`
This should be done using the same syntax that is supported for the URI argument of dynamic tests (`?line=...&column=...`).

## Related issues
- https://github.com/junit-team/junit5/issues/3366#issuecomment-1625211213
- #3484
- #3485


Discussion / hints:

Fix patch:
diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc
index 5f8f864a4a6b..c9a3569af431 100644
--- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc
+++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc
@@ -31,6 +31,8 @@ JUnit repository on GitHub.
   calling the internal `ReflectionUtils.makeAccessible(Field)` method directly.
 * Support both the primitive type `void` and the wrapper type `Void` in the internal
   `ReflectionUtils` to allow `String` to `Class` conversion in parameterized tests.
+* Add support for passing line and column number to `ConsoleLauncher` via
+  `--select-file` and `--select-resource`.
 
 
 [[release-notes-5.12.0-M1-junit-jupiter]]
diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/ResourceUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ResourceUtils.java
similarity index 74%
rename from junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/ResourceUtils.java
rename to junit-platform-commons/src/main/java/org/junit/platform/commons/util/ResourceUtils.java
index b61598d59b8e..fd6b78e3223e 100644
--- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/descriptor/ResourceUtils.java
+++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ResourceUtils.java
@@ -8,19 +8,21 @@
  * https://www.eclipse.org/legal/epl-v20.html
  */
 
-package org.junit.platform.engine.support.descriptor;
+package org.junit.platform.commons.util;
+
+import static org.apiguardian.api.API.Status.INTERNAL;
 
 import java.net.URI;
 
-import org.junit.platform.commons.util.Preconditions;
-import org.junit.platform.commons.util.StringUtils;
+import org.apiguardian.api.API;
 
 /**
  * Collection of static utility methods for working with resources.
  *
- * @since 1.3
+ * @since 1.3 (originally in org.junit.platform.engine.support.descriptor)
  */
-final class ResourceUtils {
+@API(status = INTERNAL, since = "1.12")
+public final class ResourceUtils {
 
 	private ResourceUtils() {
 		/* no-op */
@@ -33,8 +35,10 @@ private ResourceUtils() {
 	 * @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
+	 *
+	 * @since 1.3
 	 */
-	static URI stripQueryComponent(URI uri) {
+	public static URI stripQueryComponent(URI uri) {
 		Preconditions.notNull(uri, "URI must not be null");
 
 		if (StringUtils.isBlank(uri.getQuery())) {
diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/SelectorConverter.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/SelectorConverter.java
index 7c18320eba67..6f1803185706 100644
--- a/junit-platform-console/src/main/java/org/junit/platform/console/options/SelectorConverter.java
+++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/SelectorConverter.java
@@ -19,12 +19,16 @@
 import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage;
 import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUri;
 
+import java.net.URI;
+
 import org.junit.platform.commons.PreconditionViolationException;
+import org.junit.platform.commons.util.ResourceUtils;
 import org.junit.platform.engine.DiscoverySelectorIdentifier;
 import org.junit.platform.engine.discovery.ClassSelector;
 import org.junit.platform.engine.discovery.ClasspathResourceSelector;
 import org.junit.platform.engine.discovery.DirectorySelector;
 import org.junit.platform.engine.discovery.DiscoverySelectors;
+import org.junit.platform.engine.discovery.FilePosition;
 import org.junit.platform.engine.discovery.FileSelector;
 import org.junit.platform.engine.discovery.IterationSelector;
 import org.junit.platform.engine.discovery.MethodSelector;
@@ -53,8 +57,12 @@ public UriSelector convert(String value) {
 	static class File implements ITypeConverter<FileSelector> {
 		@Override
 		public FileSelector convert(String value) {
-			return selectFile(value);
+			URI uri = URI.create(value);
+			String path = ResourceUtils.stripQueryComponent(uri).getPath();
+			FilePosition filePosition = FilePosition.fromQuery(uri.getQuery()).orElse(null);
+			return selectFile(path, filePosition);
 		}
+
 	}
 
 	static class Directory implements ITypeConverter<DirectorySelector> {
@@ -88,7 +96,10 @@ public MethodSelector convert(String value) {
 	static class ClasspathResource implements ITypeConverter<ClasspathResourceSelector> {
 		@Override
 		public ClasspathResourceSelector convert(String value) {
-			return selectClasspathResource(value);
+			URI uri = URI.create(value);
+			String path = ResourceUtils.stripQueryComponent(uri).getPath();
+			FilePosition filePosition = FilePosition.fromQuery(uri.getQuery()).orElse(null);
+			return selectClasspathResource(path, filePosition);
 		}
 	}
 
diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptionsMixin.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptionsMixin.java
index d71b1f0a6e2f..46298abb9d11 100644
--- a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptionsMixin.java
+++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptionsMixin.java
@@ -76,7 +76,10 @@ static class SelectorOptions {
 		private final List<UriSelector> selectedUris2 = new ArrayList<>();
 
 		@Option(names = { "-f",
-				"--select-file" }, paramLabel = "FILE", arity = "1", converter = SelectorConverter.File.class, description = "Select a file for test discovery. This option can be repeated.")
+				"--select-file" }, paramLabel = "FILE", arity = "1", converter = SelectorConverter.File.class, //
+				description = "Select a file for test discovery. "
+						+ "The line and column numbers can be provided as URI query parameters (e.g. foo.txt?line=12&column=34). "
+						+ "This option can be repeated.")
 		private final List<FileSelector> selectedFiles = new ArrayList<>();
 
 		@Option(names = { "--f", "-select-file" }, arity = "1", hidden = true, converter = SelectorConverter.File.class)
@@ -123,7 +126,10 @@ static class SelectorOptions {
 		private final List<MethodSelector> selectedMethods2 = new ArrayList<>();
 
 		@Option(names = { "-r",
-				"--select-resource" }, paramLabel = "RESOURCE", arity = "1", converter = SelectorConverter.ClasspathResource.class, description = "Select a classpath resource for test discovery. This option can be repeated.")
+				"--select-resource" }, paramLabel = "RESOURCE", arity = "1", converter = SelectorConverter.ClasspathResource.class, //
+				description = "Select a classpath resource for test discovery. "
+						+ "The line and column numbers can be provided as URI query parameters (e.g. foo.csv?line=12&column=34). "
+						+ "This option can be repeated.")
 		private final List<ClasspathResourceSelector> selectedClasspathResources = new ArrayList<>();
 
 		@Option(names = { "--r",
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 595dd33cd307..e326a3343a58 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
@@ -19,6 +19,7 @@
 import org.apiguardian.api.API;
 import org.junit.platform.commons.PreconditionViolationException;
 import org.junit.platform.commons.util.Preconditions;
+import org.junit.platform.commons.util.ResourceUtils;
 import org.junit.platform.commons.util.ToStringBuilder;
 import org.junit.platform.engine.TestSource;
 
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 bd97bbe609e1..fef3e61b5db7 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,6 +20,7 @@
 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.ResourceUtils;
 import org.junit.platform.engine.TestSource;
 
 /**


Test patch:
diff --git a/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java b/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java
index 3b9b10935c9f..acb491afa48f 100644
--- a/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java
+++ b/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java
@@ -44,6 +44,7 @@
 import org.junit.jupiter.params.provider.EnumSource;
 import org.junit.platform.engine.DiscoverySelector;
 import org.junit.platform.engine.discovery.DiscoverySelectors;
+import org.junit.platform.engine.discovery.FilePosition;
 
 /**
  * @since 1.10
@@ -382,7 +383,9 @@ void parseValidFileSelectors(ArgsType type) {
 				() -> assertEquals(List.of(selectFile("foo.txt")), type.parseArgLine("-select-file=foo.txt").discovery.getSelectedFiles()),
 				() -> assertEquals(List.of(selectFile("foo.txt")), type.parseArgLine("--select-file foo.txt").discovery.getSelectedFiles()),
 				() -> assertEquals(List.of(selectFile("foo.txt")), type.parseArgLine("--select-file=foo.txt").discovery.getSelectedFiles()),
-				() -> assertEquals(List.of(selectFile("foo.txt"), selectFile("bar.csv")), type.parseArgLine("-f foo.txt -f bar.csv").discovery.getSelectedFiles())
+				() -> assertEquals(List.of(selectFile("foo.txt"), selectFile("bar.csv")), type.parseArgLine("-f foo.txt -f bar.csv").discovery.getSelectedFiles()),
+				() -> assertEquals(List.of(selectFile("foo.txt", FilePosition.from(5))), type.parseArgLine("-f foo.txt?line=5").discovery.getSelectedFiles()),
+				() -> assertEquals(List.of(selectFile("foo.txt", FilePosition.from(12, 34))), type.parseArgLine("-f foo.txt?line=12&column=34").discovery.getSelectedFiles())
 		);
 		// @formatter:on
 	}
@@ -509,7 +512,9 @@ void parseValidClasspathResourceSelectors(ArgsType type) {
 				() -> assertEquals(List.of(selectClasspathResource("/foo.csv")), type.parseArgLine("-select-resource=/foo.csv").discovery.getSelectedClasspathResources()),
 				() -> assertEquals(List.of(selectClasspathResource("/foo.csv")), type.parseArgLine("--select-resource /foo.csv").discovery.getSelectedClasspathResources()),
 				() -> assertEquals(List.of(selectClasspathResource("/foo.csv")), type.parseArgLine("--select-resource=/foo.csv").discovery.getSelectedClasspathResources()),
-				() -> assertEquals(List.of(selectClasspathResource("/foo.csv"), selectClasspathResource("bar.json")), type.parseArgLine("-r /foo.csv -r bar.json").discovery.getSelectedClasspathResources())
+				() -> assertEquals(List.of(selectClasspathResource("/foo.csv"), selectClasspathResource("bar.json")), type.parseArgLine("-r /foo.csv -r bar.json").discovery.getSelectedClasspathResources()),
+				() -> assertEquals(List.of(selectClasspathResource("/foo.csv", FilePosition.from(5))), type.parseArgLine("-r /foo.csv?line=5").discovery.getSelectedClasspathResources()),
+				() -> assertEquals(List.of(selectClasspathResource("/foo.csv", FilePosition.from(12, 34))), type.parseArgLine("-r /foo.csv?line=12&column=34").discovery.getSelectedClasspathResources())
 		);
 		// @formatter:on
 	}