Ticket #22183: 22183.patch

File 22183.patch, 11.3 KB (added by taylor.smock, 4 years ago)

Better reporting of suppressed/ignored exceptions in EDT and worker threads

  • src/org/openstreetmap/josm/gui/progress/swing/ProgressMonitorExecutor.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/progress/swing/ProgressMonitorExecutor.java b/src/org/openstreetmap/josm/gui/progress/swing/ProgressMonitorExecutor.java
    a b  
    1010
    1111import org.openstreetmap.josm.tools.Logging;
    1212import org.openstreetmap.josm.tools.Utils;
     13import org.openstreetmap.josm.tools.bugreport.BugReport;
    1314
    1415/**
    1516 * Executor that displays the progress monitor to the user.
     
    5859        }
    5960        if (t != null) {
    6061            Logging.error("Thread {0} raised {1}", Thread.currentThread().getName(), t);
     62            BugReport.addSuppressedException(t);
    6163        }
    6264    }
    6365}
  • src/org/openstreetmap/josm/gui/util/GuiHelper.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/util/GuiHelper.java b/src/org/openstreetmap/josm/gui/util/GuiHelper.java
    a b  
    211211     */
    212212    static void handleEDTException(Throwable t) {
    213213        Logging.logWithStackTrace(Logging.LEVEL_ERROR, t, "Exception raised in EDT");
     214        BugReport.addSuppressedException(t);
    214215    }
    215216
    216217    /**
  • src/org/openstreetmap/josm/tools/bugreport/BugReport.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/tools/bugreport/BugReport.java b/src/org/openstreetmap/josm/tools/bugreport/BugReport.java
    a b  
    44import java.io.PrintWriter;
    55import java.io.Serializable;
    66import java.io.StringWriter;
     7import java.time.Instant;
     8import java.util.ArrayDeque;
     9import java.util.Deque;
    710import java.util.concurrent.CopyOnWriteArrayList;
    811import java.util.function.Predicate;
    912
     13import org.openstreetmap.josm.tools.Pair;
     14
    1015/**
    1116 * This class contains utility methods to create and handle a bug report.
    1217 * <p>
     
    3944 */
    4045public final class BugReport implements Serializable {
    4146    private static final long serialVersionUID = 1L;
     47    /** The maximum suppressed exceptions to keep to report */
     48    private static final byte MAXIMUM_SUPPRESSED_EXCEPTIONS = 4;
     49    /** The list of suppressed exceptions, Pair&lt;time reported, exception&gt; */
     50    private static final Deque<Pair<Instant, Throwable>> SUPPRESSED_EXCEPTIONS = new ArrayDeque<>(MAXIMUM_SUPPRESSED_EXCEPTIONS);
    4251
    4352    private boolean includeStatusReport = true;
    4453    private boolean includeData = true;
     
    5564        includeAllStackTraces = e.mayHaveConcurrentSource();
    5665    }
    5766
     67    /**
     68     * Add a suppressed exception. Mostly useful for when a chain of exceptions causes an actual bug report.
     69     * This should only be used when an exception is raised in {@link org.openstreetmap.josm.gui.util.GuiHelper}
     70     * or {@link org.openstreetmap.josm.gui.progress.swing.ProgressMonitorExecutor} at this time.
     71     * @param t The throwable raised. If {@code null}, we add a new {@code NullPointerException} instead.
     72     * @since xxx
     73     */
     74    public static void addSuppressedException(Throwable t) {
     75        SUPPRESSED_EXCEPTIONS.add(new Pair<>(Instant.now(), t != null ? t : new NullPointerException()));
     76        // Ensure we aren't keeping exceptions forever
     77        while (SUPPRESSED_EXCEPTIONS.size() > MAXIMUM_SUPPRESSED_EXCEPTIONS) {
     78            SUPPRESSED_EXCEPTIONS.pop();
     79        }
     80    }
     81
    5882    /**
    5983     * Determines if this report should include a system status report
    6084     * @return <code>true</code> to include it.
     
    135159        if (isIncludeAllStackTraces()) {
    136160            exception.printReportThreadsTo(out);
    137161        }
     162        if (!SUPPRESSED_EXCEPTIONS.isEmpty()) {
     163            out.println("=== ADDITIONAL EXCEPTIONS ===");
     164            while (SUPPRESSED_EXCEPTIONS.peek() != null) {
     165                Pair<Instant, Throwable> currentException = SUPPRESSED_EXCEPTIONS.pop();
     166                out.println("==== Exception at " + currentException.a.toEpochMilli() + " ====");
     167                currentException.b.printStackTrace(out);
     168            }
     169        }
    138170        return stringWriter.toString().replaceAll("\r", "");
    139171    }
    140172
  • test/unit/org/openstreetmap/josm/tools/bugreport/BugReportTest.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/test/unit/org/openstreetmap/josm/tools/bugreport/BugReportTest.java b/test/unit/org/openstreetmap/josm/tools/bugreport/BugReportTest.java
    a b  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.tools.bugreport;
    33
     4import static org.junit.jupiter.api.Assertions.assertAll;
     5import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
    46import static org.junit.jupiter.api.Assertions.assertEquals;
     7import static org.junit.jupiter.api.Assertions.assertFalse;
    58import static org.junit.jupiter.api.Assertions.assertSame;
    69import static org.junit.jupiter.api.Assertions.assertTrue;
    710
    811import java.io.IOException;
    912import java.io.PrintWriter;
    1013import java.io.StringWriter;
     14import java.lang.reflect.Field;
     15import java.util.function.Consumer;
     16import java.util.regex.Matcher;
     17import java.util.regex.Pattern;
     18import java.util.stream.Stream;
    1119
    1220import org.junit.jupiter.api.Test;
     21import org.junit.jupiter.params.ParameterizedTest;
     22import org.junit.jupiter.params.provider.Arguments;
     23import org.junit.jupiter.params.provider.MethodSource;
     24import org.junit.platform.commons.util.ReflectionUtils;
    1325import org.openstreetmap.josm.actions.ShowStatusReportAction;
     26import org.openstreetmap.josm.gui.MainApplication;
     27import org.openstreetmap.josm.gui.util.GuiHelper;
    1428import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
     29import org.openstreetmap.josm.tools.Logging;
    1530
    1631/**
    1732 * Tests the bug report class.
     
    6984    private String testGetCallingMethod2() {
    7085        return BugReport.getCallingMethod(2);
    7186    }
     87
     88    @Test
     89    void testSuppressedExceptionsOrder() {
     90        final String methodName = "testSuppressedExceptionsOrder";
     91        BugReport.addSuppressedException(new NullPointerException(methodName));
     92        BugReport.addSuppressedException(new IllegalStateException(methodName));
     93        BugReport bugReport = new BugReport(BugReport.intercept(new IOException(methodName)));
     94        final String report = assertDoesNotThrow(() -> bugReport.getReportText(methodName));
     95        assertAll(() -> assertTrue(report.contains("NullPointerException")),
     96                () -> assertTrue(report.contains("IOException")),
     97                () -> assertTrue(report.contains("IllegalStateException")));
     98        int ioe = report.indexOf("IOException");
     99        int npe = report.indexOf("NullPointerException");
     100        int ise = report.indexOf("IllegalStateException");
     101        assertAll("Ordering of exceptions is wrong",
     102                () -> assertTrue(ioe < npe, "IOException should be reported before NullPointerException"),
     103                () -> assertTrue(npe < ise, "NullPointerException should be reported before IllegalStateException"));
     104    }
     105
     106    static Stream<Arguments> testSuppressedExceptions() {
     107        return Stream.of(
     108                Arguments.of("GuiHelper::runInEDTAndWaitAndReturn",
     109                        (Consumer<Runnable>) r -> GuiHelper.runInEDTAndWaitAndReturn(() -> {
     110                            r.run();
     111                            return null;
     112                        })),
     113                Arguments.of("GuiHelper::runInEDTAndWait", (Consumer<Runnable>) GuiHelper::runInEDTAndWait),
     114                Arguments.of("MainApplication.worker", (Consumer<Runnable>) MainApplication.worker::execute)
     115        );
     116    }
     117
     118    @ParameterizedTest
     119    @MethodSource
     120    void testSuppressedExceptions(String workerName, Consumer<Runnable> worker) {
     121        // Throw a npe in the worker. Workers might give us the exception, wrapped or otherwise.
     122        try {
     123            worker.accept(() -> {
     124                throw new NullPointerException();
     125            });
     126        } catch (Exception e) {
     127            // pass. MainApplication.worker can continue throwing the NPE;
     128            Logging.trace(e);
     129        }
     130        // Ensure that the threads are synced
     131        assertDoesNotThrow(() -> worker.accept(() -> { /* sync */ }));
     132        // Now throw an exception
     133        BugReport bugReport = new BugReport(BugReport.intercept(new IOException("testSuppressedExceptions")));
     134        String report = bugReport.getReportText(workerName);
     135        assertTrue(report.contains("IOException"));
     136        assertTrue(report.contains("NullPointerException"));
     137    }
     138
     139    @Test
     140    void testSuppressedExceptionsReportedOnce() {
     141        // Add the exception
     142        BugReport.addSuppressedException(new NullPointerException("testSuppressedExceptionsReportedOnce"));
     143        BugReport bugReport = new BugReport(BugReport.intercept(new IOException("testSuppressedExceptionsReportedOnce")));
     144        // Get the report which clears the suppressed exceptions
     145        String report = bugReport.getReportText("");
     146        assertTrue(report.contains("IOException"));
     147        assertTrue(report.contains("NullPointerException"));
     148
     149        BugReport bugReport2 = new BugReport(BugReport.intercept(new IOException("testSuppressedExceptionsReportedOnce")));
     150        String report2 = bugReport2.getReportText("");
     151        assertTrue(report2.contains("IOException"));
     152        assertFalse(report2.contains("NullPointerException"));
     153    }
     154
     155    @Test
     156    void testManyExceptions() throws ReflectiveOperationException {
     157        Field suppressedExceptions = BugReport.class.getDeclaredField("MAXIMUM_SUPPRESSED_EXCEPTIONS");
     158        ReflectionUtils.makeAccessible(suppressedExceptions);
     159        final byte expected = suppressedExceptions.getByte(null);
     160        final int end = 2 * expected;
     161        // Add many suppressed exceptions
     162        for (int i = 0; i < end; i++) {
     163            BugReport.addSuppressedException(new NullPointerException("NPE: " + i));
     164        }
     165        BugReport bugReport = new BugReport(BugReport.intercept(new IOException("testManyExceptions")));
     166        String report = bugReport.getReportText("");
     167        Matcher matcher = Pattern.compile("NPE: (\\d+)").matcher(report);
     168        for (int i = end - expected; i < end; ++i) {
     169            assertTrue(matcher.find());
     170            assertEquals(Integer.toString(i), matcher.group(1));
     171        }
     172        assertFalse(matcher.find());
     173    }
    72174}