Skip to content

Commit

Permalink
Add FilePosition in FileSelector and ClasspathResourceSelector (#2253)
Browse files Browse the repository at this point in the history
This commit adds the possibility to select files and classpath resources with a given `FilePosition` (line and/or column number).

Resolves #2146.
  • Loading branch information
timtebeek authored May 22, 2020
1 parent 00e0f37 commit 375792d
Show file tree
Hide file tree
Showing 9 changed files with 477 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ on GitHub.
* When using `ConsoleLauncher`, explicitly selected classes from `--select-class`
and `--select-method` are now always executed regardless of class name patterns
provided with `--include-classname` or the default class name pattern.

* Support `FilePosition` in `FileSelector` and `ClasspathResourceSelector`.

[[release-notes-5.7.0-M2-junit-jupiter]]
=== JUnit Jupiter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import static org.apiguardian.api.API.Status.STABLE;

import java.util.Objects;
import java.util.Optional;

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

private final String classpathResourceName;
private final FilePosition position;

ClasspathResourceSelector(String classpathResourceName) {
ClasspathResourceSelector(String classpathResourceName, FilePosition position) {
boolean startsWithSlash = classpathResourceName.startsWith("/");
this.classpathResourceName = (startsWithSlash ? classpathResourceName.substring(1) : classpathResourceName);
this.position = position;
}

/**
Expand All @@ -59,6 +62,13 @@ public String getClasspathResourceName() {
return this.classpathResourceName;
}

/**
* Get the selected {@code FilePosition} within the classpath resource.
*/
public Optional<FilePosition> getPosition() {
return Optional.ofNullable(this.position);
}

/**
* @since 1.3
*/
Expand All @@ -72,7 +82,8 @@ public boolean equals(Object o) {
return false;
}
ClasspathResourceSelector that = (ClasspathResourceSelector) o;
return Objects.equals(this.classpathResourceName, that.classpathResourceName);
return Objects.equals(this.classpathResourceName, that.classpathResourceName)
&& Objects.equals(this.position, that.position);
}

/**
Expand All @@ -81,12 +92,13 @@ public boolean equals(Object o) {
@API(status = STABLE, since = "1.3")
@Override
public int hashCode() {
return this.classpathResourceName.hashCode();
return Objects.hash(this.classpathResourceName, this.position);
}

@Override
public String toString() {
return new ToStringBuilder(this).append("classpathResourceName", this.classpathResourceName).toString();
return new ToStringBuilder(this).append("classpathResourceName", this.classpathResourceName).append("position",
this.position).toString();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,12 @@ public static UriSelector selectUri(URI uri) {
* @param path the path to the file to select; never {@code null} or blank
* @see FileSelector
* @see #selectFile(File)
* @see #selectFile(String, FilePosition)
* @see #selectDirectory(String)
* @see #selectDirectory(File)
*/
public static FileSelector selectFile(String path) {
Preconditions.notBlank(path, "File path must not be null or blank");
return new FileSelector(path);
return selectFile(path, null);
}

/**
Expand All @@ -120,15 +120,54 @@ public static FileSelector selectFile(String path) {
* @param file the file to select; never {@code null}
* @see FileSelector
* @see #selectFile(String)
* @see #selectFile(File, FilePosition)
* @see #selectDirectory(String)
* @see #selectDirectory(File)
*/
public static FileSelector selectFile(File file) {
return selectFile(file, null);
}

/**
* Create a {@code FileSelector} for the supplied file path.
*
* <p>This method selects the file using the supplied path <em>as is</em>,
* without verifying if the file exists.
*
* @param path the path to the file to select; never {@code null} or blank
* @param position the position inside the file; may be {@code null}
* @see FileSelector
* @see #selectFile(String)
* @see #selectFile(File, FilePosition)
* @see #selectDirectory(String)
* @see #selectDirectory(File)
*/
public static FileSelector selectFile(String path, FilePosition position) {
Preconditions.notBlank(path, "File path must not be null or blank");
return new FileSelector(path, position);
}

/**
* Create a {@code FileSelector} for the supplied {@linkplain File file}.
*
* <p>This method selects the file in its {@linkplain File#getCanonicalPath()
* canonical} form and throws a {@link PreconditionViolationException} if the
* file does not exist.
*
* @param file the file to select; never {@code null}
* @param position the position inside the file; may be {@code null}
* @see FileSelector
* @see #selectFile(File)
* @see #selectFile(String, FilePosition)
* @see #selectDirectory(String)
* @see #selectDirectory(File)
*/
public static FileSelector selectFile(File file, FilePosition position) {
Preconditions.notNull(file, "File must not be null");
Preconditions.condition(file.isFile(),
() -> String.format("The supplied java.io.File [%s] must represent an existing file", file));
try {
return new FileSelector(file.getCanonicalPath());
return new FileSelector(file.getCanonicalPath(), position);
}
catch (IOException ex) {
throw new PreconditionViolationException("Failed to retrieve canonical path for file: " + file, ex);
Expand Down Expand Up @@ -232,14 +271,44 @@ public static List<ClasspathRootSelector> selectClasspathRoots(Set<Path> classpa
*
* @param classpathResourceName the name of the classpath resource; never
* {@code null} or blank
* @see #selectClasspathResource(String, FilePosition)
* @see ClasspathResourceSelector
* @see ClassLoader#getResource(String)
* @see ClassLoader#getResourceAsStream(String)
* @see ClassLoader#getResources(String)
*/
public static ClasspathResourceSelector selectClasspathResource(String classpathResourceName) {
return selectClasspathResource(classpathResourceName, null);
}

/**
* Create a {@code ClasspathResourceSelector} for the supplied classpath
* resource name.
*
* <p>The name of a <em>classpath resource</em> must follow the semantics
* for resource paths as defined in {@link ClassLoader#getResource(String)}.
*
* <p>If the supplied classpath resource name is prefixed with a slash
* ({@code /}), the slash will be removed.
*
* <p>Since {@linkplain org.junit.platform.engine.TestEngine engines} are not
* expected to modify the classpath, the supplied classpath resource must be
* on the classpath of the
* {@linkplain Thread#getContextClassLoader() context class loader} of the
* {@linkplain Thread thread} that uses the resulting selector.
*
* @param classpathResourceName the name of the classpath resource; never
* {@code null} or blank
* @param position the position inside the classpath resource; may be {@code null}
* @see ClasspathResourceSelector
* @see ClassLoader#getResource(String)
* @see ClassLoader#getResourceAsStream(String)
* @see ClassLoader#getResources(String)
*/
public static ClasspathResourceSelector selectClasspathResource(String classpathResourceName,
FilePosition position) {
Preconditions.notBlank(classpathResourceName, "Classpath resource name must not be null or blank");
return new ClasspathResourceSelector(classpathResourceName);
return new ClasspathResourceSelector(classpathResourceName, position);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
* Copyright 2015-2020 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
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.platform.engine.discovery;

import static org.apiguardian.api.API.Status.STABLE;

import java.io.Serializable;
import java.util.Objects;
import java.util.Optional;

import org.apiguardian.api.API;
import org.junit.platform.commons.logging.Logger;
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;

/**
* Position inside a file represented by {@linkplain #getLine line} and
* {@linkplain #getColumn column} numbers.
*
* @since 1.7
*/
@API(status = STABLE, since = "1.7")
public class FilePosition implements Serializable {

private static final long serialVersionUID = 1L;

private static final Logger logger = LoggerFactory.getLogger(FilePosition.class);

/**
* Create a new {@code FilePosition} using the supplied {@code line} number
* and an undefined column number.
*
* @param line the line number; must be greater than zero
* @return a {@link FilePosition} with the given line number
*/
public static FilePosition from(int line) {
return new FilePosition(line);
}

/**
* Create a new {@code FilePosition} using the supplied {@code line} and
* {@code column} numbers.
*
* @param line the line number; must be greater than zero
* @param column the column number; must be greater than zero
* @return a {@link FilePosition} with the given line and column numbers
*/
public static FilePosition from(int line, int column) {
return new FilePosition(line, column);
}

/**
* Create an optional {@code FilePosition} by parsing the supplied
* {@code query} string.
*
* <p>Examples of valid {@code query} strings:
* <ul>
* <li>{@code "line=23"}</li>
* <li>{@code "line=23&column=42"}</li>
* </ul>
*
* @param query the query string; may be {@code null}
* @return an {@link Optional} containing a {@link FilePosition} with
* the parsed line and column numbers; never {@code null} but potentially
* empty
* @since 1.3
* @see #from(int)
* @see #from(int, int)
*/
public static Optional<FilePosition> 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];
if (line == null && "line".equals(key)) {
line = Integer.valueOf(data[1]);
}
else if (column == null && "column".equals(key)) {
column = Integer.valueOf(data[1]);
}
}

// Already found what we're looking for?
if (line != null && column != null) {
break;
}
}
}
catch (IllegalArgumentException ex) {
logger.debug(ex, () -> "Failed to parse 'line' and/or 'column' from query string: " + 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;

private FilePosition(int line) {
Preconditions.condition(line > 0, "line number must be greater than zero");
this.line = line;
this.column = null;
}

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 = column;
}

/**
* Get the line number of this {@code FilePosition}.
*
* @return the line number
*/
public int getLine() {
return this.line;
}

/**
* Get the column number of this {@code FilePosition}, if available.
*
* @return an {@code Optional} containing the column number; never
* {@code null} but potentially empty
*/
public Optional<Integer> getColumn() {
return Optional.ofNullable(this.column);
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
FilePosition that = (FilePosition) o;
return (this.line == that.line) && Objects.equals(this.column, that.column);
}

@Override
public int hashCode() {
return Objects.hash(this.line, this.column);
}

@Override
public String toString() {
// @formatter:off
return new ToStringBuilder(this)
.append("line", this.line)
.append("column", getColumn().orElse(-1))
.toString();
// @formatter:on
}

}
Loading

0 comments on commit 375792d

Please sign in to comment.