Changeset 18549 in josm


Ignore:
Timestamp:
2022-09-06T18:40:21+02:00 (20 months ago)
Author:
taylor.smock
Message:

See #22183: NoClassDefFoundError: Could not initialize class org.openstreetmap.josm.actions.SessionSaveAction

The root cause is a suppressed NPE whose stack trace is not given
in the bug report.

This adds a method to BugReport to add suppressed exceptions to a report.
In the future, if we keep the code long-term, methods in Logging which take
exceptions may assume the exception is suppressed, and add it to the
suppressed exceptions.

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/gui/progress/swing/ProgressMonitorExecutor.java

    r18150 r18549  
    1111import org.openstreetmap.josm.tools.Logging;
    1212import org.openstreetmap.josm.tools.Utils;
     13import org.openstreetmap.josm.tools.bugreport.BugReport;
    1314
    1415/**
     
    5960        if (t != null) {
    6061            Logging.error("Thread {0} raised {1}", Thread.currentThread().getName(), t);
     62            BugReport.addSuppressedException(t);
    6163        }
    6264    }
  • trunk/src/org/openstreetmap/josm/gui/util/GuiHelper.java

    r18365 r18549  
    212212    static void handleEDTException(Throwable t) {
    213213        Logging.logWithStackTrace(Logging.LEVEL_ERROR, t, "Exception raised in EDT");
     214        BugReport.addSuppressedException(t);
    214215    }
    215216
  • trunk/src/org/openstreetmap/josm/tools/bugreport/BugReport.java

    r15735 r18549  
    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;
     12
     13import org.openstreetmap.josm.tools.Pair;
    914
    1015/**
     
    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<time reported, exception> */
     50    private static final Deque<Pair<Instant, Throwable>> SUPPRESSED_EXCEPTIONS = new ArrayDeque<>(MAXIMUM_SUPPRESSED_EXCEPTIONS);
    4251
    4352    private boolean includeStatusReport = true;
     
    5463        this.exception = e;
    5564        includeAllStackTraces = e.mayHaveConcurrentSource();
     65    }
     66
     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     * {@link org.openstreetmap.josm.tools.Logging} may call this in the future, when logging a throwable.
     72     * @param t The throwable raised. If {@code null}, we add a new {@code NullPointerException} instead.
     73     * @since 18549
     74     */
     75    public static void addSuppressedException(Throwable t) {
     76        SUPPRESSED_EXCEPTIONS.add(new Pair<>(Instant.now(), t != null ? t : new NullPointerException()));
     77        // Ensure we don't call pop in more than MAXIMUM_SUPPRESSED_EXCEPTIONS threads. This guard is
     78        // here just in case someone doesn't read the javadocs.
     79        synchronized (SUPPRESSED_EXCEPTIONS) {
     80            // Ensure we aren't keeping exceptions forever
     81            while (SUPPRESSED_EXCEPTIONS.size() > MAXIMUM_SUPPRESSED_EXCEPTIONS) {
     82                SUPPRESSED_EXCEPTIONS.pop();
     83            }
     84        }
    5685    }
    5786
     
    136165            exception.printReportThreadsTo(out);
    137166        }
     167        synchronized (SUPPRESSED_EXCEPTIONS) {
     168            if (!SUPPRESSED_EXCEPTIONS.isEmpty()) {
     169                out.println("=== ADDITIONAL EXCEPTIONS ===");
     170                // Avoid multiple bug reports from reading from the deque at the same time.
     171                while (SUPPRESSED_EXCEPTIONS.peek() != null) {
     172                    Pair<Instant, Throwable> currentException = SUPPRESSED_EXCEPTIONS.pop();
     173                    out.println("==== Exception at " + currentException.a.toEpochMilli() + " ====");
     174                    currentException.b.printStackTrace(out);
     175                }
     176            }
     177        }
    138178        return stringWriter.toString().replaceAll("\r", "");
    139179    }
  • trunk/test/unit/org/openstreetmap/josm/tools/bugreport/BugReportTest.java

    r18037 r18549  
    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;
     
    912import java.io.PrintWriter;
    1013import java.io.StringWriter;
    11 
     14import java.lang.reflect.Field;
     15import java.util.function.Consumer;
     16import java.util.logging.Handler;
     17import java.util.regex.Matcher;
     18import java.util.regex.Pattern;
     19import java.util.stream.Stream;
     20
     21import org.junit.jupiter.api.AfterAll;
     22import org.junit.jupiter.api.BeforeAll;
    1223import org.junit.jupiter.api.Test;
     24import org.junit.jupiter.params.ParameterizedTest;
     25import org.junit.jupiter.params.provider.Arguments;
     26import org.junit.jupiter.params.provider.MethodSource;
     27import org.junit.platform.commons.util.ReflectionUtils;
    1328import org.openstreetmap.josm.actions.ShowStatusReportAction;
     29import org.openstreetmap.josm.gui.MainApplication;
     30import org.openstreetmap.josm.gui.util.GuiHelper;
    1431import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
     32import org.openstreetmap.josm.tools.Logging;
    1533
    1634/**
     
    2139@BasicPreferences
    2240class BugReportTest {
     41    private static Handler[] handlers;
     42
     43    @AfterAll
     44    static void cleanup() {
     45        // Clear queue
     46        new BugReport(BugReport.intercept(new NullPointerException())).getReportText("");
     47        Logging.clearLastErrorAndWarnings();
     48        for (Handler handler : handlers) {
     49            Logging.getLogger().addHandler(handler);
     50        }
     51    }
     52
     53    @BeforeAll
     54    static void setup() {
     55        handlers = Logging.getLogger().getHandlers();
     56        // Avoid console spam
     57        for (Handler handler : handlers) {
     58            Logging.getLogger().removeHandler(handler);
     59        }
     60    }
     61
    2362    /**
    2463     * Test {@link BugReport#getReportText}
     
    70109        return BugReport.getCallingMethod(2);
    71110    }
     111
     112    @Test
     113    void testSuppressedExceptionsOrder() {
     114        final String methodName = "testSuppressedExceptionsOrder";
     115        BugReport.addSuppressedException(new NullPointerException(methodName));
     116        BugReport.addSuppressedException(new IllegalStateException(methodName));
     117        BugReport bugReport = new BugReport(BugReport.intercept(new IOException(methodName)));
     118        final String report = assertDoesNotThrow(() -> bugReport.getReportText(methodName));
     119        assertAll(() -> assertTrue(report.contains("NullPointerException")),
     120                () -> assertTrue(report.contains("IOException")),
     121                () -> assertTrue(report.contains("IllegalStateException")));
     122        int ioe = report.indexOf("IOException");
     123        int npe = report.indexOf("NullPointerException");
     124        int ise = report.indexOf("IllegalStateException");
     125        assertAll("Ordering of exceptions is wrong",
     126                () -> assertTrue(ioe < npe, "IOException should be reported before NullPointerException"),
     127                () -> assertTrue(npe < ise, "NullPointerException should be reported before IllegalStateException"));
     128    }
     129
     130    static Stream<Arguments> testSuppressedExceptions() {
     131        return Stream.of(
     132                Arguments.of("GuiHelper::runInEDTAndWaitAndReturn",
     133                        (Consumer<Runnable>) r -> GuiHelper.runInEDTAndWaitAndReturn(() -> {
     134                            r.run();
     135                            return null;
     136                        })),
     137                Arguments.of("GuiHelper::runInEDTAndWait", (Consumer<Runnable>) GuiHelper::runInEDTAndWait),
     138                Arguments.of("MainApplication.worker", (Consumer<Runnable>) MainApplication.worker::execute)
     139        );
     140    }
     141
     142    @ParameterizedTest
     143    @MethodSource
     144    void testSuppressedExceptions(String workerName, Consumer<Runnable> worker) {
     145        // Throw a npe in the worker. Workers might give us the exception, wrapped or otherwise.
     146        try {
     147            worker.accept(() -> {
     148                throw new NullPointerException();
     149            });
     150        } catch (Exception e) {
     151            // pass. MainApplication.worker can continue throwing the NPE;
     152            Logging.trace(e);
     153        }
     154        // Ensure that the threads are synced
     155        assertDoesNotThrow(() -> worker.accept(() -> { /* sync */ }));
     156        // Now throw an exception
     157        BugReport bugReport = new BugReport(BugReport.intercept(new IOException("testSuppressedExceptions")));
     158        String report = bugReport.getReportText(workerName);
     159        assertTrue(report.contains("IOException"));
     160        assertTrue(report.contains("NullPointerException"));
     161    }
     162
     163    @Test
     164    void testSuppressedExceptionsReportedOnce() {
     165        // Add the exception
     166        BugReport.addSuppressedException(new NullPointerException("testSuppressedExceptionsReportedOnce"));
     167        BugReport bugReport = new BugReport(BugReport.intercept(new IOException("testSuppressedExceptionsReportedOnce")));
     168        // Get the report which clears the suppressed exceptions
     169        String report = bugReport.getReportText("");
     170        assertTrue(report.contains("IOException"));
     171        assertTrue(report.contains("NullPointerException"));
     172
     173        BugReport bugReport2 = new BugReport(BugReport.intercept(new IOException("testSuppressedExceptionsReportedOnce")));
     174        String report2 = bugReport2.getReportText("");
     175        assertTrue(report2.contains("IOException"));
     176        assertFalse(report2.contains("NullPointerException"));
     177    }
     178
     179    @Test
     180    void testManyExceptions() throws ReflectiveOperationException {
     181        Field suppressedExceptions = BugReport.class.getDeclaredField("MAXIMUM_SUPPRESSED_EXCEPTIONS");
     182        ReflectionUtils.makeAccessible(suppressedExceptions);
     183        final byte expected = suppressedExceptions.getByte(null);
     184        final int end = 2 * expected;
     185        // Add many suppressed exceptions
     186        for (int i = 0; i < end; i++) {
     187            BugReport.addSuppressedException(new NullPointerException("NPE: " + i));
     188        }
     189        BugReport bugReport = new BugReport(BugReport.intercept(new IOException("testManyExceptions")));
     190        String report = bugReport.getReportText("");
     191        Matcher matcher = Pattern.compile("NPE: (\\d+)").matcher(report);
     192        for (int i = end - expected; i < end; ++i) {
     193            assertTrue(matcher.find());
     194            assertEquals(Integer.toString(i), matcher.group(1));
     195        }
     196        assertFalse(matcher.find());
     197    }
     198
     199    @Test
     200    void testNullException() {
     201        // This should add a NPE to the suppressed exceptions
     202        assertDoesNotThrow(() -> BugReport.addSuppressedException(null));
     203        BugReport bugReport = new BugReport(BugReport.intercept(new IOException("testNullException")));
     204        // Getting the report text should not throw an exception.
     205        String report = assertDoesNotThrow(() -> bugReport.getReportText(""));
     206        assertTrue(report.contains("IOException"));
     207        assertTrue(report.contains("NullPointerException"));
     208    }
    72209}
Note: See TracChangeset for help on using the changeset viewer.