/* * 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.junit.TestAction; 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; import java.util.Collections; /** * User: Benjamin Shine bshine * Date: Oct 21, 2009 * Time: 1:47:47 PM */ @ExportedBean public class MetaLabeledTestResultGroup extends MetaTabulatedResult { protected Map<String, LabeledTestResultGroup> childrenByLabel; protected transient Map<String, Collection<TestResult>> failedTestsByLabel; protected transient Map<String, Collection<TestResult>> passedTestsByLabel; protected transient Map<String, Collection<TestResult>> skippedTestsByLabel; protected transient Collection<TestResult> allFailedTests; protected transient Collection<TestResult> allPassedTests; protected transient Collection<TestResult> allSkippedTests; protected int failCount = 0; protected int skipCount = 0; protected int passCount = 0; protected int totalCount = 0; protected float duration = 0; protected transient boolean cacheDirty = true; protected transient MetaLabeledTestResultGroupAction parentAction = null; protected String description = ""; /** Effectively overrides TestObject.id, by overriding the accessors */ protected String groupId = ""; private static final Logger LOGGER = Logger.getLogger(MetaLabeledTestResultGroup.class.getName()); public MetaLabeledTestResultGroup() { this(null, "(no description)"); // Aha! This is how this guy is created with a null parentAction! } /** * Allow the object to rebuild its internal data structures when it is deserialized. */ private Object readResolve() { failedTestsByLabel = new HashMap<String, Collection<TestResult>>(10); passedTestsByLabel = new HashMap<String, Collection<TestResult>>(10); skippedTestsByLabel = new HashMap<String, Collection<TestResult>>(10); allPassedTests = new HashSet<TestResult>(); allFailedTests = new HashSet<TestResult>(); allSkippedTests = new HashSet<TestResult>(); updateCache(); return this; } @Override public void tally() { updateCache(); } /** * The list of labels currently in use by the children * @return */ public Collection<String> getLabels() { if (cacheDirty) updateCache(); return childrenByLabel.keySet(); } @Exported(inline=true,visibility=99) public Collection<LabeledTestResultGroup> getGroups() { if (cacheDirty) updateCache(); return childrenByLabel.values(); } public LabeledTestResultGroup getGroupByLabel(String label) { if (cacheDirty) updateCache(); LabeledTestResultGroup group = childrenByLabel.get(label); return group; } public Collection<? extends TestResult> getChildrenForLabel(String label) { LabeledTestResultGroup group = getGroupByLabel(label); if (group==null) { return Collections.EMPTY_LIST; } return group.getChildren(); } @Override public MetaLabeledTestResultGroupAction getTestResultAction() { if (parentAction ==null) { LOGGER.finest("null parentAction"); } return parentAction; } /** I wish the superclass didn't call this. * FIXME. TODO. * @return */ @Override public List<TestAction> getTestActions() { return EMPTY_TEST_ACTIONS_LIST; } private static final List<TestAction> EMPTY_TEST_ACTIONS_LIST = new ArrayList<TestAction>(); public MetaLabeledTestResultGroup(MetaLabeledTestResultGroupAction parentAction, String description ) { childrenByLabel = new HashMap<String, LabeledTestResultGroup>(10); failedTestsByLabel = new HashMap<String, Collection<TestResult>>(10); passedTestsByLabel = new HashMap<String, Collection<TestResult>>(10); skippedTestsByLabel = new HashMap<String, Collection<TestResult>>(10); allPassedTests = new HashSet<TestResult>(); allFailedTests = new HashSet<TestResult>(); allSkippedTests = new HashSet<TestResult>(); this.parentAction = parentAction; this.description = description; cacheDirty = true; } public void setParentAction(MetaLabeledTestResultGroupAction parentAction) { if (this.parentAction == parentAction) { return; } this.parentAction = parentAction; // Tell all of our children about the parent action, too. for (LabeledTestResultGroup group : childrenByLabel.values()) { group.setParentAction(parentAction); } } public void addTestResult(String label, TestResult result) { if (! childrenByLabel.keySet().contains(label)) { childrenByLabel.put(label, new LabeledTestResultGroup(this, label, Arrays.asList(result))); } else { childrenByLabel.get(label).addResult(result); } cacheDirty = true; } public void addTestResultGroup(String label, LabeledTestResultGroup group) { if (! childrenByLabel.keySet().contains(label)) { childrenByLabel.put(label, group); } else { childrenByLabel.get(label).addAll(group); } cacheDirty = true; } @Override public String getTitle() { return "Test Reports"; } @Override public String getName() { return ""; } @Override public boolean isPassed() { if (cacheDirty) updateCache(); return (failCount == 0) && (skipCount == 0); } @Override public String getChildTitle() { return "Group"; } @Override public MetaLabeledTestResultGroup getPreviousResult() { // TODO: consider caching if (parentAction == null) return null; AbstractBuild<?,?> b = parentAction.owner; while(true) { b = b.getPreviousBuild(); if(b==null) return null; MetaLabeledTestResultGroupAction r = b.getAction(MetaLabeledTestResultGroupAction.class); if(r!=null) return r.getResultAsTestResultGroup(); } } public int getPassDiff() { MetaLabeledTestResultGroup prev = getPreviousResult(); if (prev==null) return getPassCount(); return getPassCount() - prev.getPassCount(); } public int getSkipDiff() { MetaLabeledTestResultGroup prev = getPreviousResult(); if (prev==null) return getSkipCount(); return getSkipCount() - prev.getSkipCount(); } public int getFailDiff() { MetaLabeledTestResultGroup prev = getPreviousResult(); if (prev==null) return getFailCount(); return getFailCount() - prev.getFailCount(); } public int getTotalDiff() { MetaLabeledTestResultGroup prev = getPreviousResult(); if (prev==null) return getTotalCount(); return getTotalCount() - prev.getTotalCount(); } @Override public TestResult getResultInBuild(AbstractBuild<?,?> build) { AbstractTestResultAction action = build.getAction(AbstractTestResultAction.class); if (action == null) { return null; } if (action instanceof MetaLabeledTestResultGroupAction) { return ((MetaLabeledTestResultGroupAction)action).getResultAsTestResultGroup(); } return (TestResult)action.getResult(); } @Override public TestResult findCorrespondingResult(String id) { String groupName; String remainingId = null; int groupNameEnd = id.indexOf('/'); if (groupNameEnd < 0) { groupName = id; remainingId = null; } else { groupName = id.substring(0, groupNameEnd); if (groupNameEnd != id.length()) { remainingId = id.substring(groupNameEnd + 1); if (remainingId.length() == 0) { remainingId = null; } } } LabeledTestResultGroup group = getGroupByLabel(groupName); if (group != null) { if (remainingId != null) { return group.findCorrespondingResult(remainingId); } else { return group; } } return null; } @Override public int getFailedSince() { // TODO: implement this. throw new UnsupportedOperationException("hudson.plugins.labeledgroupedtests.MetaLabeledTestResultGroup#getFailedSince: Not yet implemented."); // TODO: implement } @Override public Run<?,?> getFailedSinceRun() { // TODO: implement this. throw new UnsupportedOperationException("hudson.plugins.labeledgroupedtests.MetaLabeledTestResultGroup#getFailedSinceRun: Not yet implemented."); // TODO: implement } /** * Gets the number of failed tests. */ @Exported(visibility=99) @Override public int getFailCount() { if (cacheDirty) updateCache(); return failCount; } /** * Gets the total number of skipped tests * @return */ @Exported(visibility=99) public int getSkipCount() { if (cacheDirty) updateCache(); return skipCount; } /** * Gets the number of passed tests. */ @Exported(visibility=99) @Override public int getPassCount() { if (cacheDirty) updateCache(); return passCount; } @Override public Collection<? extends TestResult> getFailedTests() { // BAD result to force problems -- this method is now effectively UNIMPLEMENTED LOGGER.severe("getFailedTests unimplemented. Expect garbage."); if (cacheDirty) updateCache(); return allFailedTests; } @Override public Collection<? extends TestResult> getSkippedTests() { LOGGER.severe("getSkippedTests unimplemented. Expect garbage."); if (cacheDirty) updateCache(); return allSkippedTests; } @Override public Collection<? extends TestResult> getPassedTests() { LOGGER.severe("getSkippedTests unimplemented. Expect garbage."); if (cacheDirty) updateCache(); return allPassedTests; } @Override public Collection<? extends TestResult> getChildren() { if (cacheDirty) updateCache(); return flattenTopTier(childrenByLabel.values()); } @Override public boolean hasChildren() { if (cacheDirty) updateCache(); return (totalCount != 0); } public AbstractBuild<?, ?> getOwner() { if (parentAction != null) return parentAction.owner; else return null; } /** * Strange API. A LabeledTestResultGroup is always the direct child of an * action, so we'll just return null here, for the parent. This is the * same behavior as TestResult. * @return */ @Override public TestObject getParent() { return null; } @Exported(visibility=99) @Override public float getDuration() { if (cacheDirty) updateCache(); return duration; } @Exported(visibility=99) /* @Override */ public String getDisplayName() { return "Test Result Groups"; } @Exported(visibility=99) @Override public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } @Override public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) { if (cacheDirty) updateCache(); // If there's a test with that label, serve up that test. TestResult thatOne = childrenByLabel.get(token); if (thatOne != null) { return thatOne; } else { return super.getDynamic(token, req, rsp); } } @Override public String toPrettyString() { if (cacheDirty) updateCache(); StringBuilder sb = new StringBuilder(); Set<String> labels = childrenByLabel.keySet(); for (String label: labels) { LabeledTestResultGroup listForThisLabel = childrenByLabel.get(label); sb.append(label); sb.append(" results:\n"); sb.append(listForThisLabel.toPrettyString()); } return sb.toString(); } protected Collection<TestResult> flattenTopTier(Collection<LabeledTestResultGroup> twoTieredCollection) { // TODO: Consider caching if (twoTieredCollection == null || twoTieredCollection.isEmpty()) return Collections.emptyList(); List<TestResult> flattenedList = new ArrayList<TestResult>(); for (LabeledTestResultGroup topTierElement : twoTieredCollection) { flattenedList.addAll(topTierElement.getChildren()); } return flattenedList; } private void storeInCache(String label, Map<String, Collection<TestResult>> sameStatusCollection, TestResult r) { if (sameStatusCollection.keySet().contains(label)) { sameStatusCollection.get(label).add(r); } else { List<TestResult> newCollection = new ArrayList<TestResult>(Arrays.asList(r)); sameStatusCollection.put(label, newCollection); } } private void updateCache() { failedTestsByLabel.clear(); skippedTestsByLabel.clear(); passedTestsByLabel.clear(); allFailedTests.clear();; allPassedTests.clear(); allSkippedTests.clear(); passCount = 0; failCount = 0; skipCount = 0; float durationAccum = 0.0f; Collection<String> theLabels = childrenByLabel.keySet(); for (String l : theLabels) { LabeledTestResultGroup groupForThisLabel = childrenByLabel.get(l); groupForThisLabel.setParentAction(parentAction); groupForThisLabel.tally(); passCount += groupForThisLabel.getPassCount(); failCount += groupForThisLabel.getFailCount(); skipCount += groupForThisLabel.getSkipCount(); for (TestResult aResult : groupForThisLabel.getChildren()) { durationAccum += aResult.getDuration(); if (aResult.isPassed()) { storeInCache(l, passedTestsByLabel, aResult); allPassedTests.add(aResult); } else if (aResult.getFailCount() > 0) { storeInCache(l, failedTestsByLabel, aResult); allFailedTests.add(aResult); } else { storeInCache(l, skippedTestsByLabel, aResult); allSkippedTests.add(aResult); } } } duration = durationAccum; totalCount = passCount + failCount + skipCount; cacheDirty=false; } }