/* * The MIT License * * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Martin Eigenbrodt, Peter Hayes * * 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 ncover; 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.Hudson; import hudson.model.ProminentProjectAction; import hudson.model.Result; import hudson.model.Run; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Publisher; import hudson.tasks.Recorder; import hudson.util.FormValidation; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.util.ArrayList; import javax.servlet.ServletException; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; /** * Saves NCover coverage for the project and publish them. * * @author Kohsuke Kawaguchi * @author Mike Rooney */ public class NCoverArchiver extends Recorder { /** * Path to the coverage directory in the workspace. */ private final String coverageDir; /** * The file to use as the index of the directory. */ private final String indexFileName; /** * If true, retain coverage for all the successful builds. */ private final boolean keepAll; @DataBoundConstructor public NCoverArchiver(String coverage_dir, String index_file_name, boolean keep_all) { this.coverageDir = coverage_dir; this.indexFileName = index_file_name; this.keepAll = keep_all; } public String getCoverageDir() { return coverageDir; } public String getIndexFileName() { return indexFileName; } public boolean isKeepAll() { return keepAll; } /** * Gets the directory where the NCover coverage is stored for the given project. */ private static File getNCoverDir(AbstractItem project) { return new File(project.getRootDir(), "ncover"); } private static void writeFile(ArrayList<String> lines, File path) throws IOException { BufferedWriter bw = new BufferedWriter(new FileWriter(path)); for (int i = 0; i < lines.size(); i++) { bw.write(lines.get(i)); bw.newLine(); } bw.close(); return; } /** * Gets the directory where the NCover coverage is stored for the given build. */ private static File getDir(Run run) { return new File(run.getRootDir(),"ncover"); } public ArrayList<String> readFile(String filePath) throws java.io.FileNotFoundException, java.io.IOException { ArrayList<String> aList = new ArrayList<String>(); try { final InputStream is = this.getClass().getResourceAsStream(filePath); try { final Reader r = new InputStreamReader(is); try { final BufferedReader br = new BufferedReader(r); try { String line = null; while ((line = br.readLine()) != null) { aList.add(line); } br.close(); r.close(); is.close(); } finally { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } finally { try { r.close(); } catch (IOException e) { e.printStackTrace(); } } } finally { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } catch (IOException e) { // failure e.printStackTrace(); } return aList; } @Override public boolean perform(AbstractBuild<?,?> build, Launcher launcher, BuildListener listener) throws InterruptedException { listener.getLogger().println("Publishing NCover HTML report..."); FilePath ncover = build.getWorkspace().child(coverageDir); FilePath target = new FilePath(keepAll ? getDir(build) : getNCoverDir(build.getProject())); // Grab the contents of the header and footer as arrays ArrayList<String> headerLines; ArrayList<String> footerLines; try { headerLines = readFile("/ncover/NCoverArchiver/header.html"); footerLines = readFile("/ncover/NCoverArchiver/footer.html"); } catch (FileNotFoundException e1) { e1.printStackTrace(); return false; } catch (IOException e1) { e1.printStackTrace(); return false; } // The index name might be a comma separated list of names, so let's figure out all the pages we should index. // Why yes, this would be one line of Python: reports = [report.strip() for report in indexFileName.split(",") if report] String[] csvReports = indexFileName.split(","); ArrayList<String> reports = new ArrayList<String>(); for (int i=0; i<csvReports.length; i++) { String report = csvReports[i]; report = report.trim(); if (!report.equals("")) { reports.add(report); listener.getLogger().println("Report: '"+report+"'"); String tabNo = "tab" + (i+1); // Make the report name the filename without the extesion. String reportName = report.substring(0, report.lastIndexOf(".")); String tabItem = "<li id=\""+tabNo+"\" class=\"unselected\" onclick=\"updateBody('"+tabNo+"');\" value=\""+report+"\">"+reportName+"</li>"; headerLines.add(tabItem); } } // Add the JS to change the link as appopriate. String hudsonUrl = Hudson.getInstance().getRootUrl(); headerLines.add("<script type=\"text/javascript\">document.getElementById(\"hudson_link\").href=\"" + hudsonUrl +"\";</script>"); try { if (!ncover.exists()) { listener.error("Specified NCover directory '" + coverageDir + "' does not exist."); build.setResult(Result.FAILURE); return true; } else if (!keepAll) { // We are only keeping one copy at the project level, so remove the old one. target.deleteRecursive(); } if (ncover.copyRecursiveTo("**/*",target)==0) { listener.error("Directory '" + ncover + "' exists but failed copying to '" + target + "'."); if(build.getResult().isBetterOrEqualTo(Result.UNSTABLE)) { // If the build failed, don't complain that there was no coverage. // The build probably didn't even get to the point where it produces coverage. listener.error("This is especially strange since your build otherwise succeeded."); } build.setResult(Result.FAILURE); return true; } } catch (IOException e) { Util.displayIOException(e,listener); e.printStackTrace(listener.fatalError("NCover failure")); build.setResult(Result.FAILURE); return true; } // Add build action, if coverage is recorded for each build if(keepAll) build.addAction(new NCoverBuildAction(build)); // Now add the footer. headerLines.addAll(footerLines); // And write this as the index try { writeFile(headerLines, new File(target.getRemote(), "coverage-wrapper.html")); } catch (IOException e) { e.printStackTrace(); } return true; } public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.BUILD; } @Override public Action getProjectAction(AbstractProject project) { return new NCoverAction(project); } protected static abstract class BaseNCoverAction implements Action { public String getUrlName() { return "ncover"; } public String getDisplayName() { /* if (new File(dir(), "help-doc.html").exists()) return Messages.JavadocArchiver_DisplayName_Javadoc(); else return Messages.JavadocArchiver_DisplayName_Generic(); */ return "Code Coverage"; } public String getIconFileName() { return "graph.gif"; } /** * Serves NCover coverage. */ public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { DirectoryBrowserSupport dbs = new DirectoryBrowserSupport(this, new FilePath(dir()), getTitle(), "graph.gif", false); dbs.setIndexFileName("coverage-wrapper.html"); // Hudson >= 1.312 dbs.generateResponse(req,rsp,this); } protected abstract String getTitle(); protected abstract File dir(); } public static class NCoverAction extends BaseNCoverAction implements ProminentProjectAction { private final AbstractItem project; public NCoverAction(AbstractItem project) { this.project = project; } @Override protected File dir() { // Would like to change AbstractItem to AbstractProject, but is // that a backwards compatible change? if (project instanceof AbstractProject) { AbstractProject abstractProject = (AbstractProject) project; Run run = abstractProject.getLastSuccessfulBuild(); if (run != null) { File javadocDir = getDir(run); if (javadocDir.exists()) return javadocDir; } } return getNCoverDir(project); } @Override protected String getTitle() { return project.getDisplayName()+" ncover2"; } } public static class NCoverBuildAction extends BaseNCoverAction { private final AbstractBuild<?,?> build; public NCoverBuildAction(AbstractBuild<?,?> build) { this.build = build; } @Override protected String getTitle() { return build.getDisplayName()+" ncover3"; } @Override protected File dir() { return getDir(build); } } @Extension public static class DescriptorImpl extends BuildStepDescriptor<Publisher> { @Override public String getDisplayName() { //return Messages.JavadocArchiver_DisplayName(); return "Publisher NCover HTML report"; } /** * 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(); } @Override public boolean isApplicable(Class<? extends AbstractProject> jobType) { return true; } } }