/*
* 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 com.google.common.io.Files.newReader;
import static fr.ens.biologie.genomique.eoulsan.util.FileUtils.checkExistingDirectoryFile;
import static fr.ens.biologie.genomique.eoulsan.util.FileUtils.checkExistingStandardFile;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.compress.utils.Charsets;
import org.testng.annotations.Factory;
import fr.ens.biologie.genomique.eoulsan.EoulsanException;
import fr.ens.biologie.genomique.eoulsan.Globals;
import fr.ens.biologie.genomique.eoulsan.util.ProcessUtils;
/**
* This class launch integration test with Testng.
* @since 2.0
* @author Laurent Jourdren
* @author Sandrine Perrin
*/
public class ITFactory {
// Java system properties keys used for integration tests
public static final String IT_CONF_PATH_SYSTEM_KEY = "it.conf.path";
public static final String IT_TEST_LIST_PATH_SYSTEM_KEY = "it.test.list.path";
public static final String IT_TEST_SYSTEM_KEY = "it.test.name";
public static final String IT_GENERATE_ALL_EXPECTED_DATA_SYSTEM_KEY =
"it.generate.all.expected.data";
public static final String IT_GENERATE_NEW_EXPECTED_DATA_SYSTEM_KEY =
"it.generate.new.expected.data";
public static final String IT_APPLICATION_PATH_KEY_SYSTEM_KEY =
"it.application.path";
public static final String IT_DEBUG_ENABLE_SYSTEM_KEY = "it.debug.enable";
/** Set test output directory to replace value in configuration file. */
public static final String IT_OUTPUT_DIR_SYSTEM_KEY = "it.output.dir";
// Configuration properties keys
static final String TESTS_DIRECTORY_CONF_KEY = "tests.directory";
static final String OUTPUT_ANALYSIS_DIRECTORY_CONF_KEY =
"output.analysis.directory";
static final String LOG_DIRECTORY_CONF_KEY = "log.directory";
static final String PRE_TEST_SCRIPT_CONF_KEY = "pre.test.script";
static final String POST_TEST_SCRIPT_CONF_KEY = "post.test.script";
static final String GENERATE_ALL_EXPECTED_DATA_CONF_KEY =
"generate.all.expected.data";
static final String GENERATE_NEW_EXPECTED_DATA_CONF_KEY =
"generate.new.expected.data";
static final String DESCRIPTION_CONF_KEY = "description";
static final String COMMAND_TO_LAUNCH_APPLICATION_CONF_KEY =
"command.to.launch.application";
static final String COMMAND_TO_GENERATE_MANUALLY_CONF_KEY =
"command.to.generate.manually";
static final String COMMAND_TO_GET_APPLICATION_VERSION_CONF_KEY =
"command.to.get.application.version";
static final String INCLUDE_CONF_KEY = "include";
// Configure to delete file matching with patterns if IT succeeded
static final String SUCCESS_IT_DELETE_FILE_CONF_KEY =
"success.it.delete.file";
// Default value for key configuration success.it.delete.file
private static final String SUCCESS_IT_DELETE_FILE_DEFAULT_VALUE = "false";
/** Patterns */
static final String FILE_TO_COMPARE_PATTERNS_CONF_KEY = "files.to.compare";
static final String EXCLUDE_TO_COMPARE_PATTERNS_CONF_KEY =
"excluded.files.to.compare";
static final String CHECK_LENGTH_FILE_PATTERNS_CONF_KEY =
"files.to.check.length";
static final String CHECK_EXISTENCE_FILE_PATTERNS_CONF_KEY =
"files.to.check.existences";
static final String CHECK_ABSENCE_FILE_PATTERNS_CONF_KEY =
"files.to.check.absence";
static final String FILE_TO_REMOVE_CONF_KEY = "files.to.remove";
static final String MANUAL_GENERATION_EXPECTED_DATA_CONF_KEY =
"manual.generation.expected.data";
static final String RUNTIME_IT_MAXIMUM_KEY = "runtime.test.maximum";
static final String PRETREATMENT_GLOBAL_SCRIPT_KEY = "pre.global.script";
static final String POSTTREATMENT_GLOBAL_SCRIPT_KEY = "post.global.script";
static final String APPLICATION_PATH_VARIABLE = "application.path";
static final String TEST_CONFIGURATION_FILENAME = "test.conf";
// Runtime maximum for a test beyond stop execution, in minutes
static final int RUNTIME_IT_MAXIMUM_DEFAULT = 1;
private static final Properties CONSTANTS = initConstants();
private final Properties globalsConf;
private final File applicationPath;
// File with tests name to execute
private final File selectedTestsFile;
private final String selectedTest;
private final File testsDataDirectory;
private final Map<String, File> testsDirectoryFoundToExecute;
/**
* Create all instance for integrated tests.
* @return array object from integrated tests
*/
@Factory
public final Object[] createInstances() {
// If no test configuration path defined, do nothing
if (this.applicationPath == null) {
return new Object[0];
}
// Set the default local for all the application
Globals.setDefaultLocale();
try {
final int testsCount = ITSuite.getInstance().getCountTest();
if (testsCount == 0) {
return new Object[0];
}
// Return all tests
return ITSuite.getInstance().getTestsInstanceToArray();
} catch (final Throwable e) {
System.err.println(e.getMessage());
}
// Return none test
return new Object[0];
}
//
// Methods to collect tests
//
/**
* Collect all tests to launch from parameter command : in one case all tests
* present in output test directory, in other case from a list with all name
* test directory. For each, it checks the file configuration 'test.txt'.
* @return collection of test directories
* @throws EoulsanException if an error occurs while create instance for each
* test.
* @throws IOException if the source file doesn't exist
*/
private Map<String, File> collectTestsDirectoryToExecute()
throws EoulsanException, IOException {
// final List<IT> tests = new ArrayList<>();
final Map<String, File> result = new HashMap<>();
final List<File> testsToExecuteDirectories = new ArrayList<>();
// Collect tests from a file with names tests
testsToExecuteDirectories.addAll(readTestListFile());
// Add the selected test if set
if (this.selectedTest != null) {
testsToExecuteDirectories
.add(new File(this.testsDataDirectory, this.selectedTest));
}
// If no test was defined by user use all the existing tests
final File[] files = this.testsDataDirectory.listFiles();
if (files != null && testsToExecuteDirectories.isEmpty()) {
testsToExecuteDirectories.addAll(Arrays.asList(files));
}
if (testsToExecuteDirectories.size() == 0) {
throw new EoulsanException("None test directory found in "
+ this.testsDataDirectory.getAbsolutePath());
}
// Build map
for (final File testDirectory : testsToExecuteDirectories) {
// Ignore file
if (testDirectory.isFile()) {
continue;
}
checkExistingDirectoryFile(testDirectory, "test directory");
if (!new File(testDirectory, TEST_CONFIGURATION_FILENAME).exists()) {
continue;
}
result.put(testDirectory.getName(), testDirectory);
}
return Collections.unmodifiableMap(result);
}
/**
* Collect tests to launch from text files with name tests.
* @return list all directories test found
* @throws IOException if an error occurs while read file
*/
private List<File> readTestListFile() throws IOException {
final List<File> result = new ArrayList<>();
if (this.selectedTestsFile == null) {
return Collections.emptyList();
}
checkExistingStandardFile(this.selectedTestsFile, "selected tests file");
final BufferedReader br =
new BufferedReader(newReader(this.selectedTestsFile,
Charsets.toCharset(Globals.DEFAULT_FILE_ENCODING)));
String nameTest;
while ((nameTest = br.readLine()) != null) {
// Skip commentary
if (nameTest.startsWith("#") || nameTest.isEmpty()) {
continue;
}
result.add(new File(this.testsDataDirectory, nameTest.trim()));
}
// Close buffer
br.close();
return result;
}
//
// Methods to load and read configuration and properties
//
/**
* Initialize the constants values.
* @return a map with the constants
*/
private static Properties initConstants() {
final Properties constants = new Properties();
// Add java properties
for (final Map.Entry<Object, Object> e : System.getProperties()
.entrySet()) {
constants.put(e.getKey(), e.getValue());
}
// Add environment properties
for (final Map.Entry<String, String> e : System.getenv().entrySet()) {
constants.put(e.getKey(), e.getValue());
}
return constants;
}
/**
* Load configuration file in properties object.
* @param configurationFile configuration file
* @return properties
* @throws IOException if an error occurs when reading file.
* @throws EoulsanException if an error occurs evaluate value property.
*/
private static Properties loadProperties(final File configurationFile)
throws IOException, EoulsanException {
// TODO Replace properties by treeMap to use constant in set environment
// variables
final Properties rawProps = new Properties();
final Properties props;
checkExistingStandardFile(configurationFile, "test configuration file");
// Load configuration file
rawProps.load(newReader(configurationFile,
Charsets.toCharset(Globals.DEFAULT_FILE_ENCODING)));
props = evaluateProperties(rawProps);
// Check include
final String includeOption = props.getProperty(INCLUDE_CONF_KEY);
if (includeOption != null) {
// Check configuration file
final File otherConfigurationFile = new File(includeOption);
checkExistingStandardFile(otherConfigurationFile,
"configuration file doesn't exist");
// Load configuration in global configuration
final Properties rawPropsIncludedConfigurationFile = new Properties();
rawPropsIncludedConfigurationFile.load(newReader(otherConfigurationFile,
Charsets.toCharset(Globals.DEFAULT_FILE_ENCODING)));
final Properties newProps =
evaluateProperties(rawPropsIncludedConfigurationFile);
for (final String propertyName : newProps.stringPropertyNames()) {
// No overwrite property from includes file configuration
if (props.containsKey(propertyName)) {
continue;
}
props.put(propertyName, newProps.getProperty(propertyName));
}
}
// Add default value
addDefaultProperties(props);
return props;
}
/**
* Adds the default properties if does not exist in the configuration file.
* @param props the props
*/
private static void addDefaultProperties(Properties props) {
// Particular case delete files
if (props.getProperty(SUCCESS_IT_DELETE_FILE_CONF_KEY) == null) {
// Set default value
props.put(SUCCESS_IT_DELETE_FILE_CONF_KEY,
SUCCESS_IT_DELETE_FILE_DEFAULT_VALUE);
}
// Add default runtime test duration
if (props.getProperty(RUNTIME_IT_MAXIMUM_KEY) == null) {
props.put(RUNTIME_IT_MAXIMUM_KEY, "" + RUNTIME_IT_MAXIMUM_DEFAULT);
}
}
/**
* Evaluate properties.
* @param rawProps the raw props
* @return the properties
* @throws EoulsanException if the evaluation expression from value failed.
*/
private static Properties evaluateProperties(final Properties rawProps)
throws EoulsanException {
final Properties props = new Properties();
final int pos = IT.PREFIX_ENV_VAR.length();
// Extract environment variable
for (final String propertyName : rawProps.stringPropertyNames()) {
if (propertyName.startsWith(IT.PREFIX_ENV_VAR)) {
// Evaluate property
final String evalPropValue =
evaluateExpressions(rawProps.getProperty(propertyName), true);
// Put in constants map
CONSTANTS.put(propertyName.substring(pos), evalPropValue);
}
}
// Evaluate property
for (final String propertyName : rawProps.stringPropertyNames()) {
final String propertyValue =
evaluateExpressions(rawProps.getProperty(propertyName), true);
// Set property
props.setProperty(propertyName, propertyValue);
}
return props;
}
/**
* Evaluate expression in a string.
* @param s string in witch expression must be replaced
* @param allowExec allow execution of code
* @return a string with expression evaluated
* @throws EoulsanException if an error occurs while parsing the string or
* executing an expression
*/
static String evaluateExpressions(final String s, final boolean allowExec)
throws EoulsanException {
if (s == null) {
return null;
}
final StringBuilder result = new StringBuilder();
final int len = s.length();
for (int i = 0; i < len; i++) {
final int c0 = s.codePointAt(i);
// Variable substitution
if (c0 == '$' && i + 1 < len) {
final int c1 = s.codePointAt(i + 1);
if (c1 == '{') {
final String expr = subStr(s, i + 2, '}');
final String trimmedExpr = expr.trim();
if (CONSTANTS.containsKey(trimmedExpr)) {
result.append(CONSTANTS.get(trimmedExpr));
}
i += expr.length() + 2;
continue;
}
}
// Command substitution
if (c0 == '`' && allowExec) {
final String expr = subStr(s, i + 1, '`');
try {
final String r =
ProcessUtils.execToString(evaluateExpressions(expr, false));
// remove last '\n' in the result
if (!r.isEmpty() && r.charAt(r.length() - 1) == '\n') {
result.append(r.substring(0, r.length() - 1));
} else {
result.append(r);
}
} catch (final IOException e) {
throw new EoulsanException(
"Error while evaluating expression \"" + expr + "\"", e);
}
i += expr.length() + 1;
continue;
}
result.appendCodePoint(c0);
}
return result.toString();
}
private static String subStr(final String s, final int beginIndex,
final int charPoint) throws EoulsanException {
final int endIndex = s.indexOf(charPoint, beginIndex);
if (endIndex == -1) {
throw new EoulsanException(
"Unexpected end of expression in \"" + s + "\"");
}
return s.substring(beginIndex, endIndex);
}
//
// Other methods
//
/**
* Adds the parameter command line in configuration.
* @throws EoulsanException occurs if the it output directory path is invalid
* (to a file or parent directory not exists).
* @throws IOException occurs if it can not be create the directory.
*/
private void addParameterCommandLineInConfiguration()
throws EoulsanException, IOException {
if (this.globalsConf == null) {
throw new EoulsanException(
"Configuration file from integrated test has not be loaded. "
+ "Can not add parameter command line.");
}
// Load command line properties
// Command generate all expected directories test
this.globalsConf.setProperty(GENERATE_ALL_EXPECTED_DATA_CONF_KEY,
getBooleanFromSystemProperty(IT_GENERATE_ALL_EXPECTED_DATA_SYSTEM_KEY)
.toString());
// Command generate new expected directories test
this.globalsConf.setProperty(GENERATE_NEW_EXPECTED_DATA_CONF_KEY,
getBooleanFromSystemProperty(IT_GENERATE_NEW_EXPECTED_DATA_SYSTEM_KEY)
.toString());
// If exist in command line, replace output directory from configuration
final File itOutputDirectoryFromCommandLine =
getFileFromSystemProperty(IT_OUTPUT_DIR_SYSTEM_KEY);
if (itOutputDirectoryFromCommandLine != null) {
checkDirectoryFileAndCreateIfNotExist(itOutputDirectoryFromCommandLine);
// Replace value from configuration file
this.globalsConf.put(OUTPUT_ANALYSIS_DIRECTORY_CONF_KEY,
itOutputDirectoryFromCommandLine.getAbsolutePath());
}
}
/**
* Check directory file and create if not exist.
* @param dir the dir
* @throws EoulsanException occurs if the it output directory path is invalid
* (to a file or parent directory not exists).
* @throws IOException occurs if it can not be create the directory.
*/
private void checkDirectoryFileAndCreateIfNotExist(File dir)
throws EoulsanException, IOException {
// Check path exist
if (dir.isDirectory()) {
return;
}
if (dir.exists() && dir.isFile()) {
throw new EoulsanException(
"The it output directory is a file not a directory "
+ dir.getAbsolutePath());
}
// Check parent directory exist
final File parentDir = dir.getParentFile();
if (!parentDir.isDirectory()) {
throw new EoulsanException("Can not create it output directory ("
+ dir.getName() + "), parent directory doesn't exist "
+ parentDir.getAbsolutePath());
}
// Check create directory
if (!dir.mkdir()) {
throw new IOException(
"Fail to create it output directory set in command line "
+ dir.getAbsolutePath());
}
}
/**
* Get a File object from a Java System property.
* @param property the key of the property to get
* @return a File object or null if the property does not exists
*/
private static File getFileFromSystemProperty(final String property) {
if (property == null) {
return null;
}
final String value = System.getProperty(property);
if (value == null) {
return null;
}
return new File(value);
}
/**
* Get a Boolean object from a Java System property.
* @param property the key of the property to get
* @return a Boolean object or false if the property does not exists
*/
private static Boolean getBooleanFromSystemProperty(final String property) {
return (property != null) && Boolean.getBoolean(property);
}
/**
* Get the application path as a File object. If the "it.application.path"
* system property is set, return a File object pointing to the file, else try
* to find the application in <tt>./target/dist</tt> directory.
* @return a File object or null if no application path is found
*/
private static File getApplicationPath() {
final File dir =
getFileFromSystemProperty(IT_APPLICATION_PATH_KEY_SYSTEM_KEY);
if (dir != null) {
return dir;
}
// Get user dir
final File distDir = new File(System.getProperty("user.dir")
+ File.separator + "target" + File.separator + "dist");
// The dist directory does not exists ?
if (!distDir.isDirectory()) {
return null;
}
// Set Java property for TestNG
System.setProperty("maven.testng.output.dir", "");
// Search if the dist directory only contains an unique directory
File subDir = null;
int dirCount = 0;
int fileCount = 0;
final File[] files = distDir.listFiles();
if (files != null) {
for (final File f : files) {
if (f.getName().startsWith(".")) {
continue;
}
if (f.isDirectory()) {
dirCount++;
subDir = f;
} else if (f.isFile()) {
fileCount++;
}
}
}
// There only on directory in dist directory
if (fileCount == 0 && dirCount == 1) {
return subDir;
}
// Other cases
return distDir;
}
//
// Constructor
//
/**
* Public constructor.
* @throws EoulsanException if an error occurs when reading configuration
* file.
* @throws IOException
*/
public ITFactory() throws EoulsanException, IOException {
// Get configuration file path
final File configurationFile =
getFileFromSystemProperty(IT_CONF_PATH_SYSTEM_KEY);
if (configurationFile != null) {
// Get application path
this.applicationPath = getApplicationPath();
CONSTANTS.setProperty(APPLICATION_PATH_VARIABLE,
this.applicationPath.getAbsolutePath());
checkExistingDirectoryFile(this.applicationPath, "application path");
// Get the file with the list of tests to run
this.selectedTestsFile =
getFileFromSystemProperty(IT_TEST_LIST_PATH_SYSTEM_KEY);
// Get the test to execute
this.selectedTest = System.getProperty(IT_TEST_SYSTEM_KEY);
// Load configuration file
this.globalsConf = loadProperties(configurationFile);
addParameterCommandLineInConfiguration();
// Set test data source directory
this.testsDataDirectory =
new File(this.globalsConf.getProperty(TESTS_DIRECTORY_CONF_KEY));
this.testsDirectoryFoundToExecute = collectTestsDirectoryToExecute();
// Init it suite with all potential tests found in test data direction
ITSuite.getInstance(this.testsDirectoryFoundToExecute, this.globalsConf,
this.applicationPath);
} else {
// Case no testng must be create when compile project with maven
this.applicationPath = null;
this.testsDataDirectory = null;
this.selectedTestsFile = null;
this.selectedTest = null;
this.globalsConf = null;
this.testsDirectoryFoundToExecute = null;
}
}
}