All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.oracle.bedrock.junit.options.TestClasses Maven / Gradle / Ivy

/*
 * File: TestClasses.java
 *
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * The contents of this file are subject to the terms and conditions of 
 * the Common Development and Distribution License 1.0 (the "License").
 *
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the License by consulting the LICENSE.txt file
 * distributed with this file, or by consulting https://oss.oracle.com/licenses/CDDL
 *
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file LICENSE.txt.
 *
 * MODIFICATIONS:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 */

package com.oracle.bedrock.junit.options;

import com.oracle.bedrock.Option;
import com.oracle.bedrock.Options;
import com.oracle.bedrock.junit.TestClassPredicate;
import com.oracle.bedrock.runtime.java.ClassPath;
import org.junit.runner.Description;
import org.junit.runner.manipulation.Filter;

import java.io.IOException;
import java.io.Serializable;
import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;

/**
 * An option representing a set of test {@link Class}es.
 * 

* Copyright (c) 2016. All Rights Reserved. Oracle Corporation.
* Oracle is a registered trademark of Oracle Corporation and/or its affiliates. * * @author Jonathan Knight */ public abstract class TestClasses implements Option.Collectable, Option, Serializable { /** * The {@link Set} of {@link TestMatcher} to determine the tests * to include in a test run. */ private Set includePatterns = new HashSet<>(); /** * The {@link Set} of {@link TestMatcher} to determine the tests * to exclude from a test run. */ private Set excludePatterns = new HashSet<>(); /** * The {@link Predicate} to use to determine whether a given * {@link Class} is a valid JUnit test class and should be * included in the test run. */ private Predicate> testClassPredicate = new TestClassPredicate(); /** * Obtain the {@link Set} of {@link Class}es to test. *

* The classes returned will be determined by any * include and exclude patterns. * * @return the {@link Set} of {@link Class}es to test */ public abstract Set> resolveTestClasses(); /** * Add the specified regular expression pattern to the set of * patterns to use to determine tests to be included in the * test run. *

* NOTE: The exclude patterns take precedence over the include * patterns, so if a test matches both include and exclude then * it is excluded. * * @param patterns the regular expressions to use to match * tests included in the test run * * @return this {@link TestClasses} instance for method chaining */ public TestClasses include(String... patterns) { if (patterns != null) { for (String pattern : patterns) { if (!pattern.isEmpty()) { includePatterns.add(new TestMatcher(pattern)); } } } return this; } /** * Add the specified regular expression pattern to the set of * patterns to use to determine tests to be excluded in the * test run. *

* NOTE: The exclude patterns take precedence over the include * patterns, so if a test matches both include and exclude then * it is excluded. * * @param patterns the regular expressions to use to match * tests included in the test run * * @return this {@link TestClasses} instance for method chaining */ public TestClasses exclude(String... patterns) { if (patterns != null) { for (String pattern : patterns) { if (!pattern.isEmpty()) { excludePatterns.add(new TestMatcher(pattern)); } } } return this; } /** * Obtain the {@link Filter} instance to be used by the test run * to determine tests that will be executed. * * @return the {@link Filter} instance to be used by the test * run to determine tests that will be executed */ public Filter getTestFilter() { if (includePatterns.isEmpty() && excludePatterns.isEmpty()) { return AlwaysRunFilter.INSTANCE; } return new TestFilter(includePatterns, excludePatterns); } /** * Obtain the {@link Predicate} to use when resolving the list * of classes to include in a test run. * * @return the {@link Predicate} to use when resolving the list * of classes to include in a test run */ protected Predicate> getTestClassPredicate() { if (includePatterns.isEmpty() && excludePatterns.isEmpty()) { return testClassPredicate; } return new IncludeExcludePredicate(testClassPredicate, includePatterns, excludePatterns); } @Override public Class getCollectorClass() { return Tests.class; } /** * Create a {@link TestClasses} option that will resolve test classes * from the specified {@link Class} instances * * @param classes the {@link Class}es to use to resolve test classes * * @return a {@link TestClasses} option that will resolve test classes * from the specified {@link Class}es */ public static TestClasses of(Class... classes) { if (classes.length == 0) { return empty(); } return new SpecificClasses(classes); } /** * Create a {@link TestClasses} option that will resolve test classes * from the specified {@link ClassPath}. * * @param classPath the {@link ClassPath} to use to resolve test classes * * @return a {@link TestClasses} option that will resolve test classes * from the specified {@link ClassPath} */ public static TestClasses from(ClassPath classPath) { if (classPath == null || classPath.isEmpty()) { return empty(); } return new ClassPathClasses(classPath); } /** * Create an empty set of test classes. * * @return an empty set of test classes */ @Options.Default public static TestClasses empty() { return new TestClasses() { @Override public Set> resolveTestClasses() { return Collections.emptySet(); } }; } /** * A JUnit test {@link Filter} that always matches everything. */ public static class AlwaysRunFilter extends Filter { /** * Field description */ public static final AlwaysRunFilter INSTANCE = new AlwaysRunFilter(); @Override public String describe() { return "AlwaysRunFilter"; } @Override public boolean shouldRun(Description description) { return true; } } /** * A {@link TestClasses} instance that resolves test classes * from a specific {@link ClassPath} instance. */ protected static class ClassPathClasses extends TestClasses implements Serializable { /** * The array of class path entries to use to resolve classes. */ private String[] paths; /** * The resolved {@link Set} of test classes. */ private transient Set> classes; /** * Create a {@link ClassPathClasses} instance that will resolve test * classes from the specified {@link ClassPath} instance. * * @param classPath the {@link ClassPath} to use to resolve test classes */ public ClassPathClasses(ClassPath classPath) { paths = new String[classPath.size()]; int index = 0; for (String path : classPath) { paths[index++] = path; } } @Override public Set> resolveTestClasses() { if (classes == null) { synchronized (this) { if (classes == null) { try { Set> testClasses = new HashSet<>(); ClassPath classPath = new ClassPath(paths); URL[] urLs = classPath.getURLs(); List paths = new ArrayList<>(); Predicate> predicate = getTestClassPredicate(); for (URL url : urLs) { paths.add(Paths.get(url.toURI())); } for (Path path : paths) { if (Files.isDirectory(path)) { testClasses.addAll(walkPath(path, predicate)); } else { String fileName = path.getFileName().toString(); if (Files.isRegularFile(path) && ClassPath.isResourceAnArchive(fileName)) { testClasses.addAll(walkFileSystem(FileSystems.newFileSystem(path, null), predicate)); } } } this.classes = testClasses; } catch (Exception e) { throw new RuntimeException(e); } } } } return Collections.unmodifiableSet(classes); } /** * Walk the specified {@link FileSystem} and return all of the {@link Class}es that * match the specified {@link Predicate}. * * @param fileSystem the {@lin FileSystem} to search for matching {@link Class} files * @param predicate the {@link Predicate} to use to match {@link Class} files * * @return the {@link Set} of {@link Class}es from the {@link FileSystem} that match the {@link Predicate} * * @throws IOException if there is an error walking the {@link FileSystem} */ private Set> walkFileSystem(FileSystem fileSystem, Predicate> predicate) throws IOException { Set> testClasses = new HashSet<>(); for (Path path : fileSystem.getRootDirectories()) { testClasses.addAll(walkPath(path, predicate)); } return testClasses; } /** * Walk the specified {@link Path} and return all of the {@link Class}es that * match the specified {@link Predicate}. * * @param path the {@lin Path} to search for matching {@link Class} files * @param predicate the {@link Predicate} to use to match {@link Class} files * * @return the {@link Set} of {@link Class}es from the {@link Path} that match the {@link Predicate} * * @throws IOException if there is an error walking the {@link Path} */ private List> walkPath(Path path, Predicate> predicate) throws IOException { List> testClasses = new ArrayList<>(); Files.walkFileTree(path, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (file.getFileName().toString().endsWith(".class")) { StringBuilder className = new StringBuilder(); Iterator iterator = path.relativize(file).iterator(); while (iterator.hasNext()) { String name = iterator.next().toString(); if (iterator.hasNext()) { className.append(name).append('.'); } else { className.append(name.substring(0, name.length() - 6)); } } try { Class testClass = Class.forName(className.toString()); if (predicate.test(testClass)) { testClasses.add(testClass); } } catch (ClassNotFoundException e) { // ignored - can't load class so do not add it to tests } } return FileVisitResult.CONTINUE; } }); return testClasses; } } /** * A {@link Predicate} that uses an inner predicate as well as this {@link TestClasses} * include and exclude patterns to filter {@link Class} files. */ public static class IncludeExcludePredicate implements Predicate> { /** * The included {@link Set} of {@link TestMatcher}s to evaluate a class. */ private Set includePatterns = new HashSet<>(); /** * The excluded {@link Set} of {@link TestMatcher}s to evaluate a class. */ private Set excludePatterns = new HashSet<>(); /** * The inner {@link Predicate} to use to verify classes. */ private final Predicate> predicate; /** * Create a {@link IncludeExcludePredicate} that will use the specified * {@link Predicate} and include and exclude patters to evaluate classes. * * @param predicate the {@link Predicate} to use to evaluate classes * @param includePatterns the included {@link Set} of {@link TestMatcher}s * to evaluate a class * @param excludePatterns the excluded {@link Set} of {@link TestMatcher}s * to evaluate a class */ protected IncludeExcludePredicate(Predicate> predicate, Set includePatterns, Set excludePatterns) { this.predicate = predicate; this.includePatterns = includePatterns; this.excludePatterns = excludePatterns; } @Override public boolean test(Class testClass) { if (testClass == null) { return false; } if (!predicate.test(testClass)) { return false; } String className = testClass.getCanonicalName(); boolean include = includePatterns.isEmpty(); for (TestMatcher matcher : includePatterns) { if (!matcher.hasClassPattern() || matcher.matches(className)) { include = true; break; } } if (include) { for (TestMatcher matcher : excludePatterns) { if (!matcher.hasMethodPattern() && matcher.matches(className)) { return false; } } return true; } return false; } } /** * A {@link TestClasses} instance that uses a fixed set of {@link Class}es * as the set of tests to execute. */ public static class SpecificClasses extends TestClasses implements Serializable { /** * The {@link Set} of names of test classes. */ private Set classNames; /** * The {@link Set} resolved of test {@link Class}es. */ private transient Set> classes; /** * Create a {@link SpecificClasses} instance with the specified * test {@link Class}es. * * @param classes the test {@link Class}es */ private SpecificClasses(Class... classes) { this.classNames = new HashSet<>(classes.length); for (Class cls : classes) { this.classNames.add(cls.getCanonicalName()); } } @Override public Set> resolveTestClasses() { // have we already resolved the list of classes if (this.classes == null) { synchronized (this) { // No, then synchronize and check again if (this.classes == null) { Set> classes = new HashSet<>(); Predicate> predicate = getTestClassPredicate(); // Add each class from the list that matches the predicate for (String className : classNames) { try { Class testClass = Class.forName(className); if (predicate.test(testClass)) { classes.add(testClass); } } catch (ClassNotFoundException e) { // cannot load the class so it will be skipped } } this.classes = classes; } } } return Collections.unmodifiableSet(classes); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } SpecificClasses that = (SpecificClasses) o; return classNames.equals(that.classNames); } @Override public int hashCode() { return classNames.hashCode(); } @Override public String toString() { return "TestClasses(" + "classNames=" + classNames + ')'; } } /** * A JUnit test {@link Filter} that filters test {@link Class} * names and method names based on the set of included and * excluded {@link TestMatcher}s. */ protected static class TestFilter extends Filter { /** * The {@link Set} of {@link TestMatcher} to determine the tests * to include in a test run. */ private final Set includePatterns; /** * The {@link Set} of {@link TestMatcher} to determine the tests * to exclude from a test run. */ private final Set excludePatterns; /** * Create a {@link TestFilter} that will use the specified include * and exclude patterns. * * @param includePatterns the include patters to use * @param excludePatterns the exclude patters to use */ public TestFilter(Set includePatterns, Set excludePatterns) { this.includePatterns = includePatterns; this.excludePatterns = excludePatterns; } @Override public String describe() { return "TestClasses Filter"; } @Override public boolean shouldRun(Description description) { if (description.isSuite()) { return true; } if (!description.isTest()) { return false; } String name = description.getDisplayName(); String className = null; String methodName = null; if (name != null) { name = name.trim(); if (name.endsWith(")")) { int index = name.lastIndexOf('('); if (index != -1) { className = name.substring(index + 1, name.length() - 1).trim(); methodName = name.substring(0, index).trim(); } } } else { return false; } if (className == null || className.isEmpty() || methodName == null || methodName.isEmpty()) { return false; } boolean included = includePatterns.isEmpty(); for (TestMatcher matcher : includePatterns) { if (matcher.matches(className, methodName)) { included = true; break; } } if (!included) { return false; } for (TestMatcher matcher : excludePatterns) { if (matcher.matches(className, methodName)) { return false; } } return true; } } /** * A class that matches a class and method name * based on a pair of regular expressions. */ protected static class TestMatcher implements Serializable { /** * The regular expression to use to match class names. */ private String classPattern; /** * The regular expression to use to match method names. */ private String methodPattern; /** * Create a {@link TestMatcher} using the specified pattern. *

* If the pattern contains a '#' charater it will be used to * split the pattern where the first part will be the class * name matcher and the second part will be the method name matcher. *

* If the pattern contains no '#' character then the whole pattern * will be used to match class names and method names will not be * filtered, i.e. all method names match. * * @param pattern the pattern to use to match class and method names */ protected TestMatcher(String pattern) { int index = pattern.indexOf('#'); if (index < 0) { classPattern = pattern.trim(); } else if (index == 0) { methodPattern = pattern.substring(1).trim(); } else { classPattern = pattern.substring(0, index).trim(); methodPattern = pattern.substring(index + 1, pattern.length()).trim(); } } /** * Obtain the regular expression that will be used to match test class names. * * @return the regular expression that will be used to match test class names */ public String getClassPattern() { return classPattern; } /** * Obtain the regular expression that will be used to match test method names. * * @return the regular expression that will be used to match test method names */ public String getMethodPattern() { return methodPattern; } /** * Determine whether the specified class name matches this {@link TestMatcher}'s * class name pattern. * * @param className the class name to match * * @return true if the specified class name matches this {@link TestMatcher}'s * class name pattern, otherwise false */ public boolean matches(String className) { return classPattern == null || className.matches(classPattern); } /** * Determine whether the specified class name matches this {@link TestMatcher}'s * class name pattern and specified mathd name matches this {@link TestMatcher}'s * method name pattern. * * @param className the class name to match * @param methodName the method name to match * * @return true if the specified class name and method name matches this {@link TestMatcher}'s * patterns, otherwise false */ public boolean matches(String className, String methodName) { if (classPattern == null || className.matches(classPattern)) { return methodPattern == null || methodName.matches(methodPattern); } return false; } /** * Indicate whether this {@link TestMatcher} has a class pattern. * * @return true if this {@link TestMatcher} has a class pattern */ public boolean hasClassPattern() { return classPattern != null &&!classPattern.isEmpty(); } /** * Indicate whether this {@link TestMatcher} has a method pattern. * * @return true if this {@link TestMatcher} has a method pattern */ public boolean hasMethodPattern() { return methodPattern != null &&!methodPattern.isEmpty(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } TestMatcher that = (TestMatcher) o; if (classPattern != null ? !classPattern.equals(that.classPattern) : that.classPattern != null) { return false; } return methodPattern != null ? methodPattern.equals(that.methodPattern) : that.methodPattern == null; } @Override public int hashCode() { int result = classPattern != null ? classPattern.hashCode() : 0; result = 31 * result + (methodPattern != null ? methodPattern.hashCode() : 0); return result; } @Override public String toString() { return "TestMatcher(" + "classPattern='" + classPattern + '\'' + ", methodPattern='" + methodPattern + '\'' + ')'; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy