/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2005-2010, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.test;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import junit.framework.TestCase;
import junit.framework.TestResult;
/**
* Test support for test cases that require an "online" resource, such as an
* external server or database.
* <p>
* Online tests work off of a "fixture". A fixture is a properties file which
* defines connection parameters for some remote service. Each online test case
* must define the id of the fixture is uses with {@link #getFixtureId()}.
* </p>
* <p>
* Fixtures are stored under the users home directory, under the "<code>.geotools</code>"
* directory. Dots "." in the fixture id represent a subdirectory path under this
* configuration file directory. For example, a fixture id <code>a.b.foo</code> would be
* resolved to <code>.geotools/a/b/foo.properties<code>.
* </p>
* <p>
* In the event that a fixture does not exist, the test case is
* aborted.
* </p>
* <p>
* Online tests connect to remote / online resources. Test cases should do all
* connection / disconnection in the {@link #connect} and {@link #disconnect()}
* methods.
* </p>
*
* <p>
* The default behaviour of this class is that if {@link #connect()} throws an exception, the test
* suite is disabled, causing each test to pass without being run. In addition, exceptions thrown by
* {@link #disconnect()} are ignored. This behaviour allows tests to be robust against transient
* outages of online resources, but also means that local software failures in {@link #connect()} or
* {@link #disconnect()} will be silent.
* </p>
*
* <p>
* To have exceptions thrown by {@link #connect()} and {@link #disconnect()} cause tests to fail,
* set <code>skip.on.failure=false</code> in the fixture property file. This restores the
* traditional behaviour of unit tests, that is, that exceptions cause unit tests to fail.
* </p>
*
* @since 2.4
*
* @source $URL$
* @version $Id$
* @author Justin Deoliveira, The Open Planning Project
* @author Ben Caradoc-Davies, CSIRO Earth Science and Resource Engineering
*/
public abstract class OnlineTestCase extends TestCase {
/**
* System property set to totally disable any online tests
*/
public static final String ONLINE_TEST_PROFILE = "onlineTestProfile";
/**
* The key in the test fixture property file used to set the behaviour of the online test if
* {@link #connect()} fails.
*/
public static final String SKIP_ON_FAILURE_KEY = "skip.on.failure";
/**
* The default value used for {@link #SKIP_ON_FAILURE_KEY} if it is not present.
*/
public static final String SKIP_ON_FAILURE_DEFAULT = "true";
/**
* A static map which tracks which fixtures are offline. This prevents continually trying to
* run a test when an external resource is offline.
*/
protected static Map<String,Boolean> online = new HashMap<String,Boolean>();
/**
* A static map which tracks which fixture files can not be found. This prevents
* continually looking up the file and reporting it not found to the user.
*/
protected static Map<String,Boolean> found = new HashMap<String,Boolean>();
/**
* The test fixture, {@code null} if the fixture is not available.
*/
protected Properties fixture;
/**
* Flag that determines effect of exceptions in connect/disconnect. If true (the default),
* exceptions in connect cause the the test to be disabled, and exceptions in disconnect to be
* ignored. If false, exceptions will be rethrown, and cause the test to fail.
*/
protected boolean skipOnFailure = true;
/**
* Override which checks if the fixture is available. If not the test is not
* executed.
*/
@Override
public void run(TestResult result) {
if (checkAvailable()) {
super.run(result);
}
}
/**
* Check whether the fixture is available. This method also loads the configuration if present,
* and tests the connection using {@link #isOnline()}.
*
* @return true if fixture is available for use
*/
boolean checkAvailable() {
configureFixture();
if (fixture == null) {
return false;
} else {
String fixtureId = getFixtureId();
// do an online/offline check
Boolean available = (Boolean) online.get(fixtureId);
if (available == null || available.booleanValue()) {
// test the connection
try {
available = isOnline();
} catch (Throwable t) {
System.out.println("Skipping " + fixtureId
+ " tests, resources not available: " + t.getMessage());
t.printStackTrace();
available = Boolean.FALSE;
}
online.put(fixtureId, available);
}
return available;
}
}
/**
* Load fixture configuration. Create example if absent.
*/
private void configureFixture() {
if (fixture == null) {
String fixtureId = getFixtureId();
if (fixtureId == null) {
return; // not available (turn test off)
}
try {
// load the fixture
File base = FixtureUtilities.getFixtureDirectory();
// look for a "profile", these can be used to group related fixtures
String profile = System.getProperty(ONLINE_TEST_PROFILE);
if (profile != null && !"".equals(profile)) {
base = new File(base, profile);
}
File fixtureFile = FixtureUtilities.getFixtureFile(base, fixtureId);
Boolean exists = found.get(fixtureFile.getCanonicalPath());
if (exists == null || exists.booleanValue()) {
if (fixtureFile.exists()) {
fixture = FixtureUtilities.loadProperties(fixtureFile);
found.put(fixtureFile.getCanonicalPath(), true);
} else {
// no fixture file, if no profile was specified write out a template
// fixture using the offline fixture properties
if (profile == null) {
Properties exampleFixture = createExampleFixture();
if (exampleFixture != null) {
File exFixtureFile = new File(fixtureFile.getAbsolutePath()
+ ".example");
if (!exFixtureFile.exists()) {
createExampleFixture(exFixtureFile, exampleFixture);
}
}
}
found.put(fixtureFile.getCanonicalPath(), false);
}
}
if (fixture == null) {
fixture = createOfflineFixture();
}
if (fixture == null && exists == null) {
// only report if exists == null since it means that this is
// the first time trying to load the fixture
FixtureUtilities.printSkipNotice(fixtureId, fixtureFile);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
void createExampleFixture(File exFixtureFile, Properties exampleFixture) {
try {
exFixtureFile.getParentFile().mkdirs();
exFixtureFile.createNewFile();
FileOutputStream fout = new FileOutputStream(exFixtureFile);
exampleFixture.store(fout, "This is an example fixture. Update the " +
"values and remove the .example suffix to enable the test");
fout.flush();
fout.close();
System.out.println("Wrote example fixture file to " + exFixtureFile);
}
catch(IOException ioe) {
System.out.println("Unable to write out example fixture " + exFixtureFile);
ioe.printStackTrace();
}
}
/**
* Loads the test fixture for the test case.
* <p>
* The fixture id is obtained via {@link #getFixtureId()}.
* </p>
*/
@Override
protected final void setUp() throws Exception {
super.setUp();
setUpInternal();
skipOnFailure = Boolean.parseBoolean(fixture.getProperty(SKIP_ON_FAILURE_KEY,
SKIP_ON_FAILURE_DEFAULT));
// call the setUp template method
try {
connect();
} catch (Exception e) {
if (skipOnFailure) {
// disable the test
fixture = null;
// leave some trace of the swallowed exception
e.printStackTrace();
} else {
// do not swallow the exception
throw e;
}
}
}
/**
* Method for subclasses to latch onto the setup phase.
*/
protected void setUpInternal() throws Exception {}
/**
* Tear down method for test, calls through to {@link #disconnect()} if the
* test is active.
*/
@Override
protected final void tearDown() throws Exception {
tearDownInternal();
if (fixture != null) {
try {
disconnect();
} catch (Exception e) {
if (skipOnFailure) {
// do nothing
} else {
throw e;
}
}
}
}
/**
* Method for subclasses to latch onto the teardown phase.
*/
protected void tearDownInternal() throws Exception {}
/**
* Tests if external resources needed to run the tests are online.
* <p>
* This method can return false to indicate the online resources are not up, or can simply
* throw an exception.
* </p>
* @return True if external resources are online, otherwise false.
* @throws Exception Any errors that occur determining if online resources are available.
*/
protected boolean isOnline() throws Exception {
return true;
}
/**
* Connection method, called from {@link #setUp()}.
* <p>
* Subclasses should do all initialization / connection here. In the event
* of a connection not being available, this method should throw an
* exception to abort the test case.
* </p>
*
* @throws Exception if the connection failed.
*/
protected void connect() throws Exception {
}
/**
* Disconnection method, called from {@link #tearDown()}.
* <p>
* Subclasses should do all cleanup here.
* </p>
*
* @throws Exception if the disconnection failed.
*/
protected void disconnect() throws Exception {
}
/**
* Allows tests to create an offline fixture in cases where the user has not
* specified an explicit fixture for the test.
* <p>
* Note, that this should method should on be implemented if the test case
* is created of creating a fixture which relies soley on embedded or offline
* resources. It should not reference any external or online resources as it
* prevents the user from running offline.
* </p>
*/
protected Properties createOfflineFixture() {
return null;
}
/**
* Allows test to create a sample fixture for users.
* <p>
* If this method returns a value the first time a fixture is looked up and not
* found this method will be called to create a fixture file with teh same id, but
* suffixed with .template.
* </p>
*/
protected Properties createExampleFixture() {
return null;
}
/**
* The fixture id for the test case.
* <p>
* This name is hierarchical, similar to a java package name. Example:
* {@code "postgis.demo_bc"}.
* </p>
*
* @return The fixture id.
*/
protected abstract String getFixtureId();
}