/* * The MIT License * * Copyright 2012 Sony Ericsson Mobile Communications. All rights reserved. * Copyright 2012 Sony Mobile Communications AB. All rights reserved. * * 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 com.sonyericsson.jenkins.plugins.bfa; import java.io.BufferedReader; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import com.sonyericsson.jenkins.plugins.bfa.graphs.ComputerGraphAction; import com.sonyericsson.jenkins.plugins.bfa.graphs.ProjectGraphAction; import com.sonyericsson.jenkins.plugins.bfa.model.FailureCause; import com.sonyericsson.jenkins.plugins.bfa.model.FailureCauseBuildAction; import com.sonyericsson.jenkins.plugins.bfa.model.FailureCauseDisplayData; import com.sonyericsson.jenkins.plugins.bfa.model.FailureCauseMatrixBuildAction; import com.sonyericsson.jenkins.plugins.bfa.model.FailureReader; import com.sonyericsson.jenkins.plugins.bfa.model.FoundFailureCause; import com.sonyericsson.jenkins.plugins.bfa.model.ScannerJobProperty; import com.sonyericsson.jenkins.plugins.bfa.model.indication.FoundIndication; import com.sonyericsson.jenkins.plugins.bfa.model.indication.Indication; import com.sonyericsson.jenkins.plugins.bfa.model.indication.MultilineBuildLogIndication; import com.sonyericsson.jenkins.plugins.bfa.statistics.StatisticsLogger; import hudson.Extension; import hudson.matrix.MatrixProject; import hudson.model.AbstractBuild; import hudson.model.Run; import hudson.model.TaskListener; import hudson.model.listeners.RunListener; import hudson.tasks.test.AbstractTestResultAction; import hudson.tasks.test.TestResult; import jenkins.model.Jenkins; import javax.annotation.Nonnull; /** * Looks for Indications, trying to find the Cause of a problem. * * @author Tomas Westling <thomas.westling@sonyericsson.com> */ @Extension(ordinal = BuildFailureScanner.ORDINAL) public class BuildFailureScanner extends RunListener<Run> { /** * The ordinal of this extension, one thousand below the GerritTrigger plugin. */ public static final int ORDINAL = 11003; private static final Logger logger = Logger.getLogger(BuildFailureScanner.class.getName()); private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = (ThreadPoolExecutor)Executors. newFixedThreadPool(PluginImpl.getInstance().getNrOfScanThreads()); @Override public void onStarted(Run build, TaskListener listener) { if (PluginImpl.shouldScan(build) && build.getParent().getProperty(ScannerJobProperty.class) == null) { try { build.getParent().addProperty(new ScannerJobProperty(false)); } catch (IOException e) { logger.log(Level.WARNING, "Failed to add a ScannerJobProperty to " + build.getParent().getFullDisplayName(), e); listener.getLogger().println("[BFA] WARNING! Failed to add the scanner property to this job."); } } } @Override public void onCompleted(Run build, @Nonnull TaskListener listener) { logger.entering(getClass().getName(), "onCompleted"); if (PluginImpl.isSizeInLimit(build)) { scanIfNotScanned(build, listener.getLogger()); } else { listener.getLogger().println("[BFA] Log exceeds limit: " + PluginImpl.getInstance().getMaxLogSize() + "MB"); } } /** * Scans the build if it should be scanned and it has not already been scanned. If configured, also reports * successful builds to the {@link StatisticsLogger}. * * @param build the build to scan * @param buildLog log to write information to */ public static void scanIfNotScanned(final Run build, final PrintStream buildLog) { if (PluginImpl.shouldScan(build) && !(build.getParent() instanceof MatrixProject)) { if (build.getActions(FailureCauseBuildAction.class).isEmpty() && build.getActions(FailureCauseMatrixBuildAction.class).isEmpty()) { if (PluginImpl.needToAnalyze(build.getResult())) { scan(build, buildLog); ProjectGraphAction.invalidateProjectGraphCache(build.getParent()); if (build instanceof AbstractBuild) { ComputerGraphAction.invalidateNodeGraphCache(((AbstractBuild)build).getBuiltOn()); } } else if (PluginImpl.getInstance().getKnowledgeBase().isSuccessfulLoggingEnabled()) { final List<FoundFailureCause> emptyCauseList = Collections.synchronizedList(new LinkedList<FoundFailureCause>()); StatisticsLogger.getInstance().log(build, emptyCauseList); } } } } /** * Performs a scan of the build, adds the {@link FailureCauseBuildAction} and reports to the * {@link StatisticsLogger}. * * @param build the build to scan * @param buildLog log to write information to. */ public static void scan(Run build, PrintStream buildLog) { try { Collection<FailureCause> causes = PluginImpl.getInstance().getKnowledgeBase().getCauses(); List<FoundFailureCause> foundCauseListToLog = findCauses(causes, build, buildLog); List<FoundFailureCause> foundCauseList; /* Register failed test cases as foundCauses. * We do not want these to be sent to the StatisticsLogger, to avoid * problems due to these causes not being present in the database. * Since StatisticsLogger spawns a background thread, we create a * copy of the list. */ if (PluginImpl.getInstance().isTestResultParsingEnabled()) { foundCauseList = Collections.synchronizedList( new LinkedList<FoundFailureCause>(foundCauseListToLog)); foundCauseList.addAll(findFailedTests(build, buildLog)); } else { foundCauseList = foundCauseListToLog; } FailureCauseBuildAction buildAction = new FailureCauseBuildAction(foundCauseList); buildAction.setBuild(build); build.addAction(buildAction); final FailureCauseDisplayData data = buildAction.getFailureCauseDisplayData(); List<FailureCauseDisplayData> downstreamFailureCauses = data.getDownstreamFailureCauses(); if (!downstreamFailureCauses.isEmpty()) { buildLog.println("[BFA] Found downstream Failure causes ..."); printDownstream(buildLog, downstreamFailureCauses); } StatisticsLogger.getInstance().log(build, foundCauseListToLog); } catch (Exception e) { logger.log(Level.SEVERE, "Could not scan build " + build, e); } } /** * * Adds all causes from downstream builds in recursion * * @param buildLog log to write information to. * @param downstreamFailureCauses the list of downstream failure causes. */ private static void printDownstream(PrintStream buildLog, List<FailureCauseDisplayData> downstreamFailureCauses) { for (FailureCauseDisplayData displayData : downstreamFailureCauses) { FailureCauseDisplayData.Links links = displayData.getLinks(); if (!displayData.getFoundFailureCauses().isEmpty()) { buildLog.println("[BFA] See: " + Jenkins.getInstance().getRootUrl() + links.getBuildUrl()); for (FoundFailureCause foundCause : displayData.getFoundFailureCauses()) { String foundString = "[BFA] " + foundCause.getName(); if (foundCause.getCategories() != null) { foundString += " from category " + foundCause.getCategories().get(0); } buildLog.println(foundString); } } printDownstream(buildLog, displayData.getDownstreamFailureCauses()); } } /** * Finds the failure causes for this build. * * @param causes the list of possible causes. * @param build the build to analyze. * @param buildLog the build log. * @return a list of found failure causes. */ private static List<FoundFailureCause> findCauses(final Collection<FailureCause> causes, final Run build, final PrintStream buildLog) { THREAD_POOL_EXECUTOR.setCorePoolSize(PluginImpl.getInstance().getNrOfScanThreads()); THREAD_POOL_EXECUTOR.setMaximumPoolSize(PluginImpl.getInstance().getNrOfScanThreads()); buildLog.println("[BFA] Scanning build for known causes..."); long start = System.currentTimeMillis(); final List<FoundFailureCause> foundFailureCauseList = findIndications(causes, build, buildLog); long time = System.currentTimeMillis() - start; if (logger.isLoggable(Level.FINER)) { logger.log(Level.FINER, "[BFA] [{0}] {1}ms", new Object[] {build.getFullDisplayName(), String.valueOf(time), }); } if (!foundFailureCauseList.isEmpty()) { buildLog.println("[BFA] Found failure cause(s):"); for (FoundFailureCause foundCause : foundFailureCauseList) { if (foundCause.getCategories() == null) { buildLog.println("[BFA] " + foundCause.getName()); } else { buildLog.println("[BFA] " + foundCause.getName() + " from category " + foundCause.getCategories().get(0)); } } } else { buildLog.println("[BFA] No failure causes found"); } buildLog.println("[BFA] Done. " + TimeUnit.MILLISECONDS.toSeconds(time) + "s"); return foundFailureCauseList; } /** * * Finds indications for all causes. * * @param causes the list of possible causes. * @param build current build. * @param buildLog build log for providing feedback. * @return a list of found indications, could be empty. */ private static List<FoundFailureCause> findIndications(final Collection<FailureCause> causes, final Run build, final PrintStream buildLog) { final List<FailureCause> singleLineCauses = new ArrayList<FailureCause>(); final List<FailureCause> notOnlySingleLineCauses = new ArrayList<FailureCause>(); splitCauses(causes, singleLineCauses, notOnlySingleLineCauses); final List<Future<?>> scanningTasks = new ArrayList<Future<?>>(notOnlySingleLineCauses.size() + 1); final List<FoundFailureCause> foundFailureCauses = Collections.synchronizedList( new ArrayList<FoundFailureCause>()); if (!singleLineCauses.isEmpty()) { scanningTasks.add(THREAD_POOL_EXECUTOR.submit(new Runnable() { @Override public void run() { foundFailureCauses.addAll(parseSingleLineCauses(build, buildLog, singleLineCauses)); Thread.currentThread().setName("BFA-scanner-" + build.getFullDisplayName()); } })); } for (final FailureCause cause : notOnlySingleLineCauses) { scanningTasks.add(THREAD_POOL_EXECUTOR.submit(new Runnable() { @Override public void run() { final List<FoundIndication> foundIndications = new ArrayList<FoundIndication>(); for (final Indication indication : cause.getIndications()) { Thread.currentThread().setName("BFA-scanner-" + build.getFullDisplayName() + ": " + cause.getName() + "-" + indication.getUserProvidedExpression()); FoundIndication foundIndication = parseIndication(build, buildLog, indication, cause.getName()); if (foundIndication != null) { foundIndications.add(foundIndication); } } if (!foundIndications.isEmpty()) { foundFailureCauses.add(new FoundFailureCause(cause, foundIndications)); } } })); } waitAllTasks(buildLog, scanningTasks); return foundFailureCauses; } /** * * Wait all scanning tasks to be completed. Cancel all of them if InterruptedException happened. * * @param buildLog build log for providing feedback. * @param scanningTasks List of scheduled scanning tasks. */ private static void waitAllTasks(PrintStream buildLog, List<Future<?>> scanningTasks) { try { for (Future<?> scanningTask : scanningTasks) { try { scanningTask.get(); } catch (ExecutionException e) { buildLog.print("[BFA] task failed due exception: " + e); } } } catch (InterruptedException e) { buildLog.print("[BFA] was interrupted: " + e); for (Future<?> scanningTask : scanningTasks) { scanningTask.cancel(true); } buildLog.print("[BFA] all bfa tasks were cancelled"); } } /** * * Split list of causes in two part: causes with single line indication only and others. * * @param causes All causes. * @param singleLineCauses Collection to put list of causes with single line indication. * @param notOnlySingleLineCauses Collection to put other causes. */ private static void splitCauses(Collection<FailureCause> causes, List<FailureCause> singleLineCauses, List<FailureCause> notOnlySingleLineCauses) { for (FailureCause cause : causes) { boolean atLeastOneNonSignalLine = false; for (Indication indication : cause.getIndications()) { if (indication instanceof MultilineBuildLogIndication) { atLeastOneNonSignalLine = true; break; } } if (atLeastOneNonSignalLine) { notOnlySingleLineCauses.add(cause); } else { singleLineCauses.add(cause); } } } /** * * Parse any indication. * * @param build current build. * @param buildLog build log for providing feedback. * @param indication indication to be found. * @param causeName name of cause this indication belongs to. * @return a found indication, null otherwise. */ private static FoundIndication parseIndication(Run build, PrintStream buildLog, Indication indication, String causeName) { long start = System.currentTimeMillis(); final FoundIndication foundIndication = findIndication(indication, build, buildLog); if (foundIndication != null) { if (logger.isLoggable(Level.FINER)) { logger.log(Level.FINER, "[BFA] [{0}] [{1}] {2}ms", new Object[]{build.getFullDisplayName(), causeName, String.valueOf(System.currentTimeMillis() - start), }); } } return foundIndication; } /** * * Parses all causes with only single line indications. * * @param build current build. * @param buildLog build log for providing feedback. * @param singleLineCauses list of causes to be found. * @return a list of causes with found indication, could be empty. */ private static List<FoundFailureCause> parseSingleLineCauses(Run build, PrintStream buildLog, List<FailureCause> singleLineCauses) { final List<FoundFailureCause> foundFailureCauses = new ArrayList<FoundFailureCause>(); BufferedReader reader = null; try { reader = new BufferedReader(build.getLogReader()); foundFailureCauses.addAll( FailureReader.scanSingleLinePatterns( singleLineCauses, build, reader, build.getLogFile().getName())); } catch (IOException e) { buildLog.print("[BFA] Exception during parsing file: " + e); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { logger.log(Level.WARNING, "Failed to close the reader. ", e); } } } return foundFailureCauses; } /** * Finds out if this indication matches the build. * * @param indication the indication to look for. * @param build the build to analyze. * @param buildLog the build log. * @return an indication if one is found, null otherwise. */ private static FoundIndication findIndication(Indication indication, Run build, PrintStream buildLog) { FailureReader failureReader = indication.getReader(); return failureReader.scan(build, buildLog); } /** * Finds the failed tests reported by this build * * @param build the build to analyze. * @param buildLog the build log. * @return a list of found failure causes based on the test results. */ private static List<FoundFailureCause> findFailedTests(final Run build, final PrintStream buildLog) { final List<FoundFailureCause> failedTestList = Collections.synchronizedList(new LinkedList<FoundFailureCause>()); final List<AbstractTestResultAction> testActions = build.getActions(AbstractTestResultAction.class); for (AbstractTestResultAction testAction : testActions) { List<? extends TestResult> failedTests = testAction.getFailedTests(); for (TestResult test : failedTests) { buildLog.println("[BFA] Found failed test case: " + test.getName()); FailureCause failureCause = new FailureCause(null, test.getName(), test.getErrorStackTrace(), "", null, PluginImpl.getInstance().getTestResultCategories(), null, null); FoundFailureCause foundFailureCause = new FoundFailureCause(failureCause); failedTestList.add(foundFailureCause); } } return failedTestList; } }