/* * The MIT License * * Copyright (c) 2010, Yahoo!, Inc. * * 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 hudson.plugins.labeledgroupedtests; import hudson.Extension; import hudson.Launcher; import hudson.matrix.MatrixAggregatable; import hudson.matrix.MatrixAggregator; import hudson.matrix.MatrixBuild; import hudson.model.*; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Publisher; import hudson.tasks.Recorder; import hudson.tasks.test.TestResult; import hudson.tasks.test.TestResultParser; import hudson.tasks.test.TestResultAggregator; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.StaplerRequest; import java.io.IOException; import java.io.Serializable; import java.util.*; import java.util.logging.Logger; public class LabeledTestResultGroupPublisher extends Recorder implements Serializable, MatrixAggregatable { private static final Logger LOGGER = Logger.getLogger(LabeledTestResultGroupPublisher.class.getName()); protected List<LabeledTestGroupConfiguration> configs; private static List<TestResultParser> testResultParsers = null; @DataBoundConstructor public LabeledTestResultGroupPublisher(List<LabeledTestGroupConfiguration> configs) { if (configs == null || configs.size() == 0) { throw new IllegalArgumentException("Null or empty list of configs passed in to LabeledTestResultGroupPublisher. Please file a bug."); } this.configs = new ArrayList<LabeledTestGroupConfiguration>(configs); discoverParsers(); } /** * Declares the scope of the synchronization monitor this {@link hudson.tasks.BuildStep} expects from outside. */ public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.BUILD; } public MatrixAggregator createAggregator(MatrixBuild build, Launcher launcher, BuildListener listener) { return new TestResultAggregator(build, launcher, listener); } public static void discoverParsers() { if (testResultParsers == null) { if (Hudson.getInstance()==null) { testResultParsers = new ArrayList<TestResultParser>(); return; // If we're not in a Hudson world, we won't have parsers. } testResultParsers = Hudson.getInstance().getExtensionList(TestResultParser.class); } } public static List<TestResultParser> getTestResultParsers() { discoverParsers(); return testResultParsers; } public static List<String> getPhases() { return Arrays.asList("unit", "smoke", "regression", "integration", "special", "misc"); } public void debugPrint() { for (LabeledTestGroupConfiguration config: configs) { LOGGER.info("got config: " + config.toString()); } for (TestResultParser parser: testResultParsers) { LOGGER.info("we have test result parser: " + parser.getClass().getName()); } } public List<LabeledTestGroupConfiguration> getConfigs() { return configs; } public void setConfigs(List<LabeledTestGroupConfiguration> configs) { this.configs = configs; } @Override public boolean perform(final AbstractBuild build, Launcher launcher, final BuildListener listener) throws InterruptedException, IOException { String startMsg = "Analyzing test results with LabeledTestResultGroupPublisher..."; listener.getLogger().println(startMsg); LOGGER.fine(startMsg); // Prepare to process results final long buildTime = build.getTimestamp().getTimeInMillis(); final long nowMaster = System.currentTimeMillis(); HashSet<String> labels = new HashSet<String>(10); HashMap<String, List<TestResult>> resultGroupsByLabel = new HashMap(3); HashMap<TestResult, String> resultsWithName = new HashMap<TestResult, String>(configs.size()); // Roll up configs so that there is zero or one config for each label/parser pair rollupConfigs(); // For each TestResults configuration, attempt to parse its results // Invoke the parser on the specified results // Label those results as the specified type (unit/smoke/regression) // Include those results in an aggregrated result for (LabeledTestGroupConfiguration config:configs) { try { String parserClassName = config.getParserClassName(); String label = config.getLabel(); labels.add(label); // adds only if not already there if (!resultGroupsByLabel.containsKey(label)) { // Make a new, empty list for that label resultGroupsByLabel.put(label, new ArrayList<TestResult>(5)); } Collection<TestResult> listForThisLabel = resultGroupsByLabel.get(label); ClassLoader uberLoader = Hudson.getInstance().getPluginManager().uberClassLoader; Class parserClass = Class.forName(parserClassName, true, uberLoader); Object parserObject = parserClass.newInstance(); String nameForThisResult = config.toNameString(); TestResult someResult = null; // Actually parse the file! // NB: we're calling a static method via an instance, because I can't figure out // how to go from the Class object to calling a static method without an instance involved. if (parserObject instanceof TestResultParser) { TestResultParser parser = (TestResultParser) parserObject; someResult = parser.parse(config.getTestResultFileMask(), build, launcher, listener); } else { LOGGER.warning("Couldn't find a parser for class: " + parserClassName); listener.getLogger().println("Couldn't find a parser for class: " + parserClassName); continue; } if (someResult != null) { listForThisLabel.add(someResult); resultsWithName.put(someResult, nameForThisResult); String msg = "Here's your result: " + someResult.toPrettyString(); listener.getLogger().println(msg); LOGGER.fine(msg); } else { String msg = "Trouble while parsing results for " + config.getTestResultFileMask() + "-- couldn't parse results."; LOGGER.warning(msg); listener.getLogger().println(msg); } } catch (IOException e) { LOGGER.warning("While processing config " + config.toString() + ":" + e.getMessage()); e.printStackTrace(); } catch (ClassNotFoundException e) { LOGGER.warning("Couldn't find parser while processing config " + config.toString() + ":" + e.getMessage()); e.printStackTrace(); } catch (IllegalAccessException e) { LOGGER.warning("Couldn't get an instance of parser while processing config " + config.toString() + ":" + e.getMessage()); e.printStackTrace(); } catch (InstantiationException e) { LOGGER.warning("Couldn't get an instance of parser while processing config " + config.toString() + ":" + e.getMessage()); e.printStackTrace(); } } // Create and populate the result that will contain all the children we parsed MetaLabeledTestResultGroup resultGroup = new MetaLabeledTestResultGroup(); for (String label: labels) { LabeledTestResultGroup group = new LabeledTestResultGroup(resultGroup, label, resultGroupsByLabel.get(label)); resultGroup.addTestResultGroup(label, group); group.setNameMap(resultsWithName); } MetaLabeledTestResultGroupAction action = new MetaLabeledTestResultGroupAction(build, resultGroup, listener); build.addAction(action); resultGroup.setParentAction(action); resultGroup.tally(); Result healthResult = determineBuildHealth(build, resultGroup); // Parsers can only decide to make the build worse than it currently is, never better. if (healthResult != null && healthResult.isWorseThan(build.getResult())) { build.setResult(healthResult); } String debugString = resultGroup.toString(); // resultGroup.toPrettyString(); LOGGER.info("Test results parsed: " + debugString); listener.getLogger().println("Test results parsed: " + debugString); return true; } /** * Roll up configs so that there is zero or one config for each label/parser pair */ private void rollupConfigs() { // Build a unique list of labels and a unique list of parsers HashSet<String> parserNames = new HashSet<String>(); HashSet<String> labelsInUse = new HashSet<String>(); for (LabeledTestGroupConfiguration config:configs) { parserNames.add(config.getParserClassName()); labelsInUse.add(config.getLabel()); } // Make one empty list of string for each label/parser pair, // and simultaneously flatten into a new, smaller list of configs List<LabeledTestGroupConfiguration> newConfigs = new ArrayList<LabeledTestGroupConfiguration>(); for (String label : labelsInUse) { for (String parserName : parserNames) { StringBuilder filemaskBuilder = new StringBuilder(); // Now, go through all of the configs searching for this label/parser combination for (LabeledTestGroupConfiguration config:configs) { if (config.getParserClassName().equals(parserName) && config.getLabel().equals(label)) { if (filemaskBuilder.length() > 0) { filemaskBuilder.append(","); } filemaskBuilder.append(config.getTestResultFileMask()); } } // At this point we have a complete list of the file masks for this label/parser combination. // Make a new config representing that unified filemask! String combinedFilemask = filemaskBuilder.toString(); if (combinedFilemask.length() > 0) { // only build a new config if there is some content in the filemask string LabeledTestGroupConfiguration newConfig = new LabeledTestGroupConfiguration(parserName, filemaskBuilder.toString(), label); newConfigs.add(newConfig); } } } // Now replace the old set of configs with the new, smaller one we just built. this.configs = newConfigs; } private Result determineBuildHealth(AbstractBuild build, MetaLabeledTestResultGroup resultGroup) { // Set build health on the basis of all configured test report groups Result worstSoFar = build.getResult(); for (TestResult result : resultGroup.getChildren()) { Result thisResult = result.getBuildResult(); if (thisResult != null && thisResult.isWorseThan(worstSoFar)) { worstSoFar = result.getBuildResult(); } } return worstSoFar; } @Override public Action getProjectAction(AbstractProject<?, ?> project) { return new MetaLabeledTestResultGroupProjectAction(project); } @Extension public static class DescriptorImpl extends BuildStepDescriptor<Publisher> { public String getDisplayName() { return "Publish Test Results in Labeled Groups"; } @Override public Publisher newInstance(StaplerRequest req, JSONObject formData) throws hudson.model.Descriptor.FormException { LOGGER.info(formData.toString()); return req.bindJSON(LabeledTestResultGroupPublisher.class, formData); } public boolean isApplicable(Class<? extends AbstractProject> jobType) { // for Maven we have SurefireArchiver that automatically kicks in. return !"AbstractMavenProject".equals(jobType.getClass().getSimpleName()); } } }