Skip to content

Commit 375792d

Browse files
authored
Add FilePosition in FileSelector and ClasspathResourceSelector (#2253)
This commit adds the possibility to select files and classpath resources with a given `FilePosition` (line and/or column number). Resolves #2146.
1 parent 00e0f37 commit 375792d

File tree

9 files changed

+477
-19
lines changed

9 files changed

+477
-19
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.7.0-M2.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ on GitHub.
3434
* When using `ConsoleLauncher`, explicitly selected classes from `--select-class`
3535
and `--select-method` are now always executed regardless of class name patterns
3636
provided with `--include-classname` or the default class name pattern.
37-
37+
* Support `FilePosition` in `FileSelector` and `ClasspathResourceSelector`.
3838

3939
[[release-notes-5.7.0-M2-junit-jupiter]]
4040
=== JUnit Jupiter

junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClasspathResourceSelector.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import static org.apiguardian.api.API.Status.STABLE;
1414

1515
import java.util.Objects;
16+
import java.util.Optional;
1617

1718
import org.apiguardian.api.API;
1819
import org.junit.platform.commons.util.ToStringBuilder;
@@ -39,10 +40,12 @@
3940
public class ClasspathResourceSelector implements DiscoverySelector {
4041

4142
private final String classpathResourceName;
43+
private final FilePosition position;
4244

43-
ClasspathResourceSelector(String classpathResourceName) {
45+
ClasspathResourceSelector(String classpathResourceName, FilePosition position) {
4446
boolean startsWithSlash = classpathResourceName.startsWith("/");
4547
this.classpathResourceName = (startsWithSlash ? classpathResourceName.substring(1) : classpathResourceName);
48+
this.position = position;
4649
}
4750

4851
/**
@@ -59,6 +62,13 @@ public String getClasspathResourceName() {
5962
return this.classpathResourceName;
6063
}
6164

65+
/**
66+
* Get the selected {@code FilePosition} within the classpath resource.
67+
*/
68+
public Optional<FilePosition> getPosition() {
69+
return Optional.ofNullable(this.position);
70+
}
71+
6272
/**
6373
* @since 1.3
6474
*/
@@ -72,7 +82,8 @@ public boolean equals(Object o) {
7282
return false;
7383
}
7484
ClasspathResourceSelector that = (ClasspathResourceSelector) o;
75-
return Objects.equals(this.classpathResourceName, that.classpathResourceName);
85+
return Objects.equals(this.classpathResourceName, that.classpathResourceName)
86+
&& Objects.equals(this.position, that.position);
7687
}
7788

7889
/**
@@ -81,12 +92,13 @@ public boolean equals(Object o) {
8192
@API(status = STABLE, since = "1.3")
8293
@Override
8394
public int hashCode() {
84-
return this.classpathResourceName.hashCode();
95+
return Objects.hash(this.classpathResourceName, this.position);
8596
}
8697

8798
@Override
8899
public String toString() {
89-
return new ToStringBuilder(this).append("classpathResourceName", this.classpathResourceName).toString();
100+
return new ToStringBuilder(this).append("classpathResourceName", this.classpathResourceName).append("position",
101+
this.position).toString();
90102
}
91103

92104
}

junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,12 @@ public static UriSelector selectUri(URI uri) {
102102
* @param path the path to the file to select; never {@code null} or blank
103103
* @see FileSelector
104104
* @see #selectFile(File)
105+
* @see #selectFile(String, FilePosition)
105106
* @see #selectDirectory(String)
106107
* @see #selectDirectory(File)
107108
*/
108109
public static FileSelector selectFile(String path) {
109-
Preconditions.notBlank(path, "File path must not be null or blank");
110-
return new FileSelector(path);
110+
return selectFile(path, null);
111111
}
112112

113113
/**
@@ -120,15 +120,54 @@ public static FileSelector selectFile(String path) {
120120
* @param file the file to select; never {@code null}
121121
* @see FileSelector
122122
* @see #selectFile(String)
123+
* @see #selectFile(File, FilePosition)
123124
* @see #selectDirectory(String)
124125
* @see #selectDirectory(File)
125126
*/
126127
public static FileSelector selectFile(File file) {
128+
return selectFile(file, null);
129+
}
130+
131+
/**
132+
* Create a {@code FileSelector} for the supplied file path.
133+
*
134+
* <p>This method selects the file using the supplied path <em>as is</em>,
135+
* without verifying if the file exists.
136+
*
137+
* @param path the path to the file to select; never {@code null} or blank
138+
* @param position the position inside the file; may be {@code null}
139+
* @see FileSelector
140+
* @see #selectFile(String)
141+
* @see #selectFile(File, FilePosition)
142+
* @see #selectDirectory(String)
143+
* @see #selectDirectory(File)
144+
*/
145+
public static FileSelector selectFile(String path, FilePosition position) {
146+
Preconditions.notBlank(path, "File path must not be null or blank");
147+
return new FileSelector(path, position);
148+
}
149+
150+
/**
151+
* Create a {@code FileSelector} for the supplied {@linkplain File file}.
152+
*
153+
* <p>This method selects the file in its {@linkplain File#getCanonicalPath()
154+
* canonical} form and throws a {@link PreconditionViolationException} if the
155+
* file does not exist.
156+
*
157+
* @param file the file to select; never {@code null}
158+
* @param position the position inside the file; may be {@code null}
159+
* @see FileSelector
160+
* @see #selectFile(File)
161+
* @see #selectFile(String, FilePosition)
162+
* @see #selectDirectory(String)
163+
* @see #selectDirectory(File)
164+
*/
165+
public static FileSelector selectFile(File file, FilePosition position) {
127166
Preconditions.notNull(file, "File must not be null");
128167
Preconditions.condition(file.isFile(),
129168
() -> String.format("The supplied java.io.File [%s] must represent an existing file", file));
130169
try {
131-
return new FileSelector(file.getCanonicalPath());
170+
return new FileSelector(file.getCanonicalPath(), position);
132171
}
133172
catch (IOException ex) {
134173
throw new PreconditionViolationException("Failed to retrieve canonical path for file: " + file, ex);
@@ -232,14 +271,44 @@ public static List<ClasspathRootSelector> selectClasspathRoots(Set<Path> classpa
232271
*
233272
* @param classpathResourceName the name of the classpath resource; never
234273
* {@code null} or blank
274+
* @see #selectClasspathResource(String, FilePosition)
235275
* @see ClasspathResourceSelector
236276
* @see ClassLoader#getResource(String)
237277
* @see ClassLoader#getResourceAsStream(String)
238278
* @see ClassLoader#getResources(String)
239279
*/
240280
public static ClasspathResourceSelector selectClasspathResource(String classpathResourceName) {
281+
return selectClasspathResource(classpathResourceName, null);
282+
}
283+
284+
/**
285+
* Create a {@code ClasspathResourceSelector} for the supplied classpath
286+
* resource name.
287+
*
288+
* <p>The name of a <em>classpath resource</em> must follow the semantics
289+
* for resource paths as defined in {@link ClassLoader#getResource(String)}.
290+
*
291+
* <p>If the supplied classpath resource name is prefixed with a slash
292+
* ({@code /}), the slash will be removed.
293+
*
294+
* <p>Since {@linkplain org.junit.platform.engine.TestEngine engines} are not
295+
* expected to modify the classpath, the supplied classpath resource must be
296+
* on the classpath of the
297+
* {@linkplain Thread#getContextClassLoader() context class loader} of the
298+
* {@linkplain Thread thread} that uses the resulting selector.
299+
*
300+
* @param classpathResourceName the name of the classpath resource; never
301+
* {@code null} or blank
302+
* @param position the position inside the classpath resource; may be {@code null}
303+
* @see ClasspathResourceSelector
304+
* @see ClassLoader#getResource(String)
305+
* @see ClassLoader#getResourceAsStream(String)
306+
* @see ClassLoader#getResources(String)
307+
*/
308+
public static ClasspathResourceSelector selectClasspathResource(String classpathResourceName,
309+
FilePosition position) {
241310
Preconditions.notBlank(classpathResourceName, "Classpath resource name must not be null or blank");
242-
return new ClasspathResourceSelector(classpathResourceName);
311+
return new ClasspathResourceSelector(classpathResourceName, position);
243312
}
244313

245314
/**
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* Copyright 2015-2020 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.platform.engine.discovery;
12+
13+
import static org.apiguardian.api.API.Status.STABLE;
14+
15+
import java.io.Serializable;
16+
import java.util.Objects;
17+
import java.util.Optional;
18+
19+
import org.apiguardian.api.API;
20+
import org.junit.platform.commons.logging.Logger;
21+
import org.junit.platform.commons.logging.LoggerFactory;
22+
import org.junit.platform.commons.util.Preconditions;
23+
import org.junit.platform.commons.util.StringUtils;
24+
import org.junit.platform.commons.util.ToStringBuilder;
25+
26+
/**
27+
* Position inside a file represented by {@linkplain #getLine line} and
28+
* {@linkplain #getColumn column} numbers.
29+
*
30+
* @since 1.7
31+
*/
32+
@API(status = STABLE, since = "1.7")
33+
public class FilePosition implements Serializable {
34+
35+
private static final long serialVersionUID = 1L;
36+
37+
private static final Logger logger = LoggerFactory.getLogger(FilePosition.class);
38+
39+
/**
40+
* Create a new {@code FilePosition} using the supplied {@code line} number
41+
* and an undefined column number.
42+
*
43+
* @param line the line number; must be greater than zero
44+
* @return a {@link FilePosition} with the given line number
45+
*/
46+
public static FilePosition from(int line) {
47+
return new FilePosition(line);
48+
}
49+
50+
/**
51+
* Create a new {@code FilePosition} using the supplied {@code line} and
52+
* {@code column} numbers.
53+
*
54+
* @param line the line number; must be greater than zero
55+
* @param column the column number; must be greater than zero
56+
* @return a {@link FilePosition} with the given line and column numbers
57+
*/
58+
public static FilePosition from(int line, int column) {
59+
return new FilePosition(line, column);
60+
}
61+
62+
/**
63+
* Create an optional {@code FilePosition} by parsing the supplied
64+
* {@code query} string.
65+
*
66+
* <p>Examples of valid {@code query} strings:
67+
* <ul>
68+
* <li>{@code "line=23"}</li>
69+
* <li>{@code "line=23&column=42"}</li>
70+
* </ul>
71+
*
72+
* @param query the query string; may be {@code null}
73+
* @return an {@link Optional} containing a {@link FilePosition} with
74+
* the parsed line and column numbers; never {@code null} but potentially
75+
* empty
76+
* @since 1.3
77+
* @see #from(int)
78+
* @see #from(int, int)
79+
*/
80+
public static Optional<FilePosition> fromQuery(String query) {
81+
FilePosition result = null;
82+
Integer line = null;
83+
Integer column = null;
84+
if (StringUtils.isNotBlank(query)) {
85+
try {
86+
for (String pair : query.split("&")) {
87+
String[] data = pair.split("=");
88+
if (data.length == 2) {
89+
String key = data[0];
90+
if (line == null && "line".equals(key)) {
91+
line = Integer.valueOf(data[1]);
92+
}
93+
else if (column == null && "column".equals(key)) {
94+
column = Integer.valueOf(data[1]);
95+
}
96+
}
97+
98+
// Already found what we're looking for?
99+
if (line != null && column != null) {
100+
break;
101+
}
102+
}
103+
}
104+
catch (IllegalArgumentException ex) {
105+
logger.debug(ex, () -> "Failed to parse 'line' and/or 'column' from query string: " + query);
106+
// fall-through and continue
107+
}
108+
109+
if (line != null) {
110+
result = column == null ? new FilePosition(line) : new FilePosition(line, column);
111+
}
112+
}
113+
return Optional.ofNullable(result);
114+
}
115+
116+
private final int line;
117+
private final Integer column;
118+
119+
private FilePosition(int line) {
120+
Preconditions.condition(line > 0, "line number must be greater than zero");
121+
this.line = line;
122+
this.column = null;
123+
}
124+
125+
private FilePosition(int line, int column) {
126+
Preconditions.condition(line > 0, "line number must be greater than zero");
127+
Preconditions.condition(column > 0, "column number must be greater than zero");
128+
this.line = line;
129+
this.column = column;
130+
}
131+
132+
/**
133+
* Get the line number of this {@code FilePosition}.
134+
*
135+
* @return the line number
136+
*/
137+
public int getLine() {
138+
return this.line;
139+
}
140+
141+
/**
142+
* Get the column number of this {@code FilePosition}, if available.
143+
*
144+
* @return an {@code Optional} containing the column number; never
145+
* {@code null} but potentially empty
146+
*/
147+
public Optional<Integer> getColumn() {
148+
return Optional.ofNullable(this.column);
149+
}
150+
151+
@Override
152+
public boolean equals(Object o) {
153+
if (this == o) {
154+
return true;
155+
}
156+
if (o == null || getClass() != o.getClass()) {
157+
return false;
158+
}
159+
FilePosition that = (FilePosition) o;
160+
return (this.line == that.line) && Objects.equals(this.column, that.column);
161+
}
162+
163+
@Override
164+
public int hashCode() {
165+
return Objects.hash(this.line, this.column);
166+
}
167+
168+
@Override
169+
public String toString() {
170+
// @formatter:off
171+
return new ToStringBuilder(this)
172+
.append("line", this.line)
173+
.append("column", getColumn().orElse(-1))
174+
.toString();
175+
// @formatter:on
176+
}
177+
178+
}

0 commit comments

Comments
 (0)