/* Copyright 2014 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.jenkins.flakyTestHandler.plugin;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Maps;
import com.google.jenkins.flakyTestHandler.plugin.FlakyTestResultAction.FlakyRunStats;
import com.google.jenkins.flakyTestHandler.plugin.deflake.DeflakeCause;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Stack;
import java.util.TreeMap;
import javax.servlet.ServletException;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.plugins.git.GitSCM;
import hudson.plugins.git.Revision;
import hudson.plugins.git.util.BuildData;
import hudson.scm.SCM;
/**
* Action for aggregate and display information for flaky history for all the tests
*
* @author Qingzhou Luo
*/
public class HistoryAggregatedFlakyTestResultAction implements Action {
/**
* The project which is running
*/
AbstractProject<?, ?> project;
/**
* Map between test name and flaky stats for a single test
*/
Map<String, SingleTestFlakyStats> aggregatedFlakyStats;
/**
* Map between test name and the map between each scm revision and its running stats (# passes + #
* fails)
*/
Map<String, Map<String, SingleTestFlakyStats>> aggregatedTestFlakyStatsWithRevision;
/**
* The set of all tests being run in last non-deflake build
*/
Set<String> allTests;
/**
* Whether to only show flaky tests or all tests
*/
boolean onlyShowFlakyTests;
public HistoryAggregatedFlakyTestResultAction(AbstractProject<?, ?> project) {
this.project = project;
this.aggregatedTestFlakyStatsWithRevision =
new TreeMap<String, Map<String, SingleTestFlakyStats>>();
this.aggregatedFlakyStats = new TreeMap<String, SingleTestFlakyStats>();
this.allTests = new HashSet<String>();
this.onlyShowFlakyTests = true;
}
/**
* Aggregate all the previous builds to get flaky stats information for all the tests
*/
void aggregate() {
// set of all the previous builds
Stack<AbstractBuild> builds = new Stack<AbstractBuild>();
for (AbstractBuild<?, ?> build : project._getRuns().values()) {
builds.push(build);
}
while (!builds.empty()) {
aggregateOneBuild(builds.pop());
}
}
/**
* Aggregate flaky runs one previous build and put results into a map between test name and
* its map between scm revisions and aggregated flaky stats for that revision
*
* @param build the build to be aggregated
*/
public void aggregateOneBuild(AbstractBuild<?, ?> build) {
FlakyTestResultAction action = build.getAction(FlakyTestResultAction.class);
if (action == null) {
return;
}
FlakyRunStats runStats = action.getFlakyRunStats();
if (runStats == null) {
return;
}
Map<String, SingleTestFlakyStatsWithRevision> testFlakyStatsMap = runStats
.getTestFlakyStatsWithRevisionMap();
if (testFlakyStatsMap == null) {
// Skip old build which doesn't have the map
return;
}
if (build.getCause(DeflakeCause.class) == null) {
// This is a non-deflake build, update allTests
allTests = testFlakyStatsMap.keySet();
}
for (Map.Entry<String, SingleTestFlakyStatsWithRevision> testFlakyStat : testFlakyStatsMap
.entrySet()) {
String testName = testFlakyStat.getKey();
String revision = testFlakyStat.getValue().getRevision();
SingleTestFlakyStats stats = testFlakyStat.getValue().getStats();
if (aggregatedTestFlakyStatsWithRevision.containsKey(testName)) {
Map<String, SingleTestFlakyStats> testFlakyStatMap = aggregatedTestFlakyStatsWithRevision
.get(testName);
if (testFlakyStatMap.containsKey(revision)) {
// Merge flaky stats with the same test and the same revision
testFlakyStatMap.get(revision).merge(stats);
} else {
// First specific revision flaky stat for a given test
testFlakyStatMap.put(revision, new SingleTestFlakyStats(stats));
}
} else {
// The first test entry
Map<String, SingleTestFlakyStats> testFlakyStatMap =
new LinkedHashMap<String, SingleTestFlakyStats>();
testFlakyStatMap.put(revision, new SingleTestFlakyStats(stats));
aggregatedTestFlakyStatsWithRevision.put(testName, testFlakyStatMap);
}
}
aggregatedFlakyStats = Maps
.filterKeys(Maps.transformValues(aggregatedTestFlakyStatsWithRevision,
REVISION_STATS_MAP_TO_AGGREGATED_STATS), Predicates.in(allTests));
}
public Map<String, Map<String, SingleTestFlakyStats>> getAggregatedTestFlakyStatsWithRevision() {
return aggregatedTestFlakyStatsWithRevision;
}
public boolean getOnlyShowFlakyTests() {
return onlyShowFlakyTests;
}
/**
* Function to aggregate flaky stats over revisions
*/
public static final Function<Map<String, SingleTestFlakyStats>, SingleTestFlakyStats>
REVISION_STATS_MAP_TO_AGGREGATED_STATS = new Function<Map<String, SingleTestFlakyStats>,
SingleTestFlakyStats>() {
@Override
public SingleTestFlakyStats apply(
Map<String, SingleTestFlakyStats> revisionStatsMap) {
SingleTestFlakyStats aggregatedStatsOverRevision = new SingleTestFlakyStats(0, 0, 0);
for (SingleTestFlakyStats singleTestFlakyStats : revisionStatsMap.values()) {
if (singleTestFlakyStats.isPassed()) {
aggregatedStatsOverRevision.increasePass();
} else if (singleTestFlakyStats.isFlaked()) {
aggregatedStatsOverRevision.increaseFlake();
} else if (singleTestFlakyStats.isFailed()) {
aggregatedStatsOverRevision.increaseFail();
}
}
return aggregatedStatsOverRevision;
}
};
public void doShowAll( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
onlyShowFlakyTests = !onlyShowFlakyTests;
rsp.sendRedirect("..");
}
public Map<String, SingleTestFlakyStats> getAggregatedFlakyStats() {
return aggregatedFlakyStats;
}
/**
* Get filtered tests to display on the project page. Users can decide whether to show all tests
* or just flaky tests
*
* @return the filtered tests
*/
public Map<String, SingleTestFlakyStats> getFilteredAggregatedFlakyStats() {
Predicate<Entry<String, SingleTestFlakyStats>> flakyTestsFilter;
if (onlyShowFlakyTests) {
flakyTestsFilter = new Predicate<Entry<String, SingleTestFlakyStats>>() {
@Override
public boolean apply(
Entry<String, SingleTestFlakyStats> singleTestFlakyStatsEntry) {
return singleTestFlakyStatsEntry.getValue().getFlake() > 0;
}
};
} else {
flakyTestsFilter = Predicates.alwaysTrue();
}
return Maps.filterEntries(aggregatedFlakyStats, flakyTestsFilter);
}
public String getIconFileName() {
return null;
}
public String getDisplayName() {
return null;
}
public String getUrlName() {
return "historyAggregate";
}
/**
* Class for flaky information for one single test
*/
public static class SingleTestFlakyStats {
int flake;
int pass;
int fail;
public int getFlake() {
return flake;
}
public int getPass() {
return pass;
}
public int getFail() {
return fail;
}
public void increasePass() {
pass++;
}
public void increaseFail() {
fail++;
}
public void increaseFlake() {
flake++;
}
public SingleTestFlakyStats(int pass, int fail, int flake) {
this.pass = pass;
this.fail = fail;
this.flake = flake;
}
public SingleTestFlakyStats(SingleTestFlakyStats stats) {
this.pass = stats.pass;
this.fail = stats.fail;
this.flake = stats.flake;
}
public void merge(SingleTestFlakyStats otherTestStats) {
this.pass += otherTestStats.pass;
this.fail += otherTestStats.fail;
this.flake += otherTestStats.flake;
}
public boolean isPassed() {
return pass > 0 && fail == 0 && flake == 0;
}
public boolean isFailed() {
return fail > 0 && pass == 0 && flake == 0;
}
public boolean isFlaked() {
return (pass > 0 && fail > 0) || flake > 0;
}
public boolean isUnknown() {
return pass == 0 && fail == 0 && flake == 0;
}
}
/**
* A class which augments {@link SingleTestFlakyStats} with a revision string.
*/
public static class SingleTestFlakyStatsWithRevision {
/**
* Embedded {@link SingleTestFlakyStats} object
*/
private SingleTestFlakyStats stats;
/**
* The revision with this test stats. If using GIT for scm, then it will be the git Shal string;
* Otherwise it will be the build number.
*/
private String revision;
/**
* Construct a SingleTestFlakyStatsWithRevision object with {@link SingleTestFlakyStats} and
* build information.
*
* @param stats Embedded {@link SingleTestFlakyStats} object
* @param build The {@link hudson.model.AbstractBuild} object to get SCM information from.
*/
public SingleTestFlakyStatsWithRevision(SingleTestFlakyStats stats, AbstractBuild build) {
this.stats = stats;
revision = Integer.toString(build.getNumber());
SCM scm = build.getProject().getScm();
if (scm != null && scm instanceof GitSCM) {
GitSCM gitSCM = (GitSCM) scm;
BuildData buildData = gitSCM.getBuildData(build);
if (buildData != null) {
Revision gitRevision = buildData.getLastBuiltRevision();
if (gitRevision != null) {
revision = gitRevision.getSha1String();
}
}
}
}
public SingleTestFlakyStatsWithRevision(SingleTestFlakyStats stats, String revision) {
this.stats = stats;
this.revision = revision;
}
public String getRevision() {
return revision;
}
public SingleTestFlakyStats getStats() {
return stats;
}
}
}