/* 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.jenkins.flakyTestHandler.junit.FlakyCaseResult; import com.google.jenkins.flakyTestHandler.junit.FlakyTestResult; import com.google.jenkins.flakyTestHandler.plugin.HistoryAggregatedFlakyTestResultAction.SingleTestFlakyStatsWithRevision; import com.thoughtworks.xstream.XStream; import java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import hudson.Launcher; import hudson.XmlFile; import hudson.model.AbstractBuild; import hudson.model.BuildListener; import hudson.model.Run; import hudson.tasks.junit.TestResult; import hudson.tasks.test.AbstractTestResultAction; import hudson.util.HeapSpaceStringConverter; import hudson.util.XStream2; import jenkins.model.RunAction2; /** * Action for each test run, to record flaky stats for all the tests * * @author Qingzhou Luo */ public class FlakyTestResultAction implements RunAction2 { /** * FlakyRunStats object, use WeakReference to reduce memory overhead since it will be * stored on the disk */ private transient WeakReference<FlakyRunStats> flakyRunStats; private static final XStream XSTREAM = new XStream2(); /** * This build */ private AbstractBuild<?,?> build; public static final Logger logger = Logger.getLogger(FlakyTestResultAction.class.getName()); static { XSTREAM.registerConverter(new HeapSpaceStringConverter(),100); } /** * Construct a FlakyTestResultAction object with AbstractBuild and BuildListener * * @param build this build * @param listener listener of this build */ public FlakyTestResultAction(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { this.build = build; // TODO consider the possibility that there is >1 such action AbstractTestResultAction action = build.getAction(AbstractTestResultAction.class); if (action != null) { Object latestResult = action.getResult(); if (latestResult != null && latestResult instanceof TestResult) { FlakyTestResult flakyTestResult = launcher.getChannel().call(new FlakyTestResultCollector((TestResult) latestResult)); flakyTestResult.freeze(action, build); FlakyRunStats stats = new FlakyRunStats(flakyTestResult.getTestFlakyStatsMap()); setFlakyRunStats(stats, listener); } } else { logger.log(Level.WARNING, "No test result found, please publish junit report first"); } } /** * Empty constructor for testing purpose */ // Visible for testing FlakyTestResultAction() { } private XmlFile getDataFile() { return new XmlFile(XSTREAM,new File(build.getRootDir(), "junitFlakyStatsResult.xml")); } /** * Loads a {@link TestResult} from disk. */ private FlakyRunStats load() { FlakyRunStats stats; try { stats = (FlakyRunStats)getDataFile().read(); } catch (IOException e) { stats = new FlakyRunStats(); // return a dummy } return stats; } @Override public void onAttached(Run<?, ?> r) { this.build = (AbstractBuild<?,?>) r; } @Override public void onLoad(Run<?, ?> r) { this.build = (AbstractBuild<?,?>) r; } @Override public String getIconFileName() { return null; } @Override public String getDisplayName() { return null; } @Override public String getUrlName() { return null; } public synchronized FlakyRunStats getFlakyRunStats() { FlakyRunStats stats; if (flakyRunStats == null) { stats = load(); flakyRunStats = new WeakReference<FlakyRunStats>(stats); } else { stats = flakyRunStats.get(); } if (stats == null) { stats = load(); flakyRunStats = new WeakReference<FlakyRunStats>(stats); } return stats; } // Visible for testing synchronized void setFlakyRunStats(FlakyRunStats stats) { flakyRunStats = new WeakReference<FlakyRunStats>(stats); } /** * Overwrites the {@link FlakyRunStats} by a new data set. */ public synchronized void setFlakyRunStats(FlakyRunStats stats, BuildListener listener) { // persist the data try { getDataFile().write(stats); } catch (IOException e) { e.printStackTrace(listener.fatalError("Failed to save the JUnit flaky test stats result")); } this.flakyRunStats = new WeakReference<FlakyRunStats>(stats); } /** * Get display names for all the test cases * * @param results Collection of {@link hudson.tasks.junit.TestResult} objects * @return the set of display names for all the test cases */ public static Set<String> getTestIdFromTestResults( Collection<? extends hudson.tasks.test.TestResult> results) { Set<String> testIdSet = new HashSet<String>(); for (hudson.tasks.test.TestResult testResult : results) { if (testResult instanceof FlakyCaseResult) { testIdSet.add(((FlakyCaseResult)testResult).getFullDisplayName()); } } return testIdSet; } /** * Class to hold all the passing/failing/flaky tests for one run */ public static class FlakyRunStats { /** * Map between test case name and its flaky stats with revision info */ Map<String, SingleTestFlakyStatsWithRevision> testFlakyStatsWithRevisionMap; public FlakyRunStats() { this.testFlakyStatsWithRevisionMap = new HashMap<String, SingleTestFlakyStatsWithRevision>(); } public FlakyRunStats(Map<String, SingleTestFlakyStatsWithRevision> testFlakyStatsWithRevisionMap) { this.testFlakyStatsWithRevisionMap = testFlakyStatsWithRevisionMap; } public Map<String, SingleTestFlakyStatsWithRevision> getTestFlakyStatsWithRevisionMap() { return testFlakyStatsWithRevisionMap; } /** * Is current run flaky or not. Build will be marked as unstable if there are flaky tests * and no failing test * * @return true if there is no failing test but there are some flaky tests */ public boolean isFlaked() { if (testFlakyStatsWithRevisionMap == null) { return false; } boolean seenFlake = false; for (Map.Entry<String, SingleTestFlakyStatsWithRevision> singleTestFlakyStatsWithRevisionEntry : testFlakyStatsWithRevisionMap.entrySet()) { if (singleTestFlakyStatsWithRevisionEntry.getValue().getStats().isFailed()) { return false; } else if (singleTestFlakyStatsWithRevisionEntry.getValue().getStats().isFlaked()) { seenFlake = true; } } return seenFlake; } } }