/*
* The MIT License (MIT)
*
* Copyright (c) 2014, 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 org.jenkinsci.plugins.xunit;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Singleton;
import hudson.FilePath;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.Hudson;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.remoting.VirtualChannel;
import hudson.tasks.junit.TestResult;
import hudson.tasks.junit.TestResultAction;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.types.FileSet;
import org.jenkinsci.lib.dtkit.model.InputMetric;
import org.jenkinsci.lib.dtkit.type.TestType;
import org.jenkinsci.plugins.xunit.exception.XUnitException;
import org.jenkinsci.plugins.xunit.service.*;
import org.jenkinsci.plugins.xunit.threshold.XUnitThreshold;
import org.jenkinsci.plugins.xunit.types.CustomType;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
/**
* @author Gregory Boissinot
*/
public class XUnitProcessor implements Serializable {
private static final long serialVersionUID = 1L;
private TestType[] types;
private XUnitThreshold[] thresholds;
private int thresholdMode;
private ExtraConfiguration extraConfiguration;
public XUnitProcessor(TestType[] types, XUnitThreshold[] thresholds, int thresholdMode, ExtraConfiguration extraConfiguration) {
this.types = types;
if (types == null) {
throw new NullPointerException("The types section is required.");
}
this.thresholds = thresholds;
this.thresholdMode = thresholdMode;
this.extraConfiguration = extraConfiguration;
}
public boolean performXunit(boolean dryRun, AbstractBuild<?, ?> build, BuildListener listener)
throws IOException, InterruptedException {
return performXUnit(dryRun, build, build.getWorkspace(), listener);
}
public boolean performXUnit(boolean dryRun, Run<?, ?> build, FilePath workspace, TaskListener listener)
throws IOException, InterruptedException {
final XUnitLog xUnitLog = getXUnitLogObject(listener);
try {
xUnitLog.infoConsoleLogger("Starting to record.");
boolean continueTestProcessing;
try {
continueTestProcessing = performTests(xUnitLog, build, workspace, listener);
} catch (StopTestProcessingException e) {
build.setResult(Result.FAILURE);
xUnitLog.infoConsoleLogger("There are errors when processing test results.");
xUnitLog.infoConsoleLogger("Skipping tests recording.");
xUnitLog.infoConsoleLogger("Stop build.");
return true;
}
if (!continueTestProcessing) {
xUnitLog.infoConsoleLogger("There are errors when processing test results.");
xUnitLog.infoConsoleLogger("Skipping tests recording.");
return true;
}
recordTestResult(build, workspace, listener, xUnitLog);
processDeletion(dryRun, workspace, xUnitLog);
Result result = getBuildStatus(build, xUnitLog);
if (result != null) {
if (!dryRun) {
xUnitLog.infoConsoleLogger("Setting the build status to " + result);
build.setResult(result);
} else {
xUnitLog.infoConsoleLogger("Through the xUnit plugin, the build status will be set to " + result.toString());
}
}
xUnitLog.infoConsoleLogger("Stopping recording.");
return true;
} catch (XUnitException xe) {
xUnitLog.errorConsoleLogger("The plugin hasn't been performed correctly: " + xe.getMessage());
build.setResult(Result.FAILURE);
return false;
}
}
private XUnitLog getXUnitLogObject(final TaskListener listener) {
return Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bind(TaskListener.class).toInstance(listener);
}
}).getInstance(XUnitLog.class);
}
private boolean performTests(XUnitLog xUnitLog, Run<?, ?> build, FilePath workspace, TaskListener listener) throws IOException, InterruptedException, StopTestProcessingException {
XUnitReportProcessorService xUnitReportService = getXUnitReportProcessorServiceObject(listener);
boolean findTest = false;
for (TestType tool : types) {
xUnitLog.infoConsoleLogger("Processing " + tool.getDescriptor().getDisplayName());
if (!isEmptyGivenPattern(xUnitReportService, tool)) {
String expandedPattern = getExpandedResolvedPattern(tool, build, listener);
XUnitToolInfo xUnitToolInfo = getXUnitToolInfoObject(tool, expandedPattern, build, workspace, listener);
XUnitTransformer xUnitTransformer = getXUnitTransformerObject(xUnitToolInfo, listener);
boolean result = false;
try {
result = workspace.act(xUnitTransformer);
findTest = true;
} catch (InterruptedException ie) {
// handled tunneled exceptions
Throwable originalException = null;
Throwable cause = ie.getCause();
while (cause != null) {
originalException = cause;
cause = cause.getCause();
}
if (originalException instanceof InterruptedException)
ie = (InterruptedException) originalException;
if (ie instanceof NoFoundTestException) {
xUnitLog.infoConsoleLogger("Failing BUILD.");
throw new StopTestProcessingException();
}
if (ie instanceof SkipTestException) {
xUnitLog.infoConsoleLogger("Skipping the metric tool processing.");
continue;
}
if (ie instanceof OldTestReportException) {
xUnitLog.infoConsoleLogger("Failing BUILD.");
throw new StopTestProcessingException();
}
xUnitLog.warningConsoleLogger("Caught exception of unexpected type " + ie.getClass() + ", rethrowing");
throw ie;
}
if (!result && xUnitToolInfo.isStopProcessingIfError()) {
xUnitLog.infoConsoleLogger("Failing BUILD because 'set build failed if errors' option is activated.");
throw new StopTestProcessingException();
}
}
}
return findTest;
}
private XUnitReportProcessorService getXUnitReportProcessorServiceObject(final TaskListener listener) {
return Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bind(TaskListener.class).toInstance(listener);
}
}).getInstance(XUnitReportProcessorService.class);
}
private static class StopTestProcessingException extends Exception {
}
private boolean isEmptyGivenPattern(XUnitReportProcessorService xUnitReportService, TestType tool) {
return xUnitReportService.isEmptyPattern(tool.getPattern());
}
private String getExpandedResolvedPattern(TestType tool, Run build, TaskListener listener) throws IOException, InterruptedException {
String newExpandedPattern = tool.getPattern();
newExpandedPattern = newExpandedPattern.replaceAll("[\t\r\n]+", " ");
return Util.replaceMacro(newExpandedPattern, build.getEnvironment(listener));
}
private XUnitToolInfo getXUnitToolInfoObject(final TestType tool, final String expandedPattern, final Run build, final FilePath workspace, final TaskListener listener) throws IOException, InterruptedException {
InputMetric inputMetric = tool.getInputMetric();
inputMetric = Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bind(TaskListener.class).toInstance(listener);
bind(XUnitLog.class).in(Singleton.class);
bind(XUnitValidationService.class).in(Singleton.class);
bind(XUnitConversionService.class).in(Singleton.class);
}
}).getInstance(inputMetric.getClass());
return new XUnitToolInfo(
new FilePath(new File(Hudson.getInstance().getRootDir(), "userContent")),
inputMetric,
expandedPattern,
tool.isSkipNoTestFiles(),
tool.isFailIfNotNew(),
tool.isDeleteOutputFiles(), tool.isStopProcessingIfError(),
build.getTimeInMillis(),
this.extraConfiguration.getTestTimeMargin(),
(tool instanceof CustomType) ? getCustomStylesheet(tool, build, workspace, listener) : null);
}
private FilePath getCustomStylesheet(final TestType tool, final Run build, final FilePath workspace, final TaskListener listener) throws IOException, InterruptedException {
final String customXSLPath = Util.replaceMacro(((CustomType) tool).getCustomXSL(), build.getEnvironment(listener));
//Try full path
FilePath customXSLFilePath = new FilePath(new File(customXSLPath));
if (!customXSLFilePath.exists()) {
//Try from workspace
customXSLFilePath = workspace.child(customXSLPath);
}
if (!customXSLFilePath.exists()) {
throw new XUnitException("The given xsl '" + customXSLPath + "'doesn't exist.");
}
return customXSLFilePath;
}
private XUnitTransformer getXUnitTransformerObject(final XUnitToolInfo xUnitToolInfo, final TaskListener listener) {
return Guice.createInjector(new AbstractModule() {
@Override
protected void configure() {
bind(TaskListener.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(XUnitReportProcessorService.class).in(Singleton.class);
}
}).getInstance(XUnitTransformer.class);
}
private TestResultAction getTestResultAction(Run<?, ?> build) {
return build.getAction(TestResultAction.class);
}
private TestResultAction getPreviousTestResultAction(Run<?, ?> build) {
Run previousBuild = build.getPreviousBuild();
if (previousBuild == null) {
return null;
}
return getTestResultAction(previousBuild);
}
private void recordTestResult(Run<?, ?> build, FilePath workspace, TaskListener listener, XUnitLog xUnitLog) 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(workspace, "**/TEST-*.xml", existingTestResults, buildTime, nowMaster);
if (result != null) {
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) {
xUnitLog.warningConsoleLogger("All test reports are empty.");
}
if (existingAction == null) {
build.getActions().add(action);
}
}
}
/**
* Gets a Test result object (a new one if any)
*
* @param workspace the build's workspace
* @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 FilePath workspace,
final String junitFilePattern,
final TestResult existingTestResults,
final long buildTime, final long nowMaster)
throws XUnitException {
try {
return workspace.act(new jenkins.SlaveToMasterFileCallable<TestResult>() {
public TestResult invoke(File ws, VirtualChannel channel) throws IOException {
final long nowSlave = System.currentTimeMillis();
File generatedJunitDir = new File(ws, XUnitDefaultValues.GENERATED_JUNIT_DIR);
//Ignore return value
generatedJunitDir.mkdirs();
FileSet fs = Util.createFileSet(generatedJunitDir, junitFilePattern);
DirectoryScanner ds = fs.getDirectoryScanner();
String[] files = ds.getIncludedFiles();
if (files.length == 0) {
// no test result. Most likely a configuration error or fatal problem
return null;
}
try {
if (existingTestResults == null) {
return new TestResult(buildTime + (nowSlave - nowMaster), ds, true);
} else {
existingTestResults.parse(buildTime + (nowSlave - nowMaster), ds);
return existingTestResults;
}
} catch (IOException ioe) {
throw new IOException(ioe);
}
}
});
} catch (IOException ioe) {
throw new XUnitException(ioe.getMessage(), ioe);
} catch (InterruptedException ie) {
throw new XUnitException(ie.getMessage(), ie);
}
}
private Result getBuildStatus(Run<?, ?> build, XUnitLog xUnitLog) {
Result curResult = getResultWithThreshold(xUnitLog, build);
Result previousResultStep = build.getResult();
if (curResult != null) {
if (previousResultStep == null) {
return curResult;
}
if (previousResultStep != Result.NOT_BUILT && previousResultStep.isWorseOrEqualTo(curResult)) {
curResult = previousResultStep;
}
return curResult;
}
return null;
}
private Result getResultWithThreshold(XUnitLog log, Run<?, ?> build) {
TestResultAction testResultAction = getTestResultAction(build);
TestResultAction previousTestResultAction = getPreviousTestResultAction(build);
if (testResultAction == null) {
return Result.FAILURE;
} else {
return processResultThreshold(log, build, testResultAction, previousTestResultAction);
}
}
private Result processResultThreshold(XUnitLog log,
Run<?, ?> build,
TestResultAction testResultAction,
TestResultAction previousTestResultAction) {
if (thresholds != null) {
for (XUnitThreshold threshold : thresholds) {
log.infoConsoleLogger(String.format("Check '%s' threshold.", threshold.getDescriptor().getDisplayName()));
Result result;
if (XUnitDefaultValues.MODE_PERCENT == thresholdMode) {
result = threshold.getResultThresholdPercent(log, build, testResultAction, previousTestResultAction);
} else {
result = threshold.getResultThresholdNumber(log, build, testResultAction, previousTestResultAction);
}
if (result.isWorseThan(Result.SUCCESS)) {
return result;
}
}
}
return Result.SUCCESS;
}
private void processDeletion(boolean dryRun, FilePath workspace, XUnitLog xUnitLog) throws XUnitException {
try {
boolean keepJUnitDirectory = false;
for (TestType tool : types) {
InputMetric inputMetric = tool.getInputMetric();
if (dryRun || tool.isDeleteOutputFiles()) {
workspace.child(XUnitDefaultValues.GENERATED_JUNIT_DIR + "/" + inputMetric.getToolName()).deleteRecursive();
} else {
//Mark the tool file parent directory to no deletion
keepJUnitDirectory = true;
}
}
if (!keepJUnitDirectory) {
workspace.child(XUnitDefaultValues.GENERATED_JUNIT_DIR).deleteRecursive();
}
} catch (IOException ioe) {
throw new XUnitException("Problem on deletion", ioe);
} catch (InterruptedException ie) {
throw new XUnitException("Problem on deletion", ie);
}
}
}