/* * 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.model.AbstractBuild; import hudson.model.Run; import hudson.tasks.test.TestResult; import hudson.tasks.test.AbstractTestResultAction; import hudson.tasks.test.MetaTabulatedResult; import hudson.tasks.test.TestObject; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import java.util.*; import java.util.logging.Logger; /** * Represents a list of several test results that share a common label. * It is designed to be used for children of MetaLabeledTestResultGroup. */ @ExportedBean public class LabeledTestResultGroup extends MetaTabulatedResult { /** * We expect a maximum of one AbstractTestResult per parser class. */ protected List<TestResult> children = null; protected transient List<TestResult> childrenWithFailures = null; protected transient List<TestResult> passedChildren = null; protected String label; protected int failCount = 0; protected int skipCount = 0; protected int passCount = 0; protected int totalCount = 0; protected float duration = 0; protected TestResult parent; // TODO: This should be transient. protected String description = ""; protected transient boolean cacheDirty = true; protected Map<String, TestResult> childrenByName; protected Map<TestResult, String> nameToChildMap; protected boolean namesHaveBeenSet = false; private static final Logger LOGGER = Logger.getLogger(LabeledTestResultGroup.class.getName()); public LabeledTestResultGroup() { this(null, "unlabeled", new ArrayList<TestResult>()); } public LabeledTestResultGroup(TestResult parent, String label, List<TestResult> children) { this.parent = parent; this.label = label; this.children = children; childrenWithFailures = new ArrayList<TestResult>(); passedChildren = new ArrayList<TestResult>(); namesHaveBeenSet = false; childrenByName = null; // we'll instantiate this just-in-time nameToChildMap = null; // we'll instantiate this just-in-time cacheDirty = true; } @Override public void setParentAction(AbstractTestResultAction action) { for (TestResult result : children) { result.setParentAction(action); } } @Exported(visibility=99) @Override public int getPassCount() { if (cacheDirty) updateCache(); return passCount; } @Exported(visibility=99) @Override public int getSkipCount() { if (cacheDirty) updateCache(); return skipCount; } @Exported(visibility=99) @Override public int getFailCount() { if (cacheDirty) updateCache(); return failCount; } @Exported(visibility=99) public String getLabel() { return label; } @Override public String getName() { return label; } public String getDisplayNameForChild(TestResult c) { if (!namesHaveBeenSet) lockInNames(); String niceName = nameToChildMap.get(c); if (niceName == null) { String msg = "LabeledTestResultGroup can't find a name for test child: " + c.toPrettyString(); LOGGER.severe(msg); System.err.println(msg); return "no_such_child"; } return niceName; } public TestResult getChildByIndex(int i) { if (i < 0 || i >= children.size()) { String msg = "Requested child with index " + i + " but only " + children.size() + "children exist"; LOGGER.severe(msg); throw new NoSuchElementException(msg); } return children.get(i); } @Override public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) { if (cacheDirty) updateCache(); if (!namesHaveBeenSet) { String msg = "trouble: we're in LabeledTestResultGroup.getDynamic, but we haven't created a name map yet."; LOGGER.severe(msg); throw new RuntimeException(msg); } // If there's a test with that name, serve up that test. TestResult thatOne = childrenByName.get(token); if (thatOne != null) { return thatOne; } else { Object result = super.getDynamic(token, req, rsp); if (result != null) { return result; } else { return new Run.RedirectUp(); } } } /** * Allow the object to rebuild its internal data structures when it is deserialized. */ public Object readResolve() { childrenWithFailures = new ArrayList<TestResult>(); passedChildren = new ArrayList<TestResult>(); // TODO: should I lockInNames here? Probably. updateCache(); return this; } @Override public void tally() { // Always update, even if the cache isn't marked dirty. updateCache(); } protected void updateCache() { // clean out all resutls failCount = 0; skipCount = 0; passCount = 0; totalCount = 0; childrenWithFailures.clear(); passedChildren.clear(); AbstractTestResultAction parentAction = (parent == null ? null : parent.getTestResultAction()); // not cool, but when we're in readResolve, we don't have much choice. float durationAccum = 0.0f; for (TestResult r : children) { r.setParentAction(parentAction); r.setParent(this); r.tally(); durationAccum += r.getDuration(); passCount += r.getPassCount(); failCount += r.getFailCount(); skipCount += r.getSkipCount(); if (r.isPassed()) { passedChildren.add(r); } else if (r.getFailCount() > 0) { childrenWithFailures.add(r); } } duration = durationAccum; totalCount = passCount + failCount + skipCount; cacheDirty = false; } @Override public Collection<? extends TestResult> getFailedTests() { if (cacheDirty) updateCache(); return childrenWithFailures; } @Exported(visibility=99) @Override public Collection<? extends TestResult> getChildren() { if (cacheDirty) updateCache(); return children; } @Override public boolean hasChildren() { if (cacheDirty) updateCache(); return children.size() > 0; } @Override public AbstractBuild<?, ?> getOwner() { if (parent == null) return null; return parent.getOwner(); } @Override public TestObject getParent() { return parent; } @Override public float getDuration() { if (cacheDirty) updateCache(); return duration; } @Exported(visibility=99) public String getDisplayName() { return label; } /** * Add the result, unless we've already got it * @param result */ public void addResult(TestResult result) { if (!children.contains(result)) { children.add(result); cacheDirty = true; } } /** * Add the children from this group, unless we've already got them * @param group */ public void addAll(LabeledTestResultGroup group) { for (TestResult r : group.getChildren()) { if (!children.contains(r)) { children.add(r); cacheDirty = true; } } } @Override public TestResult getPreviousResult() { if (parent==null) { LOGGER.warning("Can't getPreviousResult; parent was null."); return null; } AbstractBuild<?,?> b = parent.getOwner(); if (b==null) { LOGGER.warning("Can't getPreviousResult; parent.getOwner() was null"); return null; } while(true) { AbstractBuild<?,?> n = b; b = b.getPreviousBuild(); if(b==null) { if (n.getNumber()!=1) { LOGGER.warning("Can't getPreviousResult; no previousBuild can be found on build " + n.getNumber() + "."); } return null; } MetaLabeledTestResultGroupAction r = b.getAction(MetaLabeledTestResultGroupAction.class); if(r!=null) { return r.getLabeledTestResultGroup(label); } // If we get to here, then there was no LabeledGroupsTestResultAction on the previous build. // Should we give up and return null, or walk to a previous build? The core code look // farther back, so let's do that, too. } } @Override public TestResult getResultInBuild(AbstractBuild<?,?> build) { MetaLabeledTestResultGroupAction action = build.getAction(MetaLabeledTestResultGroupAction.class); if (action == null) { // Fall back to any AbstractTestResultAction if we are showing the // unit group, for historical purposes. if (label.equals("unit")) { AbstractTestResultAction tra = build.getAction(AbstractTestResultAction.class); if (tra == null) { return null; } return (TestResult)tra.getResult(); } else { return null; } } return action.getLabeledTestResultGroup(label); } @Override public TestResult findCorrespondingResult(String id) { String childName; String remainingId = null; int childNameEnd = id.indexOf('/'); if (childNameEnd < 0) { childName = id; remainingId = null; } else { childName = id.substring(0, childNameEnd); if (childNameEnd != id.length()) { remainingId = id.substring(childNameEnd + 1); } } TestResult child = childrenByName.get(childName); if (child != null) { if (remainingId != null) { return child.findCorrespondingResult(remainingId); } else { return child; } } return null; } @Override public String toPrettyString() { if (cacheDirty) updateCache(); StringBuilder sb = new StringBuilder(); for (TestResult r: children) { sb.append("\t").append(label); sb.append(": ").append(r.toPrettyString()); } return sb.toString(); } public int getPassDiff() { TestResult prev = getPreviousResult(); if (prev==null) return getPassCount(); return getPassCount() - prev.getPassCount(); } public int getSkipDiff() { TestResult prev = getPreviousResult(); if (prev==null) return getSkipCount(); return getSkipCount() - prev.getSkipCount(); } public int getFailDiff() { TestResult prev = getPreviousResult(); if (prev==null) return getFailCount(); return getFailCount() - prev.getFailCount(); } public int getTotalDiff() { TestResult prev = getPreviousResult(); if (prev==null) return getTotalCount(); return getTotalCount() - prev.getTotalCount(); } /** * This method records a unique name for each child result. * Either this *or* lockInNames should be called. setNameMap is much better, * This method allows the caller to provide meaningful names based on information * that will not be available later. See ${@link LabeledTestResultGroupPublisher :peform} */ public void setNameMap(HashMap<TestResult, String> resultToNameMap) { if (namesHaveBeenSet || (childrenByName != null) || (nameToChildMap != null)) { String msg = "LabeledTestResultGroup is in a bad state. setNameMap called, but we already have a name map."; LOGGER.severe(msg); System.out.println(msg); throw new RuntimeException(msg); } childrenByName = new HashMap<String, TestResult>( totalCount ); nameToChildMap = new HashMap<TestResult, String>( totalCount ); // Go through each of the results that we already have. for (TestResult r : children) { // Look up the name for that result group, as specified in the map passed in String name = resultToNameMap.get(r); if (name==null) { String msg = "LabeledTestResultGroup.setNameMap: can't find a name for that test result."; LOGGER.severe(msg); System.err.println(msg); } // Store the mapping from name to result, and from result to name, in our two maps. childrenByName.put(name, r); nameToChildMap.put(r, name); } namesHaveBeenSet = true; } /** * Either this *or* setNameMap should be called. Both record a unique name for * each child result. This method generates names based only on the information it * has available to it, while setNameMap can use additional information available * outside this class. * I don't expect that this will be called in the normal course of using the * LabeledResultGroupArchiver, but I've implemented it so that we can still * drill down to chilldren of the result group even if the name map was somehow * lost or corrupted. */ protected void lockInNames() { if (namesHaveBeenSet || (childrenByName != null) || (nameToChildMap != null)) { String msg = "LabeledTestResultGroup is in a bad state. lockInNames is being called, but names have already been set."; LOGGER.severe(msg); System.out.println(msg); throw new RuntimeException(msg); } LOGGER.warning("Using lockInNames to build a name map. setNameMap is preferred."); childrenByName = new HashMap<String, TestResult>( totalCount ); nameToChildMap = new HashMap<TestResult, String>( totalCount ); int i = 0; for (TestResult aResult : children) { StringBuilder sb = new StringBuilder(); sb.append("result-").append(i); String niceChildName = sb.toString(); childrenByName.put(niceChildName, aResult); nameToChildMap.put(aResult, niceChildName); } namesHaveBeenSet = true; } }