package org.jvnet.hudson.plugins.fit; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.AbstractItem; import hudson.model.AbstractProject; import hudson.model.Action; import hudson.model.BuildListener; import hudson.model.DirectoryBrowserSupport; import hudson.model.ProminentProjectAction; import hudson.model.Result; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Publisher; import hudson.tasks.Recorder; import hudson.util.FormValidation; import java.io.File; import java.io.IOException; import javax.servlet.ServletException; import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; import org.jvnet.hudson.plugins.fit.HtmlContentHandler.FitResult; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; /** * <p> * When the user configures the project and enables this builder, * {@link DescriptorImpl#newInstance(StaplerRequest)} is invoked and a new * {@link FitArchiver} is created. The created instance is persisted to the * project configuration XML by using XStream, so this allows you to use * instance fields (like {@link #pathToHtml}) to remember the configuration. * * <p> * When a build is performed, the * {@link #perform(AbstractBuild, Launcher, BuildListener)} method will be invoked. * * @author Eric Lefevre */ public class FitArchiver extends Recorder { private static final String PLUGIN_NAME = "fit"; private static final String DOT_HTML = ".html"; private static final String INDEX_HTML = "index" + DOT_HTML; private final String pathToHtml; FitArchiver(String pathToHtml) { this.pathToHtml = pathToHtml; } /** * We'll use this from the <tt>config.jelly</tt>. */ public String getPathToHtml() { return pathToHtml; } /** * Gets the directory where the files will be archived. */ private static File getTargetDir(AbstractItem project) { return new File(project.getRootDir(), PLUGIN_NAME); } public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.BUILD; } @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException { FilePath sourceDirectory = build.getWorkspace().child( pathToHtml); FilePath targetDirectory = new FilePath(getTargetDir(build.getParent())); try { // if the build has failed, then there's not much point in reporting // an error saying fit directory doesn't exist. We want the // user to focus on the real error, which is the build failure. if (build.getResult().isWorseOrEqualTo(Result.FAILURE) && !sourceDirectory.exists()) { listener.getLogger().println( Messages.FitPlugin_CouldNotArchiveFitTests()); return true; } listener .getLogger() .println( Messages .FitPlugin_DeletingContentOfArchiveDirectory(targetDirectory)); targetDirectory.deleteContents(); listener.getLogger().println( Messages.FitPlugin_CopyingFromSouceToTargetDirectory( sourceDirectory, targetDirectory)); sourceDirectory.copyRecursiveTo("**/*", targetDirectory); FilePath indexHtml = targetDirectory.child(INDEX_HTML); if (indexHtml.exists()) { String msg = indexHtml.getName() + " already present: no file will be generated"; listener.getLogger().println(msg); } else { generateIndexHtml(listener, targetDirectory); } } catch (IOException e) { Util.displayIOException(e, listener); build.setResult(Result.FAILURE); } return true; } /** * Generates an index file for the Fit results. It is necessary to have at * least one index.html or the web server will show an error. * <p> * The index file will list all HTML files, and show the number of errors * and failures. */ private void generateIndexHtml(BuildListener listener, FilePath reportDirectory) throws IOException, InterruptedException { FilePath[] htmlReports = reportDirectory.list("*" + DOT_HTML); String content = ""; for (FilePath filePath : htmlReports) { listener.getLogger().println("Now parsing " + filePath.getRemote()); FitResult fitResult = HtmlContentHandler.parse(new File(filePath .getRemote())); content += "<li>"; content += "<a href='" + filePath.getName() + "'>"; content += StringUtils.removeEnd(filePath.getName(), DOT_HTML); content += "</a> "; // content += fitResult.getErrorsNumber() + " " // + getPlural(fitResult.getErrorsNumber(), "error", "errors") // + ", "; // content += fitResult.getExpectationsNumber() // + " " // + getPlural(fitResult.getExpectationsNumber(), "failure", // "failures"); } FilePath tempFile = reportDirectory.createTextTempFile("temp", "txt", content); FilePath indexHtml = reportDirectory.child(INDEX_HTML); tempFile.copyTo(indexHtml); listener.getLogger() .println(Messages.FitPlugin_IndexCreated(indexHtml)); } private String getPlural(int number, String singular, String plural) { if (number <= 1) { return singular; } else { return plural; } } /** {@inheritDoc} */ @Override public Action getProjectAction(final AbstractProject<?, ?> project) { return new FitAction(project); } /** * Descriptor for {@link FitArchiver}. Used as a singleton. The class is * marked as public so that it can be accessed from views. * * <p> * See <tt>views/hudson/plugins/hello_world/HelloWorldBuilder/*.jelly</tt> * for the actual HTML fragment for the configuration screen. */ @Extension public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> { public DescriptorImpl() { super(FitArchiver.class); } /** * This human readable name is used in the configuration screen. */ public String getDisplayName() { return Messages.FitPlugin_PublisherDisplayName(); } /** {@inheritDoc} */ @Override public String getHelpFile() { return "/plugin/fit/help.html"; } /** * Creates a new instance of {@link FitArchiver} from a submitted form. */ @Override public FitArchiver newInstance(StaplerRequest req, JSONObject formData) throws FormException { String param1FromJellyFile = "fit.pathToHtml"; return new FitArchiver(req.getParameter(param1FromJellyFile)); } /** * Performs on-the-fly validation on the file mask wildcard. */ public FormValidation doCheck(@AncestorInPath AbstractProject project, @QueryParameter String value) throws IOException, ServletException { FilePath ws = project.getSomeWorkspace(); return ws != null ? ws.validateRelativeDirectory(value) : FormValidation.ok(); } @SuppressWarnings("unchecked") public boolean isApplicable(Class<? extends AbstractProject> jobType) { // this option should be available whether the job is Maven or not return true; } } public static final class FitAction implements ProminentProjectAction { private static final String ICON_FILENAME = "orange-square.gif"; private static final long serialVersionUID = 4399590075673857468L; private final AbstractItem project; public FitAction(AbstractItem project) { this.project = project; } public String getUrlName() { return PLUGIN_NAME; } public String getDisplayName() { return Messages.FitPlugin_ActionDisplayName(); } public String getIconFileName() { if (getTargetDir(project).exists()) { return ICON_FILENAME; } else { // hide it since we don't have fit reports yet. return null; } } public DirectoryBrowserSupport doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, InterruptedException { // handles the conversion of the URL into a proper local directory String title = project.getDisplayName() + " " + PLUGIN_NAME; FilePath systemDirectory = new FilePath(getTargetDir(project)); return new DirectoryBrowserSupport(this, systemDirectory, title, ICON_FILENAME, false); } } }