/******************************************************************************* * * Copyright (c) 2004-2009 Oracle Corporation. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * * Kohsuke Kawaguchi, Martin Eigenbrodt, Tom Huybrechts, Yahoo!, Inc., Richard Hierlmeier * * *******************************************************************************/ package hudson.tasks.junit; import hudson.AbortException; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.matrix.MatrixAggregatable; import hudson.matrix.MatrixAggregator; import hudson.matrix.MatrixBuild; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Action; import hudson.model.BuildListener; import hudson.model.CheckPoint; import hudson.model.Descriptor; import hudson.model.Result; import hudson.model.Saveable; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Publisher; import hudson.tasks.Recorder; import hudson.tasks.junit.TestResultAction.Data; import hudson.tasks.test.TestResultAggregator; import hudson.tasks.test.TestResultProjectAction; import hudson.util.DescribableList; import hudson.util.FormValidation; import net.sf.json.JSONObject; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.types.FileSet; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; /** * Generates HTML report from JUnit test result XML files. * * @author Kohsuke Kawaguchi */ public class JUnitResultArchiver extends Recorder implements Serializable, MatrixAggregatable { /** * {@link FileSet} "includes" string, like "foo/bar/*.xml" */ private final String testResults; /** * If true, retain a suite's complete stdout/stderr even if this is huge and * the suite passed. * * @since 1.358 */ private final boolean keepLongStdio; /** * {@link TestDataPublisher}s configured for this archiver, to process the * recorded data. For compatibility reasons, can be null. * * @since 1.320 */ private final DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> testDataPublishers; /** * left for backwards compatibility * * @deprecated since 2009-08-09. */ @Deprecated public JUnitResultArchiver(String testResults) { this(testResults, false, null); } @Deprecated public JUnitResultArchiver(String testResults, DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> testDataPublishers) { this(testResults, false, testDataPublishers); } @DataBoundConstructor public JUnitResultArchiver( String testResults, boolean keepLongStdio, DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> testDataPublishers) { this.testResults = testResults; this.keepLongStdio = keepLongStdio; this.testDataPublishers = testDataPublishers; } /** * @inheritDoc */ @Override public boolean needsToRun(Result buildResult) { //TODO it seems we shouldn't archive junit results if build result is worse than FAILURE, investigate this return buildResult.isBetterThan(Result.ABORTED); } /** * In progress. Working on delegating the actual parsing to the JUnitParser. */ protected TestResult parse(String expandedTestResults, AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { return new JUnitParser(isKeepLongStdio()).parse(expandedTestResults, build, launcher, listener); } @Override public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { listener.getLogger().println(Messages.JUnitResultArchiver_Recording()); TestResultAction action; final String testResults = build.getEnvironment(listener).expand(this.testResults); try { TestResult result = parse(testResults, build, launcher, listener); try { action = new TestResultAction(build, result, listener); } catch (NullPointerException npe) { throw new AbortException(Messages.JUnitResultArchiver_BadXML(testResults)); } result.freeze(action); if (result.getPassCount() == 0 && result.getFailCount() == 0) { throw new AbortException(Messages.JUnitResultArchiver_ResultIsEmpty()); } // TODO: Move into JUnitParser [BUG 3123310] List<Data> data = new ArrayList<Data>(); if (testDataPublishers != null) { for (TestDataPublisher tdp : testDataPublishers) { Data d = tdp.getTestData(build, launcher, listener, result); if (d != null) { data.add(d); } } } action.setData(data); CHECKPOINT.block(); } catch (AbortException e) { if (build.getResult() == Result.FAILURE) // most likely a build failed before it gets to the test phase. // don't report confusing error message. { return true; } listener.getLogger().println(e.getMessage()); build.setResult(Result.FAILURE); return true; } catch (IOException e) { e.printStackTrace(listener.error("Failed to archive test reports")); build.setResult(Result.FAILURE); return true; } build.getActions().add(action); CHECKPOINT.report(); if (action.getResult().getFailCount() > 0) { build.setResult(Result.UNSTABLE); } return true; } /** * Not actually used, but left for backward compatibility * * @deprecated since 2009-08-10. */ protected TestResult parseResult(DirectoryScanner ds, long buildTime) throws IOException { return new TestResult(buildTime, ds); } /** * This class does explicit checkpointing. */ public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } public String getTestResults() { return testResults; } public DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> getTestDataPublishers() { return testDataPublishers; } @Override public Collection<Action> getProjectActions(AbstractProject<?, ?> project) { return Collections.<Action>singleton(new TestResultProjectAction(project)); } public MatrixAggregator createAggregator(MatrixBuild build, Launcher launcher, BuildListener listener) { return new TestResultAggregator(build, launcher, listener); } /** * @return the keepLongStdio */ public boolean isKeepLongStdio() { return keepLongStdio; } /** * Test result tracks the diff from the previous run, hence the checkpoint. */ private static final CheckPoint CHECKPOINT = new CheckPoint( "JUnit result archiving"); private static final long serialVersionUID = 1L; @Extension public static class DescriptorImpl extends BuildStepDescriptor<Publisher> { public String getDisplayName() { return Messages.JUnitResultArchiver_DisplayName(); } @Override public String getHelpFile() { return "/help/tasks/junit/report.html"; } @Override public Publisher newInstance(StaplerRequest req, JSONObject formData) throws hudson.model.Descriptor.FormException { String testResults = formData.getString("testResults"); boolean keepLongStdio = formData.getBoolean("keepLongStdio"); DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>> testDataPublishers = new DescribableList<TestDataPublisher, Descriptor<TestDataPublisher>>(Saveable.NOOP); try { testDataPublishers.rebuild(req, formData, TestDataPublisher.all()); } catch (IOException e) { throw new FormException(e, null); } return new JUnitResultArchiver(testResults, keepLongStdio, testDataPublishers); } /** * Performs on-the-fly validation on the file mask wildcard. */ public FormValidation doCheckTestResults( @AncestorInPath AbstractProject project, @QueryParameter String value) throws IOException { return FilePath.validateFileMask(project.getSomeWorkspace(), value); } public boolean isApplicable(Class<? extends AbstractProject> jobType) { return true; } } }