blob: aaaeea1ffdc712137dd529b9cd29495ff6ad9324 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.test;
import java.util.Arrays;
import java.util.Set;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import java.io.File;
import java.net.URL;
import javax.management.JMException;
import org.apache.sis.internal.system.Shutdown;
import org.apache.sis.internal.system.SystemListener;
import org.apache.sis.util.Classes;
import org.junit.AfterClass;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import static org.junit.Assert.*;
/**
* Base class of Apache SIS test suites (except the ones that extend GeoAPI suites).
*
* @author Martin Desruisseaux (Geomatys)
* @since 0.3 (derived from geotk-3.16)
* @version 0.4
* @module
*/
@RunWith(Suite.class)
public abstract strictfp class TestSuite {
/**
* The default set of base classes that all test cases are expected to extends.
* This is the default argument value for {@link #verifyTestList(Class)} method.
*/
private static final Class<?>[] BASE_TEST_CLASSES = {
TestCase.class,
org.opengis.test.TestCase.class
};
/**
* {@code true} for disabling the search for missing tests. This is necessary
* when the test suites are executed from an external project, for example during a
* <a href="https://svn.apache.org/repos/asf/sis/release-test/maven">release test</a>.
*/
static boolean skipCheckForMissingTests;
/**
* Creates a new test suite.
*/
protected TestSuite() {
}
/**
* Verifies that we did not forgot to declare some test classes in the given suite.
* This method scans the directory for {@code *Test.class} files.
*
* <p>This check is disabled if {@link #skipCheckForMissingTests} is {@code true}.</p>
*
* @param suite The suite for which to check for missing tests.
*/
protected static void assertNoMissingTest(final Class<? extends TestSuite> suite) {
if (skipCheckForMissingTests) return;
final ClassLoader loader = suite.getClassLoader();
final URL url = loader.getResource(suite.getName().replace('.', '/') + ".class");
assertNotNull("Test suite class not found.", url);
File root;
try {
root = new File(url.toURI());
} catch (Exception e) { // (URISyntaxException | IllegalArgumentException) on JDK7 branch.
// If not a file, then it is probably an entry in a JAR file.
fail(e.toString());
return;
}
for (File c = new File(suite.getName().replace('.', File.separatorChar)); (c = c.getParentFile()) != null;) {
root = root.getParentFile();
assertNotNull("Unexpected directory structure.", root);
assertEquals("Unexpected directory structure.", c.getName(), root.getName());
}
/*
* At this point, we found the root "org" package. Verifies if we are in the Maven target directory.
* In some IDE configuration, all the ".class" files are in the same directory, in which case the
* verification performed by this method become irrelevant.
*/
File moduleDir = root;
for (int i=0; i<3; i++) {
moduleDir = moduleDir.getParentFile();
if (moduleDir == null) {
return;
}
}
if (!new File(moduleDir, "pom.xml").isFile()) {
return;
}
/*
* Now scan all "*Test.class" in the "target/org" directory and and sub-directories,
* and fail on the first missing test file if any.
*/
List<Class<?>> declared = Arrays.asList(suite.getAnnotation(Suite.SuiteClasses.class).value());
final Set<Class<?>> tests = new HashSet<Class<?>>(declared);
if (tests.size() != declared.size()) {
declared = new ArrayList<Class<?>>(declared);
assertTrue(declared.removeAll(tests));
fail("Classes defined twice in " + suite.getSimpleName() + ": " + declared);
}
removeExistingTests(loader, root, new StringBuilder(120).append(root.getName()), tests);
if (!tests.isEmpty()) {
fail("Classes not found. Are they defined in an other module? " + tests);
}
}
/**
* Ensures that all tests in the given directory and sub-directories exit in the given set.
* This method invokes itself recursively for scanning the sub-directories.
*/
private static void removeExistingTests(final ClassLoader loader, final File directory,
final StringBuilder path, final Set<Class<?>> tests)
{
final int length = path.append('.').length();
for (final File file : directory.listFiles()) {
if (!file.isHidden()) {
final String name = file.getName();
if (!name.startsWith(".")) {
path.append(name);
if (file.isDirectory()) {
removeExistingTests(loader, file, path, tests);
} else {
if (name.endsWith("Test.class")) {
path.setLength(path.length() - 6); // Remove trailing ".class"
final String classname = path.toString();
final Class<?> test;
try {
test = Class.forName(classname, false, loader);
} catch (ClassNotFoundException e) {
fail(e.toString());
return;
}
if (!tests.remove(test)) {
fail("Class " + classname + " is not specified in the test suite.");
}
}
}
path.setLength(length);
}
}
}
}
/**
* Verifies the list of tests before the suite is run.
* This method verifies the following conditions:
*
* <ul>
* <li>Every class shall extend either the SIS {@link TestCase} or the GeoAPI {@link org.opengis.test.TestCase}.</li>
* <li>No class shall be declared twice.</li>
* <li>If a test depends on another test, then the other test shall be before the dependant test.</li>
* </ul>
*
* Subclasses shall invoke this method as below:
*
* {@preformat java
* &#64;BeforeClass
* public static void verifyTestList() {
* assertNoMissingTest(MyTestSuite.class);
* verifyTestList(MyTestSuite.class);
* }
* }
*
* @param suite The suite for which to verify test order.
*/
protected static void verifyTestList(final Class<? extends TestSuite> suite) {
verifyTestList(suite, BASE_TEST_CLASSES);
}
/**
* Same verification than {@link #verifyTestList(Class)}, except that the set of base classes
* is explicitely specified. This method is preferred to {@code verifyTestList(Class)} only in
* the rare cases where some test cases need to extend something else than geoapi-conformance
* or Apache SIS test class.
*
* @param suite The suite for which to verify test order.
* @param baseTestClasses The set of base classes that all test cases are expected to extends.
*/
protected static void verifyTestList(final Class<? extends TestSuite> suite, final Class<?>[] baseTestClasses) {
final Class<?>[] testCases = suite.getAnnotation(Suite.SuiteClasses.class).value();
final Set<Class<?>> done = new HashSet<Class<?>>(testCases.length);
for (final Class<?> testCase : testCases) {
if (!Classes.isAssignableToAny(testCase, baseTestClasses)) {
fail("Class " + testCase.getCanonicalName() + " does not extends TestCase.");
}
final DependsOn dependencies = testCase.getAnnotation(DependsOn.class);
if (dependencies != null) {
for (final Class<?> dependency : dependencies.value()) {
if (!done.contains(dependency)) {
fail("Class " + testCase.getCanonicalName() + " depends on " + dependency.getCanonicalName()
+ ", but the dependency has not been found before the test.");
}
}
}
if (!done.add(testCase)) {
fail("Class " + testCase.getCanonicalName() + " is declared twice.");
}
}
}
/**
* Simulates a module uninstall after all tests. This method will first notify any classpath-dependant
* services that the should clear their cache, then stop the SIS daemon threads. Those operations are
* actually not needed in non-server environment (it is okay to just let the JVM stop by itself), but
* the intend here is to ensure that no exception is thrown.
*
* <p>Since this method stops SIS daemon threads, the SIS library shall not be used anymore after
* this method execution.</p>
*
* @throws JMException If an error occurred during unregistration of the supervisor MBean.
*/
@AfterClass
public static void shutdown() throws JMException {
SystemListener.fireClasspathChanged();
Shutdown.stop(TestSuite.class);
}
}