Ticket #21423: 21423.3.patch

File 21423.3.patch, 27.4 KB (added by taylor.smock, 3 years ago)

Add tests

  • src/org/openstreetmap/josm/data/validation/OsmValidator.java

    Subject: [PATCH] Fix #21423: Prevent error codes from clashing
    ---
    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/validation/OsmValidator.java b/src/org/openstreetmap/josm/data/validation/OsmValidator.java
    a b  
    112112    @SuppressWarnings("unchecked")
    113113    private static final Class<Test>[] CORE_TEST_CLASSES = new Class[] {// NOPMD
    114114        /* FIXME - unique error numbers for tests aren't properly unique - ignoring will not work as expected */
     115        /* Error codes are class.getName().hashCode() + "_" + oldCode. There should almost never be a collision. */
    115116        DuplicateNode.class, // ID    1 ..   99
    116117        OverlappingWays.class, // ID  101 ..  199
    117118        UntaggedNode.class, // ID  201 ..  299
     
    217218
    218219    private static void loadIgnoredErrors() {
    219220        ignoredErrors.clear();
    220         if (ValidatorPrefHelper.PREF_USE_IGNORE.get()) {
     221        if (Boolean.TRUE.equals(ValidatorPrefHelper.PREF_USE_IGNORE.get())) {
    221222            Config.getPref().getListOfMaps(ValidatorPrefHelper.PREF_IGNORELIST).forEach(ignoredErrors::putAll);
    222223            Path path = Paths.get(getValidatorDir()).resolve("ignorederrors");
    223224            try {
    224225                if (path.toFile().exists()) {
    225226                    try {
    226                         TreeSet<String> treeSet = new TreeSet<>();
    227                         treeSet.addAll(Files.readAllLines(path, StandardCharsets.UTF_8));
     227                        TreeSet<String> treeSet = new TreeSet<>(Files.readAllLines(path, StandardCharsets.UTF_8));
    228228                        treeSet.forEach(ignore -> ignoredErrors.putIfAbsent(ignore, ""));
    229229                        removeLegacyEntries(true);
    230230
     
    246246
    247247    private static void removeLegacyEntries(boolean force) {
    248248        // see #19053:
     249        boolean wasChanged = removeLegacyEntry(force, true, "3000");
     250        // see #18230 (pt_assistant, RightAngleBuildingTest)
     251        wasChanged |= removeLegacyEntry(force, false, "3701");
     252
     253        if (wasChanged) {
     254            saveIgnoredErrors();
     255        }
     256    }
     257
     258    private static boolean removeLegacyEntry(boolean force, boolean keep, String prefix) {
    249259        boolean wasChanged = false;
    250260        if (force) {
    251261            Iterator<Entry<String, String>> iter = ignoredErrors.entrySet().iterator();
    252262            while (iter.hasNext()) {
    253263                Entry<String, String> entry = iter.next();
    254                 if (entry.getKey().startsWith("3000_")) {
     264                if (entry.getKey().startsWith(prefix + "_")) {
    255265                    Logging.warn(tr("Cannot handle ignore list entry {0}", entry));
    256266                    iter.remove();
    257267                    wasChanged = true;
    258268                }
    259269            }
    260270        }
    261         String legacyEntry = ignoredErrors.remove("3000");
    262         if (legacyEntry != null) {
     271        String legacyEntry = ignoredErrors.remove(prefix);
     272        if (keep && legacyEntry != null) {
    263273            if (!legacyEntry.isEmpty()) {
    264                 addIgnoredError("3000_" + legacyEntry, legacyEntry);
     274                addIgnoredError(prefix + "_" + legacyEntry, legacyEntry);
    265275            }
    266276            wasChanged = true;
    267277        }
    268         if (wasChanged) {
    269             saveIgnoredErrors();
    270         }
     278        return wasChanged;
    271279    }
    272280
    273281    /**
     
    502510        List<Map<String, String>> list = new ArrayList<>();
    503511        cleanupIgnoredErrors();
    504512        ignoredErrors.remove("3000"); // see #19053
     513        ignoredErrors.remove("3701"); // see #18230
    505514        list.add(ignoredErrors);
    506515        int i = 0;
    507516        while (i < list.size()) {
     
    607616    }
    608617
    609618    /**
    610      * Initialize grid details based on current projection system. Values based on
     619     * Initialize grid details based on the current projection system. Values based on
    611620     * the original value fixed for EPSG:4326 (10000) using heuristics (that is, test&amp;error
    612621     * until most bugs were discovered while keeping the processing time reasonable)
    613622     */
     
    636645    private static boolean testsInitialized;
    637646
    638647    /**
    639      * Initializes all tests if this operations hasn't been performed already.
     648     * Initializes all tests if this operation hasn't been performed already.
    640649     */
    641650    public static synchronized void initializeTests() {
    642651        if (!testsInitialized) {
  • src/org/openstreetmap/josm/data/validation/TestError.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/validation/TestError.java b/src/org/openstreetmap/josm/data/validation/TestError.java
    a b  
    44import java.awt.geom.Area;
    55import java.awt.geom.PathIterator;
    66import java.text.MessageFormat;
     7import java.time.Instant;
    78import java.util.ArrayList;
    89import java.util.Arrays;
    910import java.util.Collection;
    1011import java.util.Collections;
    1112import java.util.List;
    1213import java.util.Locale;
     14import java.util.Map;
    1315import java.util.TreeSet;
    1416import java.util.function.Supplier;
    1517import java.util.stream.Collectors;
     
    3335 * @since 3669
    3436 */
    3537public class TestError implements Comparable<TestError> {
     38    /**
     39     * Used to switch users over to new ignore system, UNIQUE_CODE_MESSAGE_STATE
     40     * 1_704_067_200L -> 2024-01-01
     41     * We can probably remove this and the supporting code in 2025.
     42     */
     43    private static boolean switchOver = Instant.now().isAfter(Instant.ofEpochMilli(1_704_067_200L));
    3644    /** is this error on the ignore list */
    3745    private boolean ignored;
    3846    /** Severity */
     
    5058    private final Test tester;
    5159    /** Internal code used by testers to classify errors */
    5260    private final int code;
     61    /** Internal code used by testers to classify errors. Used for moving between JOSM versions. */
     62    private final int uniqueCode;
    5363    /** If this error is selected */
    5464    private boolean selected;
    5565    /** Supplying a command to fix the error */
     
    6373        private final Test tester;
    6474        private final Severity severity;
    6575        private final int code;
     76        private final int uniqueCode;
    6677        private String message;
    6778        private String description;
    6879        private String descriptionEn;
     
    7485            this.tester = tester;
    7586            this.severity = severity;
    7687            this.code = code;
     88            this.uniqueCode = this.tester != null ? this.tester.getClass().getName().hashCode() : code;
    7789        }
    7890
    7991        /**
     
    233245        }
    234246    }
    235247
     248    /**
     249     * Update error codes on read and save. Used for tests.
     250     * @param updateErrorCodes {@code true} to update error codes. See {@link #switchOver} for default.
     251     */
     252    static void setUpdateErrorCodes(boolean updateErrorCodes) {
     253        switchOver = updateErrorCodes;
     254    }
     255
    236256    /**
    237257     * Starts building a new {@code TestError}
    238258     * @param tester The tester
     
    254274        this.primitives = builder.primitives;
    255275        this.highlighted = builder.highlighted;
    256276        this.code = builder.code;
     277        this.uniqueCode = builder.uniqueCode;
    257278        this.fixingCommand = builder.fixingCommand;
    258279    }
    259280
     
    306327     * @return the ignore state for this error or null if any primitive is new
    307328     */
    308329    public String getIgnoreState() {
     330        return getIgnoreState(false);
     331    }
     332
     333    /**
     334     * Get the ignore state
     335     * @param useOriginal if {@code true}, use the original code to get the ignore state
     336     * @return The ignore state ({@link #getIgnoreGroup} + ignored object list)
     337     */
     338    private String getIgnoreState(boolean useOriginal) {
    309339        Collection<String> strings = new TreeSet<>();
    310340        for (OsmPrimitive o : primitives) {
    311341            // ignore data not yet uploaded
     
    321351            }
    322352            strings.add(type + '_' + o.getId());
    323353        }
    324         return strings.stream().map(o -> ':' + o).collect(Collectors.joining("", getIgnoreSubGroup(), ""));
     354        return strings.stream().map(o -> ':' + o).collect(Collectors.joining("", getIgnoreSubGroup(useOriginal), ""));
    325355    }
    326356
    327357    /**
     
    335365    }
    336366
    337367    private boolean calcIgnored() {
     368        // Begin code removal section (backwards compatibility)
     369        if (OsmValidator.hasIgnoredError(getIgnoreGroup(true))) {
     370            updateIgnoreList(getIgnoreGroup(true), getIgnoreGroup(false));
     371            return true;
     372        }
     373        if (OsmValidator.hasIgnoredError(getIgnoreSubGroup(true))) {
     374            updateIgnoreList(getIgnoreSubGroup(true), getIgnoreSubGroup(false));
     375            return true;
     376        }
     377        String oldState = getIgnoreState(true);
     378        String state = getIgnoreState(false);
     379        if (oldState != null && OsmValidator.hasIgnoredError(oldState)) {
     380            updateIgnoreList(oldState, state);
     381            return true;
     382        }
     383        // End code removal section
    338384        if (OsmValidator.hasIgnoredError(getIgnoreGroup()))
    339385            return true;
    340386        if (OsmValidator.hasIgnoredError(getIgnoreSubGroup()))
    341387            return true;
    342         String state = getIgnoreState();
    343388        return state != null && OsmValidator.hasIgnoredError(state);
    344389    }
    345390
     391    /**
     392     * Convert old keys to new keys. Only takes effect when {@link #switchOver} is true
     393     * @param oldKey The key to replace
     394     * @param newKey The new key
     395     */
     396    private static void updateIgnoreList(String oldKey, String newKey) {
     397        if (switchOver) {
     398            Map<String, String> errors = OsmValidator.getIgnoredErrors();
     399            if (errors.containsKey(oldKey)) {
     400                String value = errors.remove(oldKey);
     401                errors.put(newKey, value);
     402            }
     403        }
     404    }
     405
    346406    /**
    347407     * Gets the ignores subgroup that is more specialized than {@link #getIgnoreGroup()}
    348408     * @return The ignore sub group
    349409     */
    350410    public String getIgnoreSubGroup() {
     411        return getIgnoreSubGroup(false);
     412    }
     413
     414    /**
     415     * Get the subgroup for the error
     416     * @param useOriginal if {@code true}, use the original code instead of the new unique codes.
     417     * @return The ignore subgroup
     418     */
     419    private String getIgnoreSubGroup(boolean useOriginal) {
    351420        if (code == 3000) {
    352421            // see #19053
    353422            return "3000_" + (description == null ? message : description);
    354423        }
    355         String ignorestring = getIgnoreGroup();
     424        String ignorestring = getIgnoreGroup(useOriginal);
    356425        if (descriptionEn != null) {
    357426            ignorestring += '_' + descriptionEn;
    358427        }
     
    365434     * @see TestError#getIgnoreSubGroup()
    366435     */
    367436    public String getIgnoreGroup() {
     437        return getIgnoreGroup(false);
     438    }
     439
     440    /**
     441     * Get the ignore group
     442     * @param useOriginal if {@code true}, use the original code instead of a unique code + original code.
     443     *                    Used for reading and understanding old ignore groups.
     444     * @return The ignore group.
     445     */
     446    private String getIgnoreGroup(boolean useOriginal) {
    368447        if (code == 3000) {
    369448            // see #19053
    370449            return "3000_" + getMessage();
    371450        }
    372         return Integer.toString(code);
     451        if (useOriginal) {
     452            return Integer.toString(this.code);
     453        }
     454        return this.uniqueCode + "_" + this.code;
    373455    }
    374456
    375457    /**
     
    404486        return code;
    405487    }
    406488
     489    /**
     490     * Get the unique code for this test. Used for ignore lists.
     491     * @return The unique code (generated with {@code tester.getClass().getName().hashCode() + code}).
     492     * @since xxx
     493     */
     494    public int getUniqueCode() {
     495        return this.uniqueCode;
     496    }
     497
    407498    /**
    408499     * Returns true if the error can be fixed automatically
    409500     *
     
    546637     * @return true if two errors are similar
    547638     */
    548639    public boolean isSimilar(TestError other) {
    549         return getCode() == other.getCode()
     640        return getUniqueCode() == other.getUniqueCode()
     641                && getCode() == other.getCode()
    550642                && getMessage().equals(other.getMessage())
    551643                && getPrimitives().size() == other.getPrimitives().size()
    552644                && getPrimitives().containsAll(other.getPrimitives())
     
    570662
    571663    @Override
    572664    public String toString() {
    573         return "TestError [tester=" + tester + ", code=" + code + ", message=" + message + ']';
     665        return "TestError [tester=" + tester + ", unique code=" + this.uniqueCode +
     666                ", code=" + code + ", message=" + message + ']';
    574667    }
    575668
    576669}
  • src/org/openstreetmap/josm/io/GeoJSONMapRouletteWriter.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/io/GeoJSONMapRouletteWriter.java b/src/org/openstreetmap/josm/io/GeoJSONMapRouletteWriter.java
    a b  
    4545        final JsonObjectBuilder propertiesBuilder = Json.createObjectBuilder();
    4646        propertiesBuilder.add("message", testError.getMessage());
    4747        Optional.ofNullable(testError.getDescription()).ifPresent(description -> propertiesBuilder.add("description", description));
    48         propertiesBuilder.add("code", testError.getCode());
     48        propertiesBuilder.add("code", testError.getUniqueCode());
    4949        propertiesBuilder.add("fixable", testError.isFixable());
    5050        propertiesBuilder.add("severity", testError.getSeverity().toString());
    5151        propertiesBuilder.add("severityInteger", testError.getSeverity().getLevel());
  • new file test/unit/org/openstreetmap/josm/data/validation/TestErrorTest.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/test/unit/org/openstreetmap/josm/data/validation/TestErrorTest.java b/test/unit/org/openstreetmap/josm/data/validation/TestErrorTest.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.validation;
     3
     4import static org.junit.jupiter.api.Assertions.assertAll;
     5import static org.junit.jupiter.api.Assertions.assertEquals;
     6import static org.junit.jupiter.api.Assertions.assertFalse;
     7import static org.junit.jupiter.api.Assertions.assertNotEquals;
     8import static org.junit.jupiter.api.Assertions.assertNull;
     9import static org.junit.jupiter.api.Assertions.assertTrue;
     10
     11import java.util.Arrays;
     12import java.util.Collections;
     13import java.util.List;
     14import java.util.stream.Stream;
     15
     16import org.junit.jupiter.params.ParameterizedTest;
     17import org.junit.jupiter.params.provider.Arguments;
     18import org.junit.jupiter.params.provider.MethodSource;
     19import org.openstreetmap.josm.TestUtils;
     20import org.openstreetmap.josm.data.osm.OsmPrimitive;
     21import org.openstreetmap.josm.data.validation.tests.InternetTags;
     22import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
     23import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
     24
     25/**
     26 * Test class for {@link TestError}
     27 */
     28@BasicPreferences
     29class TestErrorTest {
     30    static Stream<Arguments> testCodeCompatibility() {
     31        return Stream.of(Arguments.of(InternetTags.class, 3301, 1166507262, false, Collections.singletonList(TestUtils.newNode("url=invalid"))),
     32                Arguments.of(InternetTags.class, 3301, 1166507262, true, Collections.singletonList(TestUtils.newNode("url=invalid"))));
     33    }
     34
     35    /**
     36     * See #18230/#21423: Keep error codes unique
     37     *
     38     * @param testClass The test class to use
     39     * @param originalCode The expected error code (original)
     40     * @param expectedCode The expected error code (new, should be {@code testClass.getName().hashCode()})
     41     * @param switchOver {@code true} if the new code should be saved instead of the original code
     42     * @param primitiveCollection The primitives to run the test on
     43     * @throws ReflectiveOperationException If the test class could not be instantiated (no-op constructor)
     44     */
     45    @ParameterizedTest
     46    @MethodSource
     47    void testCodeCompatibility(Class<? extends Test> testClass, int originalCode, int expectedCode,
     48                               boolean switchOver, List<OsmPrimitive> primitiveCollection) throws ReflectiveOperationException {
     49        // Ensure that this test always works
     50        TestError.setUpdateErrorCodes(switchOver);
     51        assertEquals(expectedCode, testClass.getName().hashCode());
     52        // Run the test
     53        final Test test = testClass.getConstructor().newInstance();
     54        test.startTest(NullProgressMonitor.INSTANCE);
     55        test.visit(primitiveCollection);
     56        test.endTest();
     57        assertFalse(test.getErrors().isEmpty());
     58        assertEquals(1, test.getErrors().size());
     59        final TestError testError = test.getErrors().get(0);
     60        final String ignoreGroup = testError.getIgnoreGroup();
     61        final String ignoreSubGroup = testError.getIgnoreSubGroup();
     62        if (primitiveCollection.size() == 1 && primitiveCollection.get(0).isNew()) {
     63            assertNull(testError.getIgnoreState());
     64            primitiveCollection.get(0).setOsmId(1, 1);
     65        }
     66        final String ignoreState = testError.getIgnoreState();
     67        final String startUniqueCode = expectedCode + "_";
     68        assertAll(() -> assertTrue(ignoreGroup.startsWith(startUniqueCode + originalCode)),
     69                () -> assertTrue(ignoreSubGroup.startsWith(startUniqueCode + originalCode)),
     70                () -> assertTrue(ignoreState.startsWith(startUniqueCode + originalCode)));
     71        for (String ignore : Arrays.asList(ignoreGroup, ignoreSubGroup, ignoreState)) {
     72            OsmValidator.clearIgnoredErrors();
     73            final String oldIgnore = ignore.replace(startUniqueCode, "");
     74            OsmValidator.addIgnoredError(oldIgnore);
     75            // Add the ignored error
     76            assertTrue(testError.updateIgnored());
     77            assertAll(() -> assertEquals(switchOver, OsmValidator.hasIgnoredError(ignore)),
     78                    () -> assertNotEquals(switchOver, OsmValidator.hasIgnoredError(oldIgnore)));
     79
     80            OsmValidator.clearIgnoredErrors();
     81            OsmValidator.addIgnoredError(ignore);
     82            // Add the ignored error
     83            assertTrue(testError.updateIgnored());
     84            assertAll(() -> assertTrue(OsmValidator.hasIgnoredError(ignore)),
     85                    () -> assertFalse(OsmValidator.hasIgnoredError(oldIgnore)));
     86        }
     87    }
     88}
  • test/unit/org/openstreetmap/josm/plugins/PluginHandlerTestIT.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/test/unit/org/openstreetmap/josm/plugins/PluginHandlerTestIT.java b/test/unit/org/openstreetmap/josm/plugins/PluginHandlerTestIT.java
    a b  
    33
    44import static org.junit.jupiter.api.Assertions.assertEquals;
    55import static org.junit.jupiter.api.Assertions.assertFalse;
     6import static org.junit.jupiter.api.Assertions.assertNotNull;
    67import static org.junit.jupiter.api.Assertions.assertTrue;
    78
    89import java.awt.GraphicsEnvironment;
     
    1617import java.util.List;
    1718import java.util.Map;
    1819import java.util.Map.Entry;
     20import java.util.Objects;
    1921import java.util.Set;
    2022import java.util.function.Consumer;
     23import java.util.logging.Handler;
     24import java.util.logging.LogRecord;
    2125import java.util.stream.Collectors;
    2226
    2327import org.junit.jupiter.api.BeforeAll;
    2428import org.junit.jupiter.api.Test;
    2529import org.junit.jupiter.api.extension.RegisterExtension;
     30import org.junit.platform.commons.util.ReflectionUtils;
    2631import org.openstreetmap.josm.TestUtils;
    2732import org.openstreetmap.josm.data.Preferences;
    2833import org.openstreetmap.josm.data.gpx.GpxData;
     
    7378
    7479        Map<String, Throwable> loadingExceptions = PluginHandler.pluginLoadingExceptions.entrySet().stream()
    7580                .filter(e -> !(Utils.getRootCause(e.getValue()) instanceof HeadlessException))
    76                 .collect(Collectors.toMap(e -> e.getKey(), e -> Utils.getRootCause(e.getValue())));
     81                .collect(Collectors.toMap(Map.Entry::getKey, e -> Utils.getRootCause(e.getValue())));
    7782
    7883        List<PluginInformation> loadedPlugins = PluginHandler.getPlugins();
    7984        Map<String, List<String>> invalidManifestEntries = loadedPlugins.stream().filter(pi -> !pi.invalidManifestEntries.isEmpty())
     
    9297            testPlugin(MainApplication.getLayerManager()::removeLayer, layer, layerExceptions, loadedPlugins);
    9398        }
    9499
     100        Map<String, String> testCodeHashCollisions = checkForHashCollisions();
     101
    95102        Map<String, Throwable> noRestartExceptions = new HashMap<>();
    96103        testCompletelyRestartlessPlugins(loadedPlugins, noRestartExceptions);
    97104
     
    99106        debugPrint(loadingExceptions);
    100107        debugPrint(layerExceptions);
    101108        debugPrint(noRestartExceptions);
     109        debugPrint(testCodeHashCollisions);
    102110
    103111        invalidManifestEntries = filterKnownErrors(invalidManifestEntries);
    104112        loadingExceptions = filterKnownErrors(loadingExceptions);
    105113        layerExceptions = filterKnownErrors(layerExceptions);
    106114        noRestartExceptions = filterKnownErrors(noRestartExceptions);
     115        testCodeHashCollisions = filterKnownErrors(testCodeHashCollisions);
    107116
    108117        String msg = errMsg("invalidManifestEntries", invalidManifestEntries) + '\n' +
    109118                errMsg("loadingExceptions", loadingExceptions) + '\n' +
    110119                errMsg("layerExceptions", layerExceptions) + '\n' +
    111                 errMsg("noRestartExceptions", noRestartExceptions);
     120                errMsg("noRestartExceptions", noRestartExceptions) + '\n' +
     121                errMsg("testCodeHashCollisions", testCodeHashCollisions);
    112122        assertTrue(invalidManifestEntries.isEmpty()
    113123                && loadingExceptions.isEmpty()
    114124                && layerExceptions.isEmpty()
    115                 && noRestartExceptions.isEmpty(), msg);
     125                && noRestartExceptions.isEmpty()
     126                && testCodeHashCollisions.isEmpty(), msg);
    116127    }
    117128
    118129    private static String errMsg(String type, Map<String, ?> map) {
     
    121132
    122133    private static void testCompletelyRestartlessPlugins(List<PluginInformation> loadedPlugins,
    123134            Map<String, Throwable> noRestartExceptions) {
     135        final List<LogRecord> records = new ArrayList<>();
     136        Handler tempHandler = new Handler() {
     137            @Override
     138            public void publish(LogRecord record) {
     139                records.add(record);
     140            }
     141
     142            @Override
     143            public void flush() { /* Do nothing */ }
     144
     145            @Override
     146            public void close() throws SecurityException { /* Do nothing */ }
     147        };
     148        Logging.getLogger().addHandler(tempHandler);
    124149        try {
    125150            List<PluginInformation> restartable = loadedPlugins.parallelStream()
    126151                    .filter(info -> PluginHandler.getPlugin(info.name) instanceof Destroyable)
     
    141166            Throwable root = Utils.getRootCause(t);
    142167            root.printStackTrace();
    143168            noRestartExceptions.put(findFaultyPlugin(loadedPlugins, root), root);
     169            records.removeIf(record -> Objects.equals(Utils.getRootCause(record.getThrown()), root));
     170        } catch (AssertionError assertionError) {
     171            noRestartExceptions.put("Plugin load/unload failed", assertionError);
     172        } finally {
     173            Logging.getLogger().removeHandler(tempHandler);
     174            for (LogRecord record : records) {
     175                if (record.getThrown() != null) {
     176                    Throwable root = Utils.getRootCause(record.getThrown());
     177                    root.printStackTrace();
     178                    noRestartExceptions.put(findFaultyPlugin(loadedPlugins, root), root);
     179                }
     180            }
    144181        }
    145182    }
     183
     184    private static Map<String, String> checkForHashCollisions() {
     185        Map<Integer, List<String>> codes = new HashMap<>();
     186        for (Class<?> clazz : ReflectionUtils.findAllClassesInPackage("org.openstreetmap",
     187                org.openstreetmap.josm.data.validation.Test.class::isAssignableFrom, s -> true)) {
     188            if (org.openstreetmap.josm.data.validation.Test.class.isAssignableFrom(clazz)
     189            && !Objects.equals(org.openstreetmap.josm.data.validation.Test.class, clazz)) {
     190                // clazz.getName().hashCode() is how the base error codes are calculated since xxx
     191                // We want to avoid cases where the hashcode is too close, so we want to
     192                // ensure that there is at least 1m available codes after the hashCode.
     193                // This is needed since some plugins pick some really large number, and count up from there.
     194                int hashCeil = (int) Math.ceil(clazz.getName().hashCode() / 1_000_000d);
     195                int hashFloor = (int) Math.floor(clazz.getName().hashCode() / 1_000_000d);
     196                codes.computeIfAbsent(hashCeil, k -> new ArrayList<>()).add(clazz.getName());
     197                codes.computeIfAbsent(hashFloor, k -> new ArrayList<>()).add(clazz.getName());
     198            }
     199        }
     200        return codes.entrySet().stream().filter(entry -> entry.getValue().size() > 1).collect(
     201                Collectors.toMap(entry -> entry.getKey().toString(), entry -> String.join(", ", entry.getValue())));
     202    }
    146203
    147204    private static <T> Map<String, T> filterKnownErrors(Map<String, T> errorMap) {
    148205        return errorMap.entrySet().parallelStream()
     
    153210    private static void debugPrint(Map<String, ?> invalidManifestEntries) {
    154211        System.out.println(invalidManifestEntries.entrySet()
    155212                .stream()
    156                 .map(e -> convertEntryToString(e))
     213                .map(PluginHandlerTestIT::convertEntryToString)
    157214                .collect(Collectors.joining(", ")));
    158215    }
    159216
     
    203260        if (GraphicsEnvironment.isHeadless()) {
    204261            for (Iterator<PluginInformation> it = plugins.iterator(); it.hasNext();) {
    205262                PluginInformation pi = it.next();
    206                 if (pi.isExternal()) {
     263                if (pi.isExternal() && (pi.name.equals("jogl") || pi.requires == null || pi.requires.contains("jogl"))) {
    207264                    System.out.println("Ignoring " + pi.name + " (unofficial plugin in headless mode)");
    208265                    it.remove();
    209266                }
     
    241298        for (PluginInformation p : plugins) {
    242299            try {
    243300                ClassLoader cl = PluginHandler.getPluginClassLoader(p.getName());
     301                assertNotNull(cl);
    244302                String pluginPackage = cl.loadClass(p.className).getPackage().getName();
    245303                for (StackTraceElement e : root.getStackTrace()) {
    246304                    try {