package hudson.plugins.clover;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildWrapperDescriptor;
import hudson.Launcher;
import hudson.Proc;
import hudson.FilePath;
import hudson.Extension;
import hudson.Util;
import hudson.util.DescribableList;
import hudson.remoting.Channel;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.Run;
import hudson.model.Descriptor;
import hudson.model.AbstractProject;
import hudson.model.Hudson;
import hudson.model.Project;
import hudson.model.FreeStyleProject;
import hudson.model.Action;
import java.io.IOException;
import java.io.OutputStream;
import java.io.File;
import java.util.Map;
import java.util.List;
import java.util.LinkedList;
import java.util.Arrays;
import java.util.ArrayList;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.DataBoundConstructor;
import net.sf.json.JSONObject;
import com.atlassian.clover.api.ci.CIOptions;
import com.atlassian.clover.api.ci.Integrator;
/**
* A BuildWrapper that decorates the command line just before a build starts with targets and properties that will automatically
* integrate Clover into the Ant build.
*/
public class CloverBuildWrapper extends BuildWrapper {
public boolean historical = true;
public boolean json = true;
public String licenseCert;
@DataBoundConstructor
public CloverBuildWrapper(boolean historical, boolean json, String licenseCert) {
this.historical = historical;
this.json = json;
this.licenseCert = licenseCert;
}
@Override
public Environment setUp(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException {
addCloverPublisher(build, listener);
return new Environment() {};
}
private void addCloverPublisher(AbstractBuild build, BuildListener listener) throws IOException {
DescribableList publishers = build.getProject().getPublishersList();
if (!publishers.contains(CloverPublisher.DESCRIPTOR)) {
final String reportDir = "clover";
listener.getLogger().println("Adding Clover Publisher with reportDir: " + reportDir);
build.getProject().getPublishersList().add(new CloverPublisher(reportDir, null));
}
}
@Override
public Action getProjectAction(AbstractProject job) {
// ensure only one project action exists on the project
if (job.getAction(CloverProjectAction.class) == null) {
return new CloverProjectAction((Project) job);
}
return super.getProjectAction(job);
}
@Override
public Launcher decorateLauncher(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException, Run.RunnerAbortedException {
final DescriptorImpl descriptor = Hudson.getInstance().getDescriptorByType(DescriptorImpl.class);
final String license = Util.nullify(licenseCert) == null ? descriptor.licenseCert : licenseCert;
final CIOptions.Builder options = new CIOptions.Builder().
json(this.json).
historical(this.historical).
fullClean(true);
final Launcher outer = launcher;
return new CloverDecoratingLauncher(outer, options, license);
}
public static final Descriptor<BuildWrapper> DESCRIPTOR = new DescriptorImpl();
/**
* Descriptor for {@link CloverPublisher}. Used as a singleton. The class is marked as public so that it can be
* accessed from views.
* <p/>
* <p/>
* See <tt>views/hudson/plugins/clover/CloverPublisher/*.jelly</tt> for the actual HTML fragment for the
* configuration screen.
*/
@Extension
public static final class DescriptorImpl extends BuildWrapperDescriptor {
public String licenseCert;
public DescriptorImpl() {
super(CloverBuildWrapper.class);
load();
}
/**
* This human readable name is used in the configuration screen.
*/
public String getDisplayName() {
return "<img src='"+CloverProjectAction.ICON+"' height='24'/> Automatically record and report Code Coverage using <a href='http://atlassian.com/clover'>Clover.</a>. Currently for Ant builds only.";
}
@Override
public String getHelpFile() {
return "/plugin/clover/help-cloverConfig.html";
}
@Override
public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
req.bindParameters(this, "clover.");
save();
return true;
}
public boolean isApplicable(AbstractProject item) {
// TODO: is there a better way to detect Ant builds?
// should only be enabled for Ant projects.
return (item instanceof FreeStyleProject);
}
}
public static class CloverDecoratingLauncher extends Launcher {
private final Launcher outer;
private final CIOptions.Builder options;
private final String license;
public CloverDecoratingLauncher(Launcher outer, CIOptions.Builder options, String license) {
super(outer);
this.outer = outer;
this.options = options;
this.license = license;
}
@Override
public Proc launch(ProcStarter starter) throws IOException {
decorateArgs(starter);
return outer.launch(starter);
}
public void decorateArgs(ProcStarter starter) throws IOException {
List<String> userArgs = new LinkedList<String>();
List<String> preSystemArgs = new LinkedList<String>();
List<String> postSystemArgs = new LinkedList<String>();
final List<String> cmds = new ArrayList<String>();
cmds.addAll(starter.cmds());
// on windows - the cmds are wrapped of the form:
// "cmd.exe", "/C", "\"ant.bat clean test.run && exit %%ERRORLEVEL%%\""
// this hacky code is used to parse out just the user specified args. ie clean test.run
final int numPreSystemCmds = 2; // hack hack hack - there are 2 commands prepended on windows...
final String sysArgSplitter = "&&";
if (!cmds.isEmpty() && cmds.size() >= numPreSystemCmds && !cmds.get(0).endsWith("ant"))
{
preSystemArgs.addAll(cmds.subList(0, numPreSystemCmds));
// get the index of the "ant.bat
String argString = cmds.get(numPreSystemCmds);
// trim leading and trailing " if they exist...
argString = argString.replaceAll("\"", "");
String[] tokens = argString.split(" ");
preSystemArgs.add(tokens[0]);
for (int i = 1; i < tokens.length; i++)
{ // chop the ant.bat
String arg = tokens[i];
if (sysArgSplitter.equals(arg))
{
// anything after the &&, break.
postSystemArgs.addAll(Arrays.asList(tokens).subList(i, tokens.length));
break;
}
userArgs.add(arg);
}
}
else
{
if (cmds.size() > 0)
{
preSystemArgs.add(cmds.get(0));
}
if (cmds.size() > 1)
{
userArgs.addAll(cmds.subList(1, cmds.size()));
}
}
if (!userArgs.isEmpty())
{
// TODO: full clean needs to be an option. see http://jira.atlassian.com/browse/CLOV-736
options.fullClean(true);
setupLicense(starter);
Integrator integrator = Integrator.Factory.newAntIntegrator(options.build());
integrator.decorateArguments(userArgs);
starter.cmds(new ArrayList<String>());
// re-assemble all commands
List<String> allCommands = new ArrayList<String>();
allCommands.addAll(preSystemArgs);
allCommands.addAll(userArgs);
allCommands.addAll(postSystemArgs);
starter.cmds(allCommands);
// masks.length must equal cmds.length
boolean[] masks = new boolean[starter.cmds().size()];
for (int i = 0; i < starter.masks().length; i++) {
masks[i] = starter.masks()[i];
}
starter.masks(masks);
}
}
private void setupLicense(ProcStarter starter) throws IOException {
if (license == null) {
listener.getLogger().println("No Clover license configured. Please download a free 30 day license from http://my.atlassian.com.");
return;
}
// create a tmp license file.
FilePath licenseFile = new FilePath(starter.pwd(), ".clover/clover.license");
try {
licenseFile.write(license, "UTF-8");
options.license(new File(licenseFile.toURI()));
} catch (InterruptedException e) {
listener.getLogger().print("Could not create license file at: " + licenseFile + ". Setting as a system property.");
listener.getLogger().print(e.getMessage());
options.licenseCert(license);
}
}
@Override
public Channel launchChannel(String[] cmd, OutputStream out, FilePath workDir, Map<String, String> envVars) throws IOException, InterruptedException {
return outer.launchChannel(cmd, out, workDir, envVars);
}
@Override
public void kill(Map<String, String> modelEnvVars) throws IOException, InterruptedException {
outer.kill(modelEnvVars);
}
}
}