/*
* Copyright 2006-2013 the original author or authors.
*
* Licensed 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 com.consol.citrus.admin.service;
import com.consol.citrus.CitrusConstants;
import com.consol.citrus.admin.configuration.*;
import com.consol.citrus.admin.exception.CitrusAdminRuntimeException;
import com.consol.citrus.admin.executor.*;
import com.consol.citrus.admin.model.*;
import com.consol.citrus.admin.util.FileHelper;
import com.consol.citrus.dsl.*;
import com.consol.citrus.dsl.annotations.CitrusTest;
import com.consol.citrus.exceptions.CitrusRuntimeException;
import com.consol.citrus.util.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import java.io.*;
import java.lang.reflect.Method;
import java.util.*;
/**
* Test case service reads tests from file system and delegates to file system test executor for
* test execution.
* @author Christoph Deppisch
* @since 1.4
*/
@Component
public class TestCaseServiceImpl extends AbstractTestCaseService {
/** Logger */
private static Logger log = LoggerFactory.getLogger(TestCaseServiceImpl.class);
/** Test executor works on filesystem */
@Autowired
private FileSystemTestExecutor fileSystemTestExecutor;
/** Test executor works on project classpath */
@Autowired
private ClasspathTestExecutor classpathTestExecutor;
@Autowired
private FileHelper fileHelper;
@Override
public List<TestCaseData> getTests(Project project) {
List<TestCaseData> tests = new ArrayList<TestCaseData>();
List<File> testFiles = FileUtils.getTestFiles(getTestDirectory(project));
for (File file : testFiles) {
String testName = FilenameUtils.getBaseName(file.getName());
String testPackageName = file.getPath().substring(getTestDirectory(project).length(), file.getPath().length() - file.getName().length())
.replace(File.separatorChar, '.');
if (testPackageName.endsWith(".")) {
testPackageName = testPackageName.substring(0, testPackageName.length() - 1);
}
TestCaseData testCase = new TestCaseData();
testCase.setType(TestCaseType.XML);
testCase.setName(testName);
testCase.setPackageName(testPackageName);
testCase.setFile(file.getParentFile().getAbsolutePath() + File.separator + FilenameUtils.getBaseName(file.getName()));
testCase.setLastModified(file.lastModified());
tests.add(testCase);
}
try {
Resource[] javaSources = new PathMatchingResourcePatternResolver().getResources("file:" + FilenameUtils.separatorsToUnix(getJavaDirectory(project)) + "**/*.java");
for (Resource resource : javaSources) {
File file = resource.getFile();
String testName = FilenameUtils.getBaseName(file.getName());
String testPackage = file.getParentFile().getAbsolutePath().substring(getJavaDirectory(project).length()).replace(File.separatorChar, '.');
if (knownToClasspath(testPackage, testName)) {
tests.addAll(getTestCaseInfoFromClass(testPackage, testName, file));
} else {
tests.addAll(getTestCaseInfoFromFile(testPackage, testName, file));
}
}
} catch (IOException e) {
log.warn("Failed to read Java source files - list of test cases for this project is incomplete", e);
}
return tests;
}
@Override
public Long getTestCount(Project project) {
Long testCount = Long.valueOf(FileUtils.getTestFiles(getTestDirectory(project)).size());
try {
Resource[] javaSources = new PathMatchingResourcePatternResolver().getResources("file:" + FilenameUtils.separatorsToUnix(getJavaDirectory(project)) + "**/*.java");
for (Resource resource : javaSources) {
File file = resource.getFile();
String testName = FilenameUtils.getBaseName(file.getName());
String testPackage = file.getParentFile().getAbsolutePath().substring(getJavaDirectory(project).length()).replace(File.separatorChar, '.');
if (knownToClasspath(testPackage, testName)) {
testCount += getTestCaseInfoFromClass(testPackage, testName, file).size();
} else {
testCount += getTestCaseInfoFromFile(testPackage, testName, file).size();
}
}
} catch (IOException e) {
log.warn("Failed to read Java source files - list of test cases for this project is incomplete", e);
}
return testCount;
}
@Override
public TestResult executeTest(Project project, String packageName, String testName, String runConfigurationId) {
TestResult result = new TestResult();
TestCaseData testCase = new TestCaseData();
testCase.setName(testName);
result.setTestCase(testCase);
try {
RunConfiguration configuration = project.getRunConfiguration(runConfigurationId);
TestExecutor<RunConfiguration> testExecutor = getTestExecutor(configuration);
testExecutor.execute(packageName, testName, configuration);
result.setSuccess(true);
} catch (Exception e) {
log.warn("Failed to execute Citrus test case '" + testName + "'", e);
result.setSuccess(false);
ByteArrayOutputStream os = new ByteArrayOutputStream();
e.printStackTrace(new PrintStream(os));
result.setStackTrace("Caused by: " + os.toString());
if (e instanceof CitrusRuntimeException) {
result.setFailureStack(((CitrusRuntimeException)e).getFailureStackAsString());
}
}
return result;
}
@Override
public String getSourceCode(Project project, String packageName, String name, TestCaseType type) {
String dir = type.equals(TestCaseType.JAVA) ? getJavaDirectory(project) : getTestDirectory(project);
try {
String sourceFilePath = dir + File.separator + packageName.replace('.', File.separatorChar) + File.separator + name + "." + type.name().toLowerCase();
if (new File(sourceFilePath).exists()) {
return FileUtils.readToString(new FileInputStream(sourceFilePath));
} else {
return "";
}
} catch (IOException e) {
throw new CitrusAdminRuntimeException("Failed to load test case source code", e);
}
}
@Override
public FileTreeModel getTestFileTree(Project project, String dir) {
FileTreeModel model = new FileTreeModel();
String testDirectory = getTestDirectory(project) + dir;
String javaDirectory = getJavaDirectory(project) + dir;
String[] folders = null;
List<FileTreeModel.TestFileModel> xmlTestFiles = new ArrayList<FileTreeModel.TestFileModel>();
List<FileTreeModel.TestFileModel> javaTestFiles = new ArrayList<FileTreeModel.TestFileModel>();
String compactFolder = "";
do {
if (folders != null) {
if (StringUtils.hasText(compactFolder)) {
compactFolder += File.separator + folders[0];
} else {
compactFolder = folders[0];
}
}
folders = fileHelper.getFolders(new File(javaDirectory + compactFolder));
String[] xmlFiles = fileHelper.getFiles(new File(testDirectory + compactFolder), ".xml");
for (String xmlFile : xmlFiles) {
FileTreeModel.TestFileModel fileModel = new FileTreeModel.TestFileModel();
fileModel.setFileName(xmlFile);
fileModel.setFilePath(testDirectory + (StringUtils.hasText(compactFolder) ? compactFolder + File.separator : ""));
xmlTestFiles.add(fileModel);
}
try {
Resource[] javaSources = new PathMatchingResourcePatternResolver().getResources("file:" + javaDirectory + compactFolder + "/*.java");
for (Resource resource : javaSources) {
File file = resource.getFile();
String testName = FilenameUtils.getBaseName(file.getName());
String testPackage = file.getParentFile().getAbsolutePath().substring(getJavaDirectory(project).length()).replace(File.separatorChar, '.');
if (knownToClasspath(testPackage, testName)) {
javaTestFiles.addAll(getTestFileTreeFromClass(javaDirectory, compactFolder, testPackage, testName));
} else {
javaTestFiles.addAll(getTestFileTreeFromFile(javaDirectory, compactFolder, testPackage, testName, file));
}
}
} catch (IOException e) {
log.warn("Failed to read Java source files - list of test cases for this project is incomplete", e);
}
} while (folders.length == 1 && xmlTestFiles.size() == 0 && javaTestFiles.size() == 0);
model.setCompactFolder(compactFolder);
model.setFolders(folders);
model.setXmlFiles(xmlTestFiles);
model.setJavaFiles(javaTestFiles);
return model;
}
/**
* Constructs test tree from class information such as Java 5 annotations. Method has to instantiate the class so class must
* be available on classpath. Class itself and all testable methods are added as executables to file tree model.
* @param javaDirectory
* @param compactFolder
* @param testPackage
* @param testName
* @return
*/
private List<? extends FileTreeModel.TestFileModel> getTestFileTreeFromClass(String javaDirectory, String compactFolder, String testPackage, String testName) {
List<FileTreeModel.TestFileModel> javaTestFiles = new ArrayList<FileTreeModel.TestFileModel>();
try {
Class<?> testBuilderClass = Class.forName(testPackage + "." + testName);
if (TestBuilder.class.isAssignableFrom(testBuilderClass)) {
FileTreeModel.TestFileModel fileModel = new FileTreeModel.TestFileModel();
fileModel.setFileName(testName);
fileModel.setFilePath(javaDirectory + (StringUtils.hasText(compactFolder) ? compactFolder + File.separator : ""));
List<String> methods = getTestMethods(testBuilderClass);
// only add methods in case several test methods found - one single method is represented by test class
if (methods.size() > 1) {
fileModel.setTestMethods(methods);
}
javaTestFiles.add(fileModel);
} else {
log.debug("Skipping java source as it is not a valid Citrus test builder: " + testPackage + "." + testName);
}
} catch (ClassNotFoundException e) {
log.debug("Java source is not part of classpath: " + testPackage + "." + testName);
}
return javaTestFiles;
}
/**
* Construct test tree from pure file content information. Class has not to be instantiated so it also has not to be
* part of the classpath. Class itself and all testable methods are added as executables to file tree model.
* @param javaDirectory
* @param compactFolder
* @param testPackage
* @param testName
* @param file
* @return
*/
private List<? extends FileTreeModel.TestFileModel> getTestFileTreeFromFile(String javaDirectory, String compactFolder, String testPackage, String testName, File file) {
List<FileTreeModel.TestFileModel> javaTestFiles = new ArrayList<FileTreeModel.TestFileModel>();
try {
String javaContent = FileUtils.readToString(new FileInputStream(file));
javaContent = StringUtils.trimAllWhitespace(javaContent);
String citrusAnnotation = "@CitrusTest";
if (javaContent.contains(TestNGCitrusTestBuilder.class.getSimpleName()) ||
javaContent.contains(JUnit4CitrusTestBuilder.class.getSimpleName()) ||
javaContent.contains(citrusAnnotation)) {
FileTreeModel.TestFileModel fileModel = new FileTreeModel.TestFileModel();
fileModel.setFileName(testName);
fileModel.setFilePath(javaDirectory + (StringUtils.hasText(compactFolder) ? compactFolder + File.separator : ""));
List<String> methods = new ArrayList<String>();
int position = javaContent.indexOf(citrusAnnotation);
while (position > 0) {
String methodContent = javaContent.substring(position);
String methodName = null;
if (methodContent.startsWith(citrusAnnotation + "(")) {
String annotationProps = methodContent.substring(methodContent.indexOf('('), methodContent.indexOf(')'));
if (StringUtils.hasText(annotationProps) && annotationProps.contains("name=\"")) {
methodName = annotationProps.substring(annotationProps.indexOf("name=\"") + "name=\"".length());
methodName = methodName.substring(0, methodName.indexOf('"'));
}
}
if (!StringUtils.hasText(methodName)) {
methodName = methodContent.substring(methodContent.indexOf("publicvoid") + "publicvoid".length());
methodName = methodName.substring(0, methodName.indexOf('('));
}
methods.add(methodName);
position = javaContent.indexOf(citrusAnnotation, position + citrusAnnotation.length());
}
fileModel.setTestMethods(methods);
javaTestFiles.add(fileModel);
} else {
log.debug("Skipping java source as it is not a valid Citrus test: " + testPackage + "." + testName);
}
} catch (IOException e) {
log.warn("Unable to access Java source on file system: " + testPackage + "." + testName, e);
}
return javaTestFiles;
}
/**
* Adds test info by reading file resource as text content. Searches for class annotations and method annotations
* on a text based search. This approach does not need to instantiate the class so Java source must not necessarily be
* part of the classpath.
* @param testPackage
* @param testName
* @param file
*/
private List<TestCaseData> getTestCaseInfoFromFile(String testPackage, String testName, File file) {
List<TestCaseData> tests = new ArrayList<TestCaseData>();
try {
String javaContent = FileUtils.readToString(new FileInputStream(file));
javaContent = StringUtils.trimAllWhitespace(javaContent);
String citrusAnnotation = "@CitrusTest";
if (javaContent.contains(citrusAnnotation)) {
int position = javaContent.indexOf(citrusAnnotation);
while (position > 0) {
String methodContent = javaContent.substring(position);
TestCaseData testCase = new TestCaseData();
testCase.setType(TestCaseType.JAVA);
testCase.setPackageName(testPackage);
testCase.setFile(file.getParentFile().getAbsolutePath() + File.separator + FilenameUtils.getBaseName(file.getName()));
testCase.setLastModified(file.lastModified());
if (methodContent.startsWith(citrusAnnotation + "(")) {
String annotationProps = methodContent.substring(methodContent.indexOf('('), methodContent.indexOf(')'));
if (StringUtils.hasText(annotationProps) && annotationProps.contains("name=\"")) {
String methodName = annotationProps.substring(annotationProps.indexOf("name=\"") + "name=\"".length());
methodName = methodName.substring(0, methodName.indexOf('"'));
testCase.setName(methodName);
}
}
if (!StringUtils.hasText(testCase.getName())) {
String methodName = methodContent.substring(methodContent.indexOf("publicvoid") + "publicvoid".length());
methodName = methodName.substring(0, methodName.indexOf('('));
testCase.setName(methodName);
}
tests.add(testCase);
position = javaContent.indexOf(citrusAnnotation, position + citrusAnnotation.length());
}
} else if (javaContent.contains(TestNGCitrusTestBuilder.class.getSimpleName()) ||
javaContent.contains(JUnit4CitrusTestBuilder.class.getSimpleName())) {
TestCaseData testCase = new TestCaseData();
testCase.setType(TestCaseType.JAVA);
testCase.setName(testName);
testCase.setPackageName(testPackage);
testCase.setFile(file.getParentFile().getAbsolutePath() + File.separator + FilenameUtils.getBaseName(file.getName()));
testCase.setLastModified(file.lastModified());
tests.add(testCase);
} else {
log.debug("Skipping java source as it is not a valid Citrus test: " + testPackage + "." + testName);
}
} catch (IOException e) {
log.warn("Unable to access Java source on file system: " + testPackage + "." + testName, e);
}
return tests;
}
/**
* Adds test info from class information such as Java 5 annotations. Method has to
* instantiate class in order to read this information.
* @param testPackage
* @param testName
* @param file
*/
private List<TestCaseData> getTestCaseInfoFromClass(String testPackage, String testName, File file) {
List<TestCaseData> tests = new ArrayList<TestCaseData>();
try {
Class<?> testBuilderClass = Class.forName(testPackage + "." + testName);
if (TestBuilder.class.isAssignableFrom(testBuilderClass)) {
List<String> methods = getTestMethods(testBuilderClass);
for (String method : methods) {
TestCaseData testCase = new TestCaseData();
testCase.setType(TestCaseType.JAVA);
testCase.setName(method);
testCase.setPackageName(testPackage);
testCase.setFile(file.getParentFile().getAbsolutePath() + File.separator + FilenameUtils.getBaseName(file.getName()) + "." + method);
testCase.setLastModified(file.lastModified());
tests.add(testCase);
}
if (tests.isEmpty()) {
// there were no Citrus annotated methods found so lets add the class itself as test case info
TestCaseData testCase = new TestCaseData();
testCase.setType(TestCaseType.JAVA);
testCase.setName(testName);
testCase.setPackageName(testPackage);
testCase.setFile(file.getParentFile().getAbsolutePath() + File.separator + FilenameUtils.getBaseName(file.getName()));
testCase.setLastModified(file.lastModified());
tests.add(testCase);
}
} else {
log.debug("Skipping java source as it is not a valid Citrus test builder: " + testPackage + "." + testName);
}
} catch (ClassNotFoundException e) {
log.debug("Java source is not part of classpath: " + testPackage + "." + testName);
}
return tests;
}
/**
* Try to instantiate class with package and name. If class is accessible from classpath
* method returns true otherwise false.
* @param testPackage
* @param testName
* @return
*/
private boolean knownToClasspath(String testPackage, String testName) {
try {
Class.forName(testPackage + "." + testName);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
/**
* Finds all Citrus annotated test methods in test class. Do only return methods when more than one test method
* is found. Otherwise use class itself as test representation.
* @param testClass
* @return
*/
private List<String> getTestMethods(Class<?> testClass) {
List<String> methodNames = new ArrayList<String>();
for (Method method : ReflectionUtils.getAllDeclaredMethods(testClass)) {
if (method.getAnnotation(CitrusTest.class) != null) {
CitrusTest citrusAnnotation = method.getAnnotation(CitrusTest.class);
if (StringUtils.hasText(citrusAnnotation.name())) {
methodNames.add(citrusAnnotation.name());
} else {
// use default method name as test
methodNames.add(method.getName());
}
}
}
return methodNames;
}
/**
* Finds proper test case executor implementation for run configuration.
* @param configuration
* @return
*/
private TestExecutor getTestExecutor(RunConfiguration configuration) {
if (configuration instanceof MavenRunConfiguration) {
return fileSystemTestExecutor;
} else if (configuration instanceof ClasspathRunConfiguration) {
return classpathTestExecutor;
}
throw new CitrusAdminRuntimeException("Unable to execute test for run configuration: " + configuration.getId());
}
/**
* Gets the current test directory based on project home and default test directory.
* @return
*/
private String getTestDirectory(Project project) {
return new File(project.getProjectHome()).getAbsolutePath() + File.separator + CitrusConstants.DEFAULT_TEST_DIRECTORY;
}
/**
* Gets the current test directory based on project home and default test directory.
* @return
*/
private String getJavaDirectory(Project project) {
return new File(project.getProjectHome()).getAbsolutePath() + File.separator + CitrusConstants.DEFAULT_JAVA_DIRECTORY;
}
}