/* * The MIT License * * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Martin Eigenbrodt, * Tom Huybrechts, Yahoo!, Inc., Richard Hierlmeier * * 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.cucumber.jsontestsupport; 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.Action; import hudson.model.BuildListener; import hudson.model.CheckPoint; import hudson.model.Job; import hudson.model.Project; import hudson.model.Result; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Run; import hudson.model.TaskListener; import hudson.remoting.Callable; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Publisher; import hudson.tasks.Recorder; import hudson.tasks.test.TestResultAggregator; import hudson.tasks.test.TestResultProjectAction; import hudson.util.FormValidation; import jenkins.security.MasterToSlaveCallable; import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.logging.Logger; import jenkins.tasks.SimpleBuildStep; import net.sf.json.JSONObject; import org.apache.tools.ant.types.FileSet; import org.jenkinsci.Symbol; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.kohsuke.stapler.DataBoundSetter; /** * Generates HTML report from Cucumber JSON files. * * @author James Nord * @author Kohsuke Kawaguchi (original JUnit code) */ public class CucumberTestResultArchiver extends Recorder implements MatrixAggregatable, SimpleBuildStep { private static final Logger LOGGER = Logger.getLogger(CucumberTestResultArchiver.class.getName()); /** * {@link FileSet} "includes" string, like "foo/bar/*.xml" */ private final String testResults; private boolean ignoreBadSteps; @DataBoundConstructor public CucumberTestResultArchiver(String testResults) { this.testResults = testResults; } public CucumberTestResultArchiver(String testResults, boolean ignoreBadSteps){ this(testResults); setIgnoreBadSteps(ignoreBadSteps); } @DataBoundSetter public void setIgnoreBadSteps(boolean ignoreBadSteps){ this.ignoreBadSteps = ignoreBadSteps; } public boolean getIgnoreBadSteps(){ return ignoreBadSteps; } @Override @SuppressFBWarnings(value={"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"}, justification="whatever") public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { return publishReport(build, build.getWorkspace(), launcher, listener); } @Override public void perform(Run<?, ?> run, FilePath filePath, Launcher launcher, TaskListener taskListener) throws InterruptedException, IOException { publishReport(run, filePath, launcher, taskListener); } @SuppressFBWarnings(value={"RV_RETURN_VALUE_IGNORED_BAD_PRACTICE"}, justification="move to java.nio for file stuff") public boolean publishReport(Run<?, ?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws InterruptedException, IOException { // listener.getLogger().println(Messages.JUnitResultArchiver_Recording()); CucumberTestResultAction action; final String _testResults = build.getEnvironment(listener).expand(this.testResults); CucumberJSONParser parser = new CucumberJSONParser(ignoreBadSteps); CucumberTestResult result = parser.parseResult(_testResults, build, workspace, launcher, listener); // TODO - look at all of the Scenarios and see if there are any embedded items contained with in them String remoteTempDir = launcher.getChannel().call(new TmpDirCallable()); // if so we need to copy them to the master. for (FeatureResult f : result.getFeatures()) { for (ScenarioResult s : f.getScenarioResults()) { for (EmbeddedItem item : s.getEmbeddedItems()) { // this is the wrong place to do the copying... // XXX Need to do something with MasterToSlaveCallable to makesure we are safe from evil // injection FilePath srcFilePath = new FilePath(launcher.getChannel(), remoteTempDir + '/' + item.getFilename()); // XXX when we support the workflow we will need to make sure that these files do not clash.... File destRoot = new File(build.getRootDir(), "/cucumber/embed/" + f.getSafeName() + '/' + s .getSafeName() + '/'); destRoot.mkdirs(); File destFile = new File(destRoot, item.getFilename()); if (!destFile.getAbsolutePath().startsWith(destRoot.getAbsolutePath())) { // someone is trying to trick us into writing abitrary files... throw new IOException("Exploit attempt detected - Build attempted to write to " + destFile.getAbsolutePath()); } FilePath destFilePath = new FilePath(destFile); srcFilePath.copyTo(destFilePath); srcFilePath.delete(); } } } action = build.getAction(CucumberTestResultAction.class); if (action == null) { action = new CucumberTestResultAction(build, result, listener); CHECKPOINT.block(); //build.addAction(action); CHECKPOINT.report(); } else { CHECKPOINT.block(); action.mergeResult(result, listener); build.save(); CHECKPOINT.report(); } // action.setHealthScaleFactor(getHealthScaleFactor()); // overwrites previous value if appending if (result.getPassCount() == 0 && result.getFailCount() == 0 && result.getSkipCount() == 0) throw new AbortException("No cucumber scenarios appear to have been run."); if (action.getResult().getTotalCount() == action.getResult().getFailCount()){ build.setResult(Result.FAILURE); } else if (action.getResult().getFailCount() > 0) { build.setResult(Result.UNSTABLE); } return true; } /** * This class does explicit checkpointing. */ public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } public String getTestResults() { return testResults; } @Override public Collection<Action> getProjectActions(AbstractProject<?, ?> project) { return Collections.<Action> singleton(new TestResultProjectAction((Job)project)); } public MatrixAggregator createAggregator(MatrixBuild build, Launcher launcher, BuildListener listener) { return new TestResultAggregator(build, launcher, listener); } /** * Test result tracks the diff from the previous run, hence the checkpoint. */ private static final CheckPoint CHECKPOINT = new CheckPoint("Cucumber result archiving"); private static final long serialVersionUID = 1L; public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } /** * {@link Callable} that gets the temporary directory from the node. */ private final static class TmpDirCallable extends MasterToSlaveCallable<String, InterruptedException> { private static final long serialVersionUID = 1L; @Override public String call() { return System.getProperty("java.io.tmpdir"); } } @Extension @Symbol("cucumber") public static class DescriptorImpl extends BuildStepDescriptor<Publisher> { public String getDisplayName() { return "Publish Cucumber test result report"; } @Override public Publisher newInstance(StaplerRequest req, JSONObject formData) throws hudson.model.Descriptor.FormException { String testResults = formData.getString("testResults"); boolean ignoreBadSteps = formData.getBoolean("ignoreBadSteps"); LOGGER.fine("ignoreBadSteps = "+ ignoreBadSteps); return new CucumberTestResultArchiver(testResults, ignoreBadSteps); } /** * Performs on-the-fly validation on the file mask wildcard. */ public FormValidation doCheckTestResults(@AncestorInPath AbstractProject project, @QueryParameter String value) throws IOException { if (project != null) { return FilePath.validateFileMask(project.getSomeWorkspace(), value); } return FormValidation.ok(); } public boolean isApplicable(Class<? extends AbstractProject> jobType) { return true; } } }