/*******************************************************************************
* Copyright (c) 2009 Thales Corporate Services SAS *
* Author : Gregory Boissinot *
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy *
* of this software and associated documentation files (the "Software"), to deal*
* in the Software without restriction, including without limitation the rights *
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell *
* copies of the Software, and to permit persons to whom the Software is *
* furnished to do so, subject to the following conditions: *
* *
* The above copyright notice and this permission notice shall be included in *
* all copies or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,*
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN *
* THE SOFTWARE. *
*******************************************************************************/
package com.thalesgroup.hudson.plugins.xunit;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Singleton;
import com.thalesgroup.dtkit.metrics.api.InputMetric;
import com.thalesgroup.dtkit.metrics.hudson.api.descriptor.TestTypeDescriptor;
import com.thalesgroup.dtkit.metrics.hudson.api.type.TestType;
import com.thalesgroup.hudson.plugins.xunit.exception.XUnitException;
import com.thalesgroup.hudson.plugins.xunit.service.XUnitConversionService;
import com.thalesgroup.hudson.plugins.xunit.service.XUnitLog;
import com.thalesgroup.hudson.plugins.xunit.service.XUnitReportProcessingService;
import com.thalesgroup.hudson.plugins.xunit.service.XUnitValidationService;
import com.thalesgroup.hudson.plugins.xunit.transformer.XUnitToolInfo;
import com.thalesgroup.hudson.plugins.xunit.transformer.XUnitTransformer;
import hudson.*;
import hudson.model.*;
import hudson.remoting.VirtualChannel;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Publisher;
import hudson.tasks.Recorder;
import hudson.tasks.junit.JUnitResultArchiver;
import hudson.tasks.junit.TestResult;
import hudson.tasks.junit.TestResultAction;
import hudson.tasks.test.TestResultProjectAction;
import net.sf.json.JSONObject;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.types.FileSet;
import org.kohsuke.stapler.StaplerRequest;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.List;
/**
* Class that converting custom reports to Junit reports and records them
*
* @author Gregory Boissinot
*/
@SuppressWarnings({"unchecked", "unused"})
public class XUnitPublisher extends Recorder implements Serializable {
private TestType[] types;
private transient XUnitLog xUnitLog;
public XUnitPublisher(TestType[] types) {
this.types = types;
}
public TestType[] getTypes() {
return types;
}
@Override
public Action getProjectAction(AbstractProject<?, ?> project) {
JUnitResultArchiver jUnitResultArchiver = project.getPublishersList().get(JUnitResultArchiver.class);
if (jUnitResultArchiver == null) {
return new TestResultProjectAction(project);
}
return null;
}
/**
* Gets a Test result object (a new one if any)
*
* @param build the current build
* @param junitFileDir the parent output JUnit directory
* @param junitFilePattern the JUnit search pattern
* @param existingTestResults the existing test result
* @param buildTime the build time
* @param nowMaster the time on master
* @return the test result object
* @throws XUnitException the plugin exception
*/
private TestResult getTestResult(final AbstractBuild<?, ?> build,
final File junitFileDir,
final String junitFilePattern,
final TestResult existingTestResults,
final long buildTime, final long nowMaster)
throws XUnitException {
try {
return build.getWorkspace().act(new FilePath.FileCallable<TestResult>() {
public TestResult invoke(File ws, VirtualChannel channel) throws IOException {
final long nowSlave = System.currentTimeMillis();
FileSet fs = Util.createFileSet(junitFileDir, junitFilePattern);
DirectoryScanner ds = fs.getDirectoryScanner();
String[] files = ds.getIncludedFiles();
if (files.length == 0) {
// no test result. Most likely a configuration error or fatal problem
throw new IOException("No test report files were found. Configuration error?");
}
try {
if (existingTestResults == null) {
return new TestResult(buildTime + (nowSlave - nowMaster), ds);
} else {
existingTestResults.parse(buildTime + (nowSlave - nowMaster), ds);
return existingTestResults;
}
}
catch (IOException ioe) {
throw new IOException(ioe);
}
}
});
}
catch (IOException ioe) {
throw new XUnitException(ioe);
}
catch (InterruptedException ie) {
throw new XUnitException(ie);
}
}
/**
* Records the test results into the current build and return the number of tests
*
* @param build the current build object
* @param listener the current listener object
* @param junitTargetDirectory the parent JUnit directory
* @throws com.thalesgroup.hudson.plugins.xunit.exception.XUnitException
* the plugin exception if an error occurs
*/
private void recordTestResult(AbstractBuild<?, ?> build, BuildListener listener, final File junitTargetDirectory) throws XUnitException {
TestResultAction existingAction = build.getAction(TestResultAction.class);
final long buildTime = build.getTimestamp().getTimeInMillis();
final long nowMaster = System.currentTimeMillis();
TestResult existingTestResults = null;
if (existingAction != null) {
existingTestResults = existingAction.getResult();
}
TestResult result = getTestResult(build, junitTargetDirectory, "**/TEST-*.xml", existingTestResults, buildTime, nowMaster);
TestResultAction action;
if (existingAction == null) {
action = new TestResultAction(build, result, listener);
} else {
action = existingAction;
action.setResult(result, listener);
}
if (result.getPassCount() == 0 && result.getFailCount() == 0) {
throw new XUnitException("None of the test reports contained any result");
}
if (existingAction == null) {
build.getActions().add(action);
}
}
@Override
public boolean perform(final AbstractBuild<?, ?> build, Launcher launcher, final BuildListener listener)
throws InterruptedException, IOException {
xUnitLog = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bind(BuildListener.class).toInstance(listener);
}
}).getInstance(XUnitLog.class);
xUnitLog.info("Starting to record.");
try {
//Creation of the output JUnit directory
final File junitOuputDir = new File(new FilePath(build.getWorkspace(), "generatedJUnitFiles").toURI());
if (!junitOuputDir.mkdirs()) {
xUnitLog.warning("Can't create the path " + junitOuputDir + ". Maybe the directory already exists.");
}
XUnitReportProcessingService xUnitReportService = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bind(BuildListener.class).toInstance(listener);
}
}).getInstance(XUnitReportProcessingService.class);
boolean isInvoked = false;
for (TestType tool : types) {
xUnitLog.info("Processing " + tool.getDescriptor().getDisplayName());
if (!xUnitReportService.isEmptyPattern(tool.getPattern())) {
//Retrieves the pattern
String newExpandedPattern = tool.getPattern();
newExpandedPattern = newExpandedPattern.replaceAll("[\t\r\n]+", " ");
newExpandedPattern = Util.replaceMacro(newExpandedPattern, build.getEnvironment(listener));
//Build a new build info
final XUnitToolInfo xUnitToolInfo = new XUnitToolInfo(tool, junitOuputDir, newExpandedPattern, build.getTimeInMillis());
// Archiving tool reports into JUnit files
XUnitTransformer xUnitTransformer = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bind(BuildListener.class).toInstance(listener);
bind(XUnitToolInfo.class).toInstance(xUnitToolInfo);
bind(XUnitValidationService.class).in(Singleton.class);
bind(XUnitConversionService.class).in(Singleton.class);
bind(XUnitLog.class).in(Singleton.class);
bind(XUnitReportProcessingService.class).in(Singleton.class);
}
}).getInstance(XUnitTransformer.class);
boolean resultTransformation = build.getWorkspace().act(xUnitTransformer);
if (!resultTransformation) {
build.setResult(Result.FAILURE);
xUnitLog.info("Stopping recording.");
return true;
}
isInvoked = true;
}
}
if (!isInvoked) {
build.setResult(Result.FAILURE);
xUnitLog.error("No test reports found. Configuration error?");
return true;
}
// Process the record of xUnit
recordTestResult(build, listener, junitOuputDir);
//Set the mew build status indicator to unstable if there are failded tests
TestResultAction testResultAction = build.getAction(TestResultAction.class);
Result curResult = Result.SUCCESS;
if (testResultAction.getResult().getFailCount() > 0) {
curResult = Result.UNSTABLE;
}
//Delete generated files if triggered
boolean resultDeletionOK = build.getWorkspace().act(new FilePath.FileCallable<Boolean>() {
@SuppressWarnings({"ResultOfMethodCallIgnored"})
public Boolean invoke(File ws, VirtualChannel channel) throws IOException {
boolean keepJUnitDirectory = false;
for (TestType tool : types) {
boolean keepDirectoryTool = false;
InputMetric inputMetric = tool.getInputMetric();
//All the files will be under a directory the toolName
File toolFileParant = new File(junitOuputDir, inputMetric.getToolName());
if (tool.isDeleteOutputFiles()) {
File[] files = toolFileParant.listFiles();
for (File f : files) {
if (!f.delete()) {
xUnitLog.warning("Can't delete the file: " + f);
}
}
} else {
//Mark the tool file parent directory to no deletion
keepDirectoryTool = true;
}
if (!keepDirectoryTool) {
//Delete the tool parent directory
toolFileParant.delete();
} else {
//Mark the parent JUnit directory to set to true
keepJUnitDirectory = true;
}
}
if (!keepJUnitDirectory) {
junitOuputDir.delete();
}
return true;
}
});
if (!resultDeletionOK) {
build.setResult(Result.FAILURE);
xUnitLog.info("Stopping recording.");
return true;
}
//Keep the previous status result if worse or equal
Result previousResult = build.getResult();
if (previousResult.isWorseOrEqualTo(curResult)) {
build.setResult(previousResult);
xUnitLog.info("Stopping recording.");
return true;
}
// Fall back case: Set the build status to new build calculated build status
xUnitLog.info("Setting the build status to " + curResult);
build.setResult(curResult);
xUnitLog.info("Stopping recording.");
return true;
}
catch (IOException ie) {
xUnitLog.error("The plugin hasn't been performed correctly: " + ie.getMessage());
build.setResult(Result.FAILURE);
return false;
}
catch (XUnitException xe) {
xUnitLog.error("The plugin hasn't been performed correctly: " + xe.getMessage());
build.setResult(Result.FAILURE);
return false;
}
}
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.NONE;
}
@Extension
@SuppressWarnings("unused")
public static final class XUnitDescriptorPublisher extends BuildStepDescriptor<Publisher> {
public XUnitDescriptorPublisher() {
super(XUnitPublisher.class);
load();
}
@Override
public String getDisplayName() {
return Messages.xUnit_PublisherName();
}
@Override
public boolean isApplicable(Class type) {
return true;
}
@Override
public String getHelpFile() {
return "/plugin/xunit/help.html";
}
public DescriptorExtensionList<TestType, TestTypeDescriptor<?>> getListXUnitTypeDescriptors() {
return TestTypeDescriptor.all();
}
@Override
public Publisher newInstance(StaplerRequest req, JSONObject formData) throws FormException {
List<TestType> types = Descriptor.newInstancesFromHeteroList(
req, formData, "tools", getListXUnitTypeDescriptors());
return new XUnitPublisher(types.toArray(new TestType[types.size()]));
}
}
}