/*
* Eoulsan development code
*
* This code may be freely distributed and modified under the
* terms of the GNU Lesser General Public License version 2.1 or
* later and CeCILL-C. This should be distributed with the code.
* If you do not have a copy, see:
*
* http://www.gnu.org/licenses/lgpl-2.1.txt
* http://www.cecill.info/licences/Licence_CeCILL-C_V1-en.txt
*
* Copyright for this code is held jointly by the Genomic platform
* of the Institut de Biologie de l'École normale supérieure and
* the individual authors. These should be listed in @author doc
* comments.
*
* For more information on the Eoulsan project and its aims,
* or to join the Eoulsan Google group, visit the home page
* at:
*
* http://outils.genomique.biologie.ens.fr/eoulsan
*
*/
package fr.ens.biologie.genomique.eoulsan.it;
import static fr.ens.biologie.genomique.eoulsan.EoulsanLogger.getLogger;
import static fr.ens.biologie.genomique.eoulsan.LocalEoulsanRuntime.initEoulsanRuntimeForExternalApp;
import static fr.ens.biologie.genomique.eoulsan.util.FileUtils.checkExistingDirectoryFile;
import static fr.ens.biologie.genomique.eoulsan.util.StringUtils.toTimeHumanReadable;
import static java.nio.file.Files.createSymbolicLink;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import com.google.common.base.Stopwatch;
import fr.ens.biologie.genomique.eoulsan.EoulsanException;
import fr.ens.biologie.genomique.eoulsan.EoulsanRuntimeException;
import fr.ens.biologie.genomique.eoulsan.Globals;
import fr.ens.biologie.genomique.eoulsan.util.ProcessUtils;
/**
* This singleton class survey the execution of a test suite (count the number
* of finished tests and manage the symbolic links in output directory).
* @author Sandrine Perrin
* @since 2.0
*/
public class ITSuite {
private static final String RUNNING_LINK_NAME = "running";
private static final String SUCCEEDED_LINK_NAME = "succeeded";
private static final String FAILED_LINK_NAME = "failed";
private static final String LATEST_LINK_NAME = "latest";
private static final Formatter DATE_FORMATTER = new Formatter().format(
Globals.DEFAULT_LOCALE, "%1$tY%1$tm%1$td_%1$tH%1$tM%1$tS", new Date());
// Singleton
private static ITSuite itSuite;
private final Stopwatch globalTimer = Stopwatch.createStarted();
private final Properties globalsConf;
private final File applicationPath;
private final File outputTestsDirectory;
private final int testsCount;
private final Map<String, File> testsToExecute;
private final List<IT> testsInstance;
private boolean debugEnabled = false;
private int failCount = 0;
private int successCount = 0;
private int testRunningCount = 0;
private int testSkippingCount = 0;
private boolean isFirstTest = true;
private final String loggerPath;
private final File testsDataDirectory;
private final String versionApplication;
private final boolean generateAllExpectedDirectoryTest;
private final boolean generateNewExpectedDirectoryTests;
private final String actionType;
//
// Singleton methods
//
/**
* Initialize an instance of ITSuite.
* @param tests tests count for the execution
* @param globalsConf the globals configuration
* @param applicationPath the application path
* @return the instance of ITSuite object
* @throws EoulsanException if an error occurs when initialize integration
* test
* @throws IOException if an error occurs with a test directories or
* configuration file
*/
public static ITSuite getInstance(final Map<String, File> tests,
final Properties globalsConf, final File applicationPath)
throws IOException, EoulsanException {
if (itSuite == null) {
itSuite = new ITSuite(tests, globalsConf, applicationPath);
return itSuite;
}
throw new EoulsanRuntimeException(
"Cannot create an instance of ITSuite because an instance has already been created.");
}
/**
* Get the instance of ITSuite object.
* @return the instance of ITSuite object
*/
public static ITSuite getInstance() {
if (itSuite != null) {
return itSuite;
}
throw new EoulsanRuntimeException(
"Cannot get an instance of ITSuite class because no instance has been created.");
}
/**
* Creates the symbolic link, if possible create a relative link otherwise a
* absolute link.
* @param linkPath the link path
* @param targetPath the target path
* @return the path the relative link path
* @throws IOException if a path is null or not exist.
*/
public static Path createRelativeOrAbsoluteSymbolicLink(final Path linkPath,
final Path targetPath) throws IOException {
if (linkPath == null) {
throw new IOException(
"Can not be create relative symbolic link, link path is null.");
}
if (targetPath == null) {
throw new IOException(
"Can not be create relative symbolic link, target path is null.");
}
final Path basePath = linkPath.getParent();
try {
// Create relative path on target path
Path pathRelative = basePath.relativize(targetPath);
// Create symbolic link
return createSymbolicLink(linkPath, pathRelative);
} catch (IllegalArgumentException e) {
// Not a Path that can be relativized against this path
// Create a absolute symbolic link
return createSymbolicLink(linkPath, targetPath);
}
}
/**
* Update counter of tests running. If it is the first, create symbolics link.
*/
public void notifyStartTest() {
if (this.isFirstTest) {
createSymbolicLinkToTest();
this.isFirstTest = false;
}
// Count test running
this.testRunningCount++;
}
/**
* Update counter of tests running. If it is the last, update symbolics link
* and close logger.
* @param itResult the it result
*/
public void notifyEndTest(final ITResult itResult) {
if (itResult.isNothingToDo()) {
this.testSkippingCount++;
} else {
// Update counter
if (itResult.isSuccess()) {
this.successCount++;
} else {
this.failCount++;
}
}
// For latest
if (this.testRunningCount == this.testsCount) {
createSymbolicLinkToTest();
endLogger();
}
}
/**
* Execute command line shell to obtain the version name of application to
* test. If fail, it return UNKNOWN.
* @param commandLine command line shell
* @param applicationPath application path to test
* @return version name of application to test
*/
public String retrieveVersionApplication(final String commandLine,
final File applicationPath) {
String version = "UNKNOWN";
if (commandLine == null || commandLine.trim().length() == 0) {
// None command line to retrieve version application set in configuration
// file
return version;
}
try {
// Execute command
final String output = ProcessUtils.execToString(commandLine);
if (output != null && output.trim().length() > 0) {
// Retrieve version
version = output.trim();
}
} catch (final IOException e) {
}
return version;
}
/**
* Create useful symbolic test to the latest and running test in output test
* directory.
*/
private void createSymbolicLinkToTest() {
// Remove old running test link
removeOldLink(RUNNING_LINK_NAME);
// Create running test link
if (this.testRunningCount == 0) {
createNewLink(RUNNING_LINK_NAME);
} else {
// Replace latest by running test link
removeOldLinkAndCreateANewOne(LATEST_LINK_NAME);
if (this.failCount == 0) {
// Update succeed link
removeOldLinkAndCreateANewOne(SUCCEEDED_LINK_NAME);
} else {
// Update failed link
removeOldLinkAndCreateANewOne(FAILED_LINK_NAME);
}
}
}
/**
* Removes the old link and create a new one.
* @param linkName the link name
*/
private void removeOldLinkAndCreateANewOne(final String linkName) {
// Remove old link
removeOldLink(linkName);
// Recreate link
createNewLink(linkName);
}
/**
* Removes the old link.
* @param linkName the link name
*/
private void removeOldLink(final String linkName) {
final Path outputTestsPath =
this.outputTestsDirectory.getParentFile().toPath();
final Path linkPath = new File(outputTestsPath.toFile(), linkName).toPath();
// Remove old link
try {
Files.delete(linkPath);
} catch (IOException e) {
getLogger().warning(
"Unable to delete old " + linkName + " directory link: " + linkPath);
}
}
/**
* Creates the new link.
* @param linkName the link name
*/
private void createNewLink(final String linkName) {
final Path outputTestsPath =
this.outputTestsDirectory.getParentFile().toPath();
// Create the link
final Path linkPath = new File(outputTestsPath.toFile(), linkName).toPath();
try {
createRelativeOrAbsoluteSymbolicLink(linkPath,
this.outputTestsDirectory.toPath());
} catch (IOException e) {
getLogger().warning(
"Unable to create " + linkName + " directory link: " + linkPath);
}
}
/**
* Check validate test, exit configuration file at the root.
* @param tests the tests
* @return all tests can be run
* @throws EoulsanException if none tests valid found
*/
private Map<String, File> checkValidateTest(final Map<String, File> tests)
throws EoulsanException {
// Keep test with test.conf file exit at the root directory
final Map<String, File> validTests = new HashMap<>();
for (final Map.Entry<String, File> entry : tests.entrySet()) {
// Check test.conf file exit
if (new File(entry.getValue(), ITFactory.TEST_CONFIGURATION_FILENAME)
.exists()) {
// Keep test
validTests.put(entry.getKey(), entry.getValue());
}
}
// Check tests found not empty
if (validTests.isEmpty()) {
throw new EoulsanException("None test valide in directory "
+ this.testsDataDirectory.getAbsolutePath());
}
return Collections.unmodifiableMap(validTests);
}
/**
* Initialize the integration test instances.
* @return the list of integration test instances.
* @throws IOException Signals that an I/O exception has occurred, when create
* integration test instances.
* @throws EoulsanException the Eoulsan exception if an error occurs when
* create integration test instances, or none instance has been
* created.
*/
private List<IT> initIT() throws IOException, EoulsanException {
final List<IT> tests = new ArrayList<>();
// Extract sorted tests name
final Set<String> testsName = new TreeSet<>(this.testsToExecute.keySet());
// Parse selected tests
for (final String testName : testsName) {
// Create instance
final IT processIT = new IT(this, this.globalsConf, this.applicationPath,
this.testsDataDirectory, this.outputTestsDirectory, testName);
// Add tests
tests.add(processIT);
}
if (tests.isEmpty()) {
throw new EoulsanException("None integration test instance create.");
}
return Collections.unmodifiableList(tests);
}
/**
* Initialization factory with principal needed directories.
* @throws IOException if a source file doesn't exist
*/
private void init() throws IOException {
// Init logger
initLogger();
// Set source directory for tests to execute
checkExistingDirectoryFile(this.testsDataDirectory, "tests data directory");
getLogger().config(
"Tests data directory: " + this.testsDataDirectory.getAbsolutePath());
// Set output directory
checkExistingDirectoryFile(this.outputTestsDirectory.getParentFile(),
"output data parent directory");
// Set directory contain all tests to execute
getLogger().config("Output tests directory: "
+ this.outputTestsDirectory.getAbsolutePath());
// Create output test directory
if (!this.outputTestsDirectory.mkdir()) {
throw new IOException("Cannot create output tests directory "
+ this.outputTestsDirectory.getAbsolutePath());
}
getLogger().config("Action " + this.actionType);
final File loggerFile = new File(this.loggerPath);
if (loggerFile.exists()) {
// Create a symbolic link in output test directory
createSymbolicLink(
new File(this.outputTestsDirectory, loggerFile.getName()).toPath(),
loggerFile.toPath());
}
}
/**
* Initialize logger.
* @throws IOException if an error occurs while create logger
*/
private void initLogger() throws IOException {
// Remove default logger
getLogger().setLevel(Level.OFF);
// Remove default Handler
getLogger().removeHandler(getLogger().getParent().getHandlers()[0]);
try {
initEoulsanRuntimeForExternalApp();
} catch (final EoulsanException ee) {
ee.printStackTrace();
}
Handler fh = null;
try {
fh = new FileHandler(this.loggerPath);
} catch (final Exception e) {
throw new IOException(e);
}
fh.setFormatter(Globals.LOG_FORMATTER);
getLogger().setLevel(Level.ALL);
// Remove output console
getLogger().setUseParentHandlers(false);
getLogger().addHandler(fh);
getLogger().info(Globals.WELCOME_MSG);
}
/**
* Close log file, add a summary on tests execution and update symbolic link
* in output test directory.
*/
private void endLogger() {
getLogger().info("End of execution for "
+ this.testRunningCount + " integration tests in "
+ toTimeHumanReadable(this.globalTimer.elapsed(TimeUnit.MILLISECONDS)));
// Add summary of tests execution
getLogger().info("RUN : "
+ this.successCount + " succeeded, " + this.failCount + " failed, "
+ this.testSkippingCount + " skipped. "
+ (this.failCount == 0 ? "All tests are OK." : ""));
this.globalTimer.stop();
}
//
// Getter and setter
//
/**
* Get the true if debug mode settings otherwise false.
* @return true if debug mode settings otherwise false.
*/
public boolean isDebugModeEnabled() {
return this.debugEnabled;
}
/**
* Set the debug mode, true if it is demand otherwise false.
* @param debugEnabled true if it is demand otherwise false.
*/
public void setDebugModeEnabled(final boolean debugEnabled) {
this.debugEnabled = debugEnabled;
}
/**
* Checks if is generate all expected directory test.
* @return true, if is generate all expected directory test
*/
public boolean isGenerateAllExpectedDirectoryTest() {
return this.generateAllExpectedDirectoryTest;
}
/**
* Checks if is generate new expected directory tests.
* @return true, if is generate new expected directory tests
*/
public boolean isGenerateNewExpectedDirectoryTests() {
return this.generateNewExpectedDirectoryTests;
}
public String getActionType() {
return this.actionType;
}
/**
* Gets the tests data directory.
* @return the tests data directory
*/
public File getTestsDataDirectory() {
return this.testsDataDirectory;
}
/**
* Gets the tests to execute.
* @return the tests to execute
*/
public Map<String, File> getTestsToExecute() {
return this.testsToExecute;
}
/**
* Gets the tests instance.
* @return the tests instance
*/
public List<IT> getTestsInstance() {
return this.testsInstance;
}
/**
* Gets the tests instance to array.
* @return the tests instance to array
*/
public Object[] getTestsInstanceToArray() {
return this.testsInstance.toArray(new Object[this.testsCount]);
}
/**
* Gets the count test.
* @return the count test
*/
public int getCountTest() {
return this.testsCount;
}
/**
* Gets the output test directory path.
* @return the output test directory path
*/
public String getOutputTestDirectoryPath() {
return this.outputTestsDirectory.getAbsolutePath();
}
//
// Constructor
//
/**
* Private constructor.
* @param tests tests count to run
* @param globalsConf the globals conf
* @param applicationPath the application path
* @throws IOException if an error occurs with a test directory or
* configuration file
* @throws EoulsanException if an error occurs when initialize integration
* test
*/
private ITSuite(final Map<String, File> tests, final Properties globalsConf,
final File applicationPath) throws IOException, EoulsanException {
checkExistingDirectoryFile(applicationPath, "application path");
this.globalsConf = globalsConf;
this.applicationPath = applicationPath;
// Set test data source directory
this.testsDataDirectory = new File(
this.globalsConf.getProperty(ITFactory.TESTS_DIRECTORY_CONF_KEY));
// Retrieve application version test
this.versionApplication = retrieveVersionApplication(
this.globalsConf
.getProperty(ITFactory.COMMAND_TO_GET_APPLICATION_VERSION_CONF_KEY),
this.applicationPath);
// Set logger path
this.loggerPath =
this.globalsConf.getProperty(ITFactory.LOG_DIRECTORY_CONF_KEY)
+ "/" + this.versionApplication + "_" + DATE_FORMATTER.toString()
+ ".log";
// Set test data output directory
this.outputTestsDirectory = new File(
this.globalsConf
.getProperty(ITFactory.OUTPUT_ANALYSIS_DIRECTORY_CONF_KEY),
this.versionApplication + "_" + DATE_FORMATTER.toString());
this.generateAllExpectedDirectoryTest = Boolean.parseBoolean(
globalsConf.getProperty(ITFactory.GENERATE_ALL_EXPECTED_DATA_CONF_KEY));
this.generateNewExpectedDirectoryTests = Boolean.parseBoolean(
globalsConf.getProperty(ITFactory.GENERATE_NEW_EXPECTED_DATA_CONF_KEY));
// Set action required
this.actionType = (this.generateAllExpectedDirectoryTest
|| this.generateNewExpectedDirectoryTests
? (this.generateAllExpectedDirectoryTest
? "regenerate all data expected directories if is is not generate manually "
: "generate all missing data expected directories ")
: "launch tests integration ");
// Initialize ITSuite before create integration test instance
init();
// Select tests to execute
this.testsToExecute = checkValidateTest(tests);
// Init all IT instances
this.testsInstance = initIT();
this.testsCount = this.testsInstance.size();
getLogger().config("Found " + this.testsCount + " tests to execute.");
// Initialize debug mode
setDebugModeEnabled(
Boolean.getBoolean(ITFactory.IT_DEBUG_ENABLE_SYSTEM_KEY));
}
}