Index: trunk/src/org/openstreetmap/josm/gui/preferences/plugin/PluginPreference.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/preferences/plugin/PluginPreference.java	(revision 18693)
+++ trunk/src/org/openstreetmap/josm/gui/preferences/plugin/PluginPreference.java	(revision 18694)
@@ -18,4 +18,5 @@
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
@@ -432,7 +433,33 @@
                     return;
                 }
+                int toUpdateSize;
+                boolean refreshRequired = false;
+                do {
+                    toUpdateSize = toUpdate.size();
+                    Set<PluginInformation> enabledPlugins = new HashSet<>(PluginHandler.getPlugins());
+                    enabledPlugins.addAll(toUpdate);
+                    Set<PluginInformation> toAdd = new HashSet<>();
+                    for (PluginInformation pi : toUpdate) {
+                        if (!PluginHandler.checkRequiredPluginsPreconditions(null, enabledPlugins, pi, false)) {
+                            // Time to find the missing plugins...
+                            toAdd.addAll(pi.getRequiredPlugins().stream().filter(plugin -> PluginHandler.getPlugin(plugin) == null)
+                                    .map(plugin -> model.getPluginInformation(plugin))
+                                    .collect(Collectors.toSet()));
+                        }
+                    }
+                    toAdd.forEach(plugin -> model.setPluginSelected(plugin.name, true));
+                    refreshRequired |= !toAdd.isEmpty(); // We need to force refresh the checkboxes if we are adding new plugins
+                    toAdd.removeIf(plugin -> !plugin.isUpdateRequired()); // Avoid downloading plugins that already exist
+                    toUpdate.addAll(toAdd);
+                } while (toUpdateSize != toUpdate.size());
+
                 pluginDownloadTask.setPluginsToDownload(toUpdate);
                 MainApplication.worker.submit(pluginDownloadTask);
                 MainApplication.worker.submit(pluginDownloadContinuation);
+                if (refreshRequired) {
+                    // Needed since we need to recreate the checkboxes to show the enabled dependent plugins that were not previously enabled
+                    pnlPluginPreferences.resetDisplayedComponents();
+                }
+                GuiHelper.runInEDT(pnlPluginPreferences::refreshView);
             };
 
Index: trunk/test/data/META-INF/services/org.junit.jupiter.api.extension.Extension
===================================================================
--- trunk/test/data/META-INF/services/org.junit.jupiter.api.extension.Extension	(revision 18693)
+++ trunk/test/data/META-INF/services/org.junit.jupiter.api.extension.Extension	(revision 18694)
@@ -1,2 +1,8 @@
+# These are the default extensions to run (all tests with JOSMTestRules would have done these by default)
+org.openstreetmap.josm.testutils.annotations.BasicPreferences$BasicPreferencesExtension
 org.openstreetmap.josm.testutils.annotations.HTTP$HTTPExtension
+org.openstreetmap.josm.testutils.annotations.I18n$I18nExtension
+org.openstreetmap.josm.testutils.annotations.JosmDefaults$DefaultsExtension
 org.openstreetmap.josm.testutils.annotations.LayerManager$LayerManagerExtension
+org.openstreetmap.josm.testutils.annotations.Logging$LoggingExtension
+org.openstreetmap.josm.testutils.annotations.Timezone$TimezoneExtension
Index: trunk/test/unit/org/openstreetmap/josm/gui/dialogs/MinimapDialogTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/gui/dialogs/MinimapDialogTest.java	(revision 18693)
+++ trunk/test/unit/org/openstreetmap/josm/gui/dialogs/MinimapDialogTest.java	(revision 18694)
@@ -30,4 +30,5 @@
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import org.awaitility.Awaitility;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -35,4 +36,5 @@
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.data.DataSource;
+import org.openstreetmap.josm.data.SystemOfMeasurement;
 import org.openstreetmap.josm.data.imagery.ImageryInfo;
 import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
@@ -63,4 +65,10 @@
     @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
     public JOSMTestRules josmTestRules = new JOSMTestRules().main().projection().fakeImagery();
+
+    @Before
+    public void beforeAll() {
+        // Needed since testShowDownloadedAreaLayerSwitching expects the measurement to be imperial
+        SystemOfMeasurement.setSystemOfMeasurement(SystemOfMeasurement.IMPERIAL);
+    }
 
     /**
Index: trunk/test/unit/org/openstreetmap/josm/gui/preferences/plugin/PluginPreferenceHighLevelTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/gui/preferences/plugin/PluginPreferenceHighLevelTest.java	(revision 18693)
+++ trunk/test/unit/org/openstreetmap/josm/gui/preferences/plugin/PluginPreferenceHighLevelTest.java	(revision 18694)
@@ -19,9 +19,13 @@
 import javax.swing.JOptionPane;
 
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import mockit.MockUp;
 import org.awaitility.Awaitility;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
 import org.openstreetmap.josm.TestUtils;
 import org.openstreetmap.josm.data.Preferences;
@@ -32,35 +36,27 @@
 import org.openstreetmap.josm.plugins.PluginProxy;
 import org.openstreetmap.josm.spi.preferences.Config;
-import org.openstreetmap.josm.testutils.JOSMTestRules;
 import org.openstreetmap.josm.testutils.PluginServer;
+import org.openstreetmap.josm.testutils.annotations.AssertionsInEDT;
+import org.openstreetmap.josm.testutils.annotations.AssumeRevision;
+import org.openstreetmap.josm.testutils.annotations.FullPreferences;
+import org.openstreetmap.josm.testutils.annotations.Main;
 import org.openstreetmap.josm.testutils.mockers.HelpAwareOptionPaneMocker;
 import org.openstreetmap.josm.testutils.mockers.JOptionPaneSimpleMocker;
-
-import com.github.tomakehurst.wiremock.client.WireMock;
-import com.github.tomakehurst.wiremock.junit.WireMockRule;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import mockit.MockUp;
 
 /**
  * Higher level tests of {@link PluginPreference} class.
  */
-public class PluginPreferenceHighLevelTest {
-    /**
-     * Setup test.
-     */
-    @Rule
-    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules().assumeRevision(
-        "Revision: 10000\n"
-    ).preferences().main().assertionsInEDT();
-
+@AssumeRevision("Revision: 10000\n")
+@AssertionsInEDT
+@FullPreferences
+@Main
+class PluginPreferenceHighLevelTest {
     /**
      * Plugin server mock.
      */
-    @Rule
-    public WireMockRule pluginServerRule = new WireMockRule(
-        options().dynamicPort().usingFilesUnderDirectory(TestUtils.getTestDataRoot())
-    );
+    @RegisterExtension
+    static WireMockExtension pluginServerRule = WireMockExtension.newInstance()
+            .options(options().dynamicPort().usingFilesUnderDirectory(TestUtils.getTestDataRoot()))
+            .build();
 
     /**
@@ -68,5 +64,5 @@
      * @throws ReflectiveOperationException never
      */
-    @Before
+    @BeforeEach
     public void setUp() throws ReflectiveOperationException {
 
@@ -88,5 +84,5 @@
         Config.getPref().put("pluginmanager.lastupdate", "999");
         Config.getPref().putList("pluginmanager.sites",
-            Collections.singletonList(this.pluginServerRule.url("/plugins"))
+            Collections.singletonList(pluginServerRule.url("/plugins"))
         );
 
@@ -107,5 +103,5 @@
      * @throws ReflectiveOperationException never
      */
-    @After
+    @AfterEach
     public void tearDown() throws ReflectiveOperationException {
         // restore actual PluginHandler#pluginList
@@ -136,5 +132,5 @@
      */
     @Test
-    public void testInstallWithoutUpdate() throws Exception {
+    void testInstallWithoutUpdate(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception {
         final PluginServer pluginServer = new PluginServer(
             new PluginServer.RemotePlugin(this.referenceDummyJarNew),
@@ -142,5 +138,5 @@
             new PluginServer.RemotePlugin(null, Collections.singletonMap("Plugin-Version", "2"), "irrelevant_plugin")
         );
-        pluginServer.applyToWireMockServer(this.pluginServerRule);
+        pluginServer.applyToWireMockServer(wireMockRuntimeInfo);
         Config.getPref().putList("plugins", Collections.singletonList("dummy_plugin"));
 
@@ -169,6 +165,6 @@
         Awaitility.await().atMost(2000, MILLISECONDS).until(() -> Config.getPref().getInt("pluginmanager.version", 999) != 999);
 
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
-        WireMock.resetAllRequests();
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
+        pluginServerRule.resetRequests();
 
         final PluginPreferencesModel model = (PluginPreferencesModel) TestUtils.getPrivateField(
@@ -183,35 +179,22 @@
         assertEquals(model.getDisplayedPlugins(), model.getAvailablePlugins());
 
-        assertEquals(
-            Arrays.asList("baz_plugin", "dummy_plugin", "irrelevant_plugin"),
-            model.getAvailablePlugins().stream().map(PluginInformation::getName).collect(Collectors.toList())
-        );
-        assertEquals(
-            Collections.singletonList("dummy_plugin"),
-            model.getSelectedPlugins().stream().map(PluginInformation::getName).collect(Collectors.toList())
-        );
-        assertEquals(
-            Arrays.asList("(null)", "31701", "(null)"),
-            model.getAvailablePlugins().stream().map(
-                (pi) -> pi.localversion == null ? "(null)" : pi.localversion
-            ).collect(Collectors.toList())
-        );
-        assertEquals(
-            Arrays.asList("6", "31772", "2"),
-            model.getAvailablePlugins().stream().map((pi) -> pi.version).collect(Collectors.toList())
-        );
+        assertEquals(Arrays.asList("baz_plugin", "dummy_plugin", "irrelevant_plugin"),
+                model.getAvailablePlugins().stream().map(PluginInformation::getName).collect(Collectors.toList()));
+        assertEquals(Collections.singletonList("dummy_plugin"),
+                model.getSelectedPlugins().stream().map(PluginInformation::getName).collect(Collectors.toList()));
+        assertEquals(Arrays.asList("(null)", "31701", "(null)"), model.getAvailablePlugins().stream().map(
+            (pi) -> pi.localversion == null ? "(null)" : pi.localversion
+        ).collect(Collectors.toList()));
+        assertEquals(Arrays.asList("6", "31772", "2"),
+                model.getAvailablePlugins().stream().map((pi) -> pi.version).collect(Collectors.toList()));
 
         // now we're going to choose to install baz_plugin
         model.setPluginSelected("baz_plugin", true);
 
-        assertEquals(
-            Collections.singletonList("baz_plugin"),
-            model.getNewlyActivatedPlugins().stream().map(PluginInformation::getName).collect(Collectors.toList())
-        );
-        assertTrue(model.getNewlyDeactivatedPlugins().isEmpty());
-        assertEquals(
-            Collections.singletonList("baz_plugin"),
-            model.getPluginsScheduledForUpdateOrDownload().stream().map(PluginInformation::getName).collect(Collectors.toList())
-        );
+        assertEquals(Collections.singletonList("baz_plugin"),
+                model.getNewlyActivatedPlugins().stream().map(PluginInformation::getName).collect(Collectors.toList()));
+        assertTrue(model.getNewlyDeactivatedPlugins().isEmpty());
+        assertEquals(Collections.singletonList("baz_plugin"),
+                model.getPluginsScheduledForUpdateOrDownload().stream().map(PluginInformation::getName).collect(Collectors.toList()));
 
         tabbedPane.savePreferences();
@@ -233,8 +216,8 @@
         assertFalse(targetBazJarNew.exists());
 
-        // the advertized version of dummy_plugin shouldn't have been fetched
-        this.pluginServerRule.verify(0, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/dummy_plugin.v31772.jar")));
+        // the advertised version of dummy_plugin shouldn't have been fetched
+        pluginServerRule.verify(0, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/dummy_plugin.v31772.jar")));
         // but the advertized version of baz_plugin *should* have
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/baz_plugin.v6.jar")));
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/baz_plugin.v6.jar")));
 
         // pluginmanager.version has been set to the current version
@@ -246,8 +229,6 @@
 
         // baz_plugin should have been added to the plugins list
-        assertEquals(
-            Arrays.asList("baz_plugin", "dummy_plugin"),
-            Config.getPref().getList("plugins", null).stream().sorted().collect(Collectors.toList())
-        );
+        assertEquals(Arrays.asList("baz_plugin", "dummy_plugin"),
+                Config.getPref().getList("plugins", null).stream().sorted().collect(Collectors.toList()));
     }
 
@@ -257,5 +238,5 @@
      */
     @Test
-    public void testDisablePluginWithUpdatesAvailable() throws Exception {
+    void testDisablePluginWithUpdatesAvailable(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception {
         final PluginServer pluginServer = new PluginServer(
             new PluginServer.RemotePlugin(this.referenceDummyJarNew),
@@ -263,5 +244,5 @@
             new PluginServer.RemotePlugin(null, null, "irrelevant_plugin")
         );
-        pluginServer.applyToWireMockServer(this.pluginServerRule);
+        pluginServer.applyToWireMockServer(wireMockRuntimeInfo);
         Config.getPref().putList("plugins", Arrays.asList("baz_plugin", "dummy_plugin"));
 
@@ -289,6 +270,6 @@
         Awaitility.await().atMost(2000, MILLISECONDS).until(() -> Config.getPref().getInt("pluginmanager.version", 999) != 999);
 
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
-        WireMock.resetAllRequests();
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
+        pluginServerRule.resetRequests();
 
         final PluginPreferencesModel model = (PluginPreferencesModel) TestUtils.getPrivateField(
@@ -303,24 +284,14 @@
         assertEquals(model.getDisplayedPlugins(), model.getAvailablePlugins());
 
-        assertEquals(
-            Arrays.asList("baz_plugin", "dummy_plugin", "irrelevant_plugin"),
-            model.getAvailablePlugins().stream().map(PluginInformation::getName).collect(Collectors.toList())
-        );
-        assertEquals(
-            Arrays.asList("baz_plugin", "dummy_plugin"),
-            model.getSelectedPlugins().stream().map(PluginInformation::getName).collect(Collectors.toList())
-        );
-        assertEquals(
-            Arrays.asList("6", "31701", "(null)"),
-            model.getAvailablePlugins().stream().map(
-                (pi) -> pi.localversion == null ? "(null)" : pi.localversion
-            ).collect(Collectors.toList())
-        );
-        assertEquals(
-            Arrays.asList("7", "31772", "(null)"),
-            model.getAvailablePlugins().stream().map(
-                (pi) -> pi.version == null ? "(null)" : pi.version
-            ).collect(Collectors.toList())
-        );
+        assertEquals(Arrays.asList("baz_plugin", "dummy_plugin", "irrelevant_plugin"),
+                model.getAvailablePlugins().stream().map(PluginInformation::getName).collect(Collectors.toList()));
+        assertEquals(Arrays.asList("baz_plugin", "dummy_plugin"),
+                model.getSelectedPlugins().stream().map(PluginInformation::getName).collect(Collectors.toList()));
+        assertEquals(Arrays.asList("6", "31701", "(null)"), model.getAvailablePlugins().stream().map(
+            (pi) -> pi.localversion == null ? "(null)" : pi.localversion
+        ).collect(Collectors.toList()));
+        assertEquals(Arrays.asList("7", "31772", "(null)"), model.getAvailablePlugins().stream().map(
+            (pi) -> pi.version == null ? "(null)" : pi.version
+        ).collect(Collectors.toList()));
 
         // now we're going to choose to disable baz_plugin
@@ -328,8 +299,6 @@
 
         assertTrue(model.getNewlyActivatedPlugins().isEmpty());
-        assertEquals(
-            Collections.singletonList("baz_plugin"),
-            model.getNewlyDeactivatedPlugins().stream().map(PluginInformation::getName).collect(Collectors.toList())
-        );
+        assertEquals(Collections.singletonList("baz_plugin"),
+                model.getNewlyDeactivatedPlugins().stream().map(PluginInformation::getName).collect(Collectors.toList()));
         // questionably correct
         assertTrue(model.getPluginsScheduledForUpdateOrDownload().isEmpty());
@@ -354,6 +323,6 @@
 
         // neither of the new jars have been fetched
-        this.pluginServerRule.verify(0, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/dummy_plugin.v31772.jar")));
-        this.pluginServerRule.verify(0, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/baz_plugin.v6.jar")));
+        pluginServerRule.verify(0, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/dummy_plugin.v31772.jar")));
+        pluginServerRule.verify(0, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/baz_plugin.v6.jar")));
 
         // pluginmanager.version has been set to the current version
@@ -365,8 +334,6 @@
 
         // baz_plugin should have been removed from the installed plugins list
-        assertEquals(
-            Collections.singletonList("dummy_plugin"),
-            Config.getPref().getList("plugins", null).stream().sorted().collect(Collectors.toList())
-        );
+        assertEquals(Collections.singletonList("dummy_plugin"),
+                Config.getPref().getList("plugins", null).stream().sorted().collect(Collectors.toList()));
     }
 
@@ -379,5 +346,5 @@
      */
     @Test
-    public void testUpdateOnlySelectedPlugin() throws Exception {
+    void testUpdateOnlySelectedPlugin(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception {
         TestUtils.assumeWorkingJMockit();
         final PluginServer pluginServer = new PluginServer(
@@ -385,5 +352,5 @@
             new PluginServer.RemotePlugin(this.referenceBazJarNew)
         );
-        pluginServer.applyToWireMockServer(this.pluginServerRule);
+        pluginServer.applyToWireMockServer(wireMockRuntimeInfo);
         Config.getPref().putList("plugins", Arrays.asList("baz_plugin", "dummy_plugin"));
 
@@ -406,6 +373,6 @@
         TestUtils.syncEDTAndWorkerThreads();
 
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
-        WireMock.resetAllRequests();
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
+        pluginServerRule.resetRequests();
 
         final PluginPreferencesModel model = (PluginPreferencesModel) TestUtils.getPrivateField(
@@ -420,22 +387,12 @@
         assertEquals(model.getDisplayedPlugins(), model.getAvailablePlugins());
 
-        assertEquals(
-            Arrays.asList("baz_plugin", "dummy_plugin"),
-            model.getAvailablePlugins().stream().map(PluginInformation::getName).collect(Collectors.toList())
-        );
-        assertEquals(
-            Arrays.asList("baz_plugin", "dummy_plugin"),
-            model.getSelectedPlugins().stream().map(PluginInformation::getName).collect(Collectors.toList())
-        );
-        assertEquals(
-            Arrays.asList("6", "31701"),
-            model.getAvailablePlugins().stream().map(
-                (pi) -> pi.localversion == null ? "(null)" : pi.localversion
-            ).collect(Collectors.toList())
-        );
-        assertEquals(
-            Arrays.asList("7", "31772"),
-            model.getAvailablePlugins().stream().map((pi) -> pi.version).collect(Collectors.toList())
-        );
+        assertEquals(Arrays.asList("baz_plugin", "dummy_plugin"),
+                model.getAvailablePlugins().stream().map(PluginInformation::getName).collect(Collectors.toList()));
+        assertEquals(Arrays.asList("baz_plugin", "dummy_plugin"),
+                model.getSelectedPlugins().stream().map(PluginInformation::getName).collect(Collectors.toList()));
+        assertEquals(Arrays.asList("6", "31701"), model.getAvailablePlugins().stream().map(
+            (pi) -> pi.localversion == null ? "(null)" : pi.localversion
+        ).collect(Collectors.toList()));
+        assertEquals(Arrays.asList("7", "31772"), model.getAvailablePlugins().stream().map((pi) -> pi.version).collect(Collectors.toList()));
 
         // now we're going to choose not to update baz_plugin
@@ -443,8 +400,6 @@
 
         assertTrue(model.getNewlyActivatedPlugins().isEmpty());
-        assertEquals(
-            Collections.singletonList("baz_plugin"),
-            model.getNewlyDeactivatedPlugins().stream().map(PluginInformation::getName).collect(Collectors.toList())
-        );
+        assertEquals(Collections.singletonList("baz_plugin"), model.getNewlyDeactivatedPlugins().stream()
+                .map(PluginInformation::getName).collect(Collectors.toList()));
         // questionably correct
         assertTrue(model.getPluginsScheduledForUpdateOrDownload().isEmpty());
@@ -481,10 +436,10 @@
         // the plugin list was rechecked
         // questionably necessary
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
         // dummy_plugin has been fetched
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/dummy_plugin.v31772.jar")));
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/dummy_plugin.v31772.jar")));
         // baz_plugin has not
-        this.pluginServerRule.verify(0, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/baz_plugin.v7.jar")));
-        WireMock.resetAllRequests();
+        pluginServerRule.verify(0, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/baz_plugin.v7.jar")));
+        pluginServerRule.resetRequests();
 
         // pluginmanager.version has been set to the current version
@@ -496,15 +451,11 @@
 
         // plugins list shouldn't have been altered, we haven't hit save yet
-        assertEquals(
-                Arrays.asList("baz_plugin", "dummy_plugin"),
-            Config.getPref().getList("plugins", null).stream().sorted().collect(Collectors.toList())
-        );
+        assertEquals(Arrays.asList("baz_plugin", "dummy_plugin"),
+                Config.getPref().getList("plugins", null).stream().sorted().collect(Collectors.toList()));
 
         // the model's selection state should be largely as before
         assertTrue(model.getNewlyActivatedPlugins().isEmpty());
-        assertEquals(
-            Collections.singletonList("baz_plugin"),
-            model.getNewlyDeactivatedPlugins().stream().map(PluginInformation::getName).collect(Collectors.toList())
-        );
+        assertEquals(Collections.singletonList("baz_plugin"), model.getNewlyDeactivatedPlugins().stream()
+                .map(PluginInformation::getName).collect(Collectors.toList()));
         assertTrue(model.getPluginsScheduledForUpdateOrDownload().isEmpty());
 
@@ -516,8 +467,6 @@
         assertTrue(model.getNewlyActivatedPlugins().isEmpty());
         assertTrue(model.getNewlyDeactivatedPlugins().isEmpty());
-        assertEquals(
-            Collections.singletonList("baz_plugin"),
-            model.getPluginsScheduledForUpdateOrDownload().stream().map(PluginInformation::getName).collect(Collectors.toList())
-        );
+        assertEquals(Collections.singletonList("baz_plugin"), model.getPluginsScheduledForUpdateOrDownload().stream()
+                .map(PluginInformation::getName).collect(Collectors.toList()));
 
         // prepare jopsMocker to handle this message
@@ -552,8 +501,8 @@
 
         // dummy_plugin was not fetched
-        this.pluginServerRule.verify(0, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/dummy_plugin.v31772.jar")));
+        pluginServerRule.verify(0, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/dummy_plugin.v31772.jar")));
         // baz_plugin however was fetched
         // questionably correct
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/baz_plugin.v7.jar")));
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/baz_plugin.v7.jar")));
 
         assertEquals(10000, Config.getPref().getInt("pluginmanager.version", 111));
@@ -567,5 +516,5 @@
      */
     @Test
-    public void testUpdateWithNoAvailableUpdates() throws Exception {
+    void testUpdateWithNoAvailableUpdates(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception {
         TestUtils.assumeWorkingJMockit();
         final PluginServer pluginServer = new PluginServer(
@@ -574,5 +523,5 @@
             new PluginServer.RemotePlugin(null, Collections.singletonMap("Plugin-Version", "123"), "irrelevant_plugin")
         );
-        pluginServer.applyToWireMockServer(this.pluginServerRule);
+        pluginServer.applyToWireMockServer(wireMockRuntimeInfo);
         Config.getPref().putList("plugins", Arrays.asList("baz_plugin", "dummy_plugin"));
 
@@ -600,6 +549,6 @@
         TestUtils.syncEDTAndWorkerThreads();
 
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
-        WireMock.resetAllRequests();
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
+        pluginServerRule.resetRequests();
 
         final PluginPreferencesModel model = (PluginPreferencesModel) TestUtils.getPrivateField(
@@ -613,22 +562,13 @@
         assertEquals(model.getDisplayedPlugins(), model.getAvailablePlugins());
 
-        assertEquals(
-            Arrays.asList("baz_plugin", "dummy_plugin", "irrelevant_plugin"),
-            model.getAvailablePlugins().stream().map(PluginInformation::getName).collect(Collectors.toList())
-        );
-        assertEquals(
-            Arrays.asList("baz_plugin", "dummy_plugin"),
-            model.getSelectedPlugins().stream().map(PluginInformation::getName).collect(Collectors.toList())
-        );
-        assertEquals(
-            Arrays.asList("6", "31701", "(null)"),
-            model.getAvailablePlugins().stream().map(
-                (pi) -> pi.localversion == null ? "(null)" : pi.localversion
-            ).collect(Collectors.toList())
-        );
-        assertEquals(
-            Arrays.asList("6", "31701", "123"),
-            model.getAvailablePlugins().stream().map((pi) -> pi.version).collect(Collectors.toList())
-        );
+        assertEquals(Arrays.asList("baz_plugin", "dummy_plugin", "irrelevant_plugin"), model.getAvailablePlugins().stream()
+                .map(PluginInformation::getName).collect(Collectors.toList()));
+        assertEquals(Arrays.asList("baz_plugin", "dummy_plugin"), model.getSelectedPlugins().stream()
+                .map(PluginInformation::getName).collect(Collectors.toList()));
+        assertEquals(Arrays.asList("6", "31701", "(null)"), model.getAvailablePlugins().stream().map(
+            (pi) -> pi.localversion == null ? "(null)" : pi.localversion
+        ).collect(Collectors.toList()));
+        assertEquals(Arrays.asList("6", "31701", "123"), model.getAvailablePlugins()
+                .stream().map((pi) -> pi.version).collect(Collectors.toList()));
 
         GuiHelper.runInEDTAndWait(
@@ -654,8 +594,8 @@
         // the plugin list was rechecked
         // questionably necessary
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
         // that should have been the only request to our PluginServer
-        assertEquals(1, this.pluginServerRule.getAllServeEvents().size());
-        WireMock.resetAllRequests();
+        assertEquals(1, pluginServerRule.getAllServeEvents().size());
+        pluginServerRule.resetRequests();
 
         // pluginmanager.version has been set to the current version
@@ -666,8 +606,6 @@
 
         // plugins list shouldn't have been altered
-        assertEquals(
-            Arrays.asList("baz_plugin", "dummy_plugin"),
-            Config.getPref().getList("plugins", null).stream().sorted().collect(Collectors.toList())
-        );
+        assertEquals(Arrays.asList("baz_plugin", "dummy_plugin"), Config.getPref().getList("plugins", null)
+                .stream().sorted().collect(Collectors.toList()));
 
         // the model's selection state should be largely as before
@@ -692,5 +630,5 @@
 
         // none of PluginServer's URLs should have been touched
-        assertEquals(0, this.pluginServerRule.getAllServeEvents().size());
+        assertEquals(0, pluginServerRule.getAllServeEvents().size());
 
         // pluginmanager.version has been set to the current version
@@ -706,5 +644,5 @@
      */
     @Test
-    public void testInstallWithoutRestartRequired() throws Exception {
+    void testInstallWithoutRestartRequired(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception {
         TestUtils.assumeWorkingJMockit();
         final boolean[] loadPluginsCalled = new boolean[] {false};
@@ -727,5 +665,5 @@
             new PluginServer.RemotePlugin(this.referenceBazJarNew)
         );
-        pluginServer.applyToWireMockServer(this.pluginServerRule);
+        pluginServer.applyToWireMockServer(wireMockRuntimeInfo);
         Config.getPref().putList("plugins", Collections.emptyList());
 
@@ -749,6 +687,6 @@
         TestUtils.syncEDTAndWorkerThreads();
 
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
-        WireMock.resetAllRequests();
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
+        pluginServerRule.resetRequests();
 
         final PluginPreferencesModel model = (PluginPreferencesModel) TestUtils.getPrivateField(
@@ -762,19 +700,11 @@
         assertEquals(model.getDisplayedPlugins(), model.getAvailablePlugins());
 
-        assertEquals(
-            Arrays.asList("baz_plugin", "dummy_plugin"),
-            model.getAvailablePlugins().stream().map(PluginInformation::getName).collect(Collectors.toList())
-        );
+        assertEquals(Arrays.asList("baz_plugin", "dummy_plugin"), model.getAvailablePlugins().stream()
+                .map(PluginInformation::getName).collect(Collectors.toList()));
         assertTrue(model.getSelectedPlugins().isEmpty());
-        assertEquals(
-            Arrays.asList("(null)", "(null)"),
-            model.getAvailablePlugins().stream().map(
-                (pi) -> pi.localversion == null ? "(null)" : pi.localversion
-            ).collect(Collectors.toList())
-        );
-        assertEquals(
-            Arrays.asList("7", "31772"),
-            model.getAvailablePlugins().stream().map((pi) -> pi.version).collect(Collectors.toList())
-        );
+        assertEquals(Arrays.asList("(null)", "(null)"), model.getAvailablePlugins().stream().map(
+            (pi) -> pi.localversion == null ? "(null)" : pi.localversion
+        ).collect(Collectors.toList()));
+        assertEquals(Arrays.asList("7", "31772"), model.getAvailablePlugins().stream().map((pi) -> pi.version).collect(Collectors.toList()));
 
         // now we select dummy_plugin
@@ -782,8 +712,6 @@
 
         // model should now reflect this
-        assertEquals(
-            Collections.singletonList("dummy_plugin"),
-            model.getNewlyActivatedPlugins().stream().map(PluginInformation::getName).collect(Collectors.toList())
-        );
+        assertEquals(Collections.singletonList("dummy_plugin"), model.getNewlyActivatedPlugins().stream()
+                .map(PluginInformation::getName).collect(Collectors.toList()));
         assertTrue(model.getNewlyDeactivatedPlugins().isEmpty());
 
@@ -804,5 +732,5 @@
 
         // dummy_plugin was fetched
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/dummy_plugin.v31772.jar")));
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/dummy_plugin.v31772.jar")));
 
         // the dummy_plugin jar has been installed
@@ -826,7 +754,7 @@
      * @throws Exception on failure
      */
-    @JOSMTestRules.OverrideAssumeRevision("Revision: 7000\n")
+    @AssumeRevision("Revision: 7000\n")
     @Test
-    public void testInstallMultiVersion() throws Exception {
+    void testInstallMultiVersion(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception {
         TestUtils.assumeWorkingJMockit();
 
@@ -835,10 +763,10 @@
             new PluginServer.RemotePlugin(this.referenceDummyJarNew),
             new PluginServer.RemotePlugin(this.referenceBazJarNew, Collections.singletonMap(
-                "6800_Plugin-Url", "6;" + this.pluginServerRule.url(bazOldServePath)
+                "6800_Plugin-Url", "6;" + pluginServerRule.url(bazOldServePath)
             ))
         );
-        pluginServer.applyToWireMockServer(this.pluginServerRule);
+        pluginServer.applyToWireMockServer(wireMockRuntimeInfo);
         // need to actually serve this older jar from somewhere
-        this.pluginServerRule.stubFor(
+        pluginServerRule.stubFor(
             WireMock.get(WireMock.urlEqualTo(bazOldServePath)).willReturn(
                 WireMock.aResponse().withStatus(200).withHeader("Content-Type", "application/java-archive").withBodyFile(
@@ -870,6 +798,6 @@
         TestUtils.syncEDTAndWorkerThreads();
 
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
-        WireMock.resetAllRequests();
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
+        pluginServerRule.resetRequests();
 
         final PluginPreferencesModel model = (PluginPreferencesModel) TestUtils.getPrivateField(
@@ -883,19 +811,11 @@
         assertEquals(model.getDisplayedPlugins(), model.getAvailablePlugins());
 
-        assertEquals(
-            Arrays.asList("baz_plugin", "dummy_plugin"),
-            model.getAvailablePlugins().stream().map(PluginInformation::getName).collect(Collectors.toList())
-        );
+        assertEquals(Arrays.asList("baz_plugin", "dummy_plugin"), model.getAvailablePlugins().stream()
+                .map(PluginInformation::getName).collect(Collectors.toList()));
         assertTrue(model.getSelectedPlugins().isEmpty());
-        assertEquals(
-            Arrays.asList("(null)", "(null)"),
-            model.getAvailablePlugins().stream().map(
-                (pi) -> pi.localversion == null ? "(null)" : pi.localversion
-            ).collect(Collectors.toList())
-        );
-        assertEquals(
-            Arrays.asList("6", "31772"),
-            model.getAvailablePlugins().stream().map((pi) -> pi.version).collect(Collectors.toList())
-        );
+        assertEquals(Arrays.asList("(null)", "(null)"), model.getAvailablePlugins().stream().map(
+            (pi) -> pi.localversion == null ? "(null)" : pi.localversion
+        ).collect(Collectors.toList()));
+        assertEquals(Arrays.asList("6", "31772"), model.getAvailablePlugins().stream().map((pi) -> pi.version).collect(Collectors.toList()));
 
         // now we select dummy_plugin
@@ -903,8 +823,6 @@
 
         // model should now reflect this
-        assertEquals(
-            Collections.singletonList("baz_plugin"),
-            model.getNewlyActivatedPlugins().stream().map(PluginInformation::getName).collect(Collectors.toList())
-        );
+        assertEquals(Collections.singletonList("baz_plugin"), model.getNewlyActivatedPlugins().stream()
+                .map(PluginInformation::getName).collect(Collectors.toList()));
         assertTrue(model.getNewlyDeactivatedPlugins().isEmpty());
 
@@ -925,5 +843,5 @@
 
         // dummy_plugin was fetched
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo(bazOldServePath)));
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo(bazOldServePath)));
 
         // the "old" baz_plugin jar has been installed
Index: trunk/test/unit/org/openstreetmap/josm/plugins/PluginHandlerJOSMTooOldTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/plugins/PluginHandlerJOSMTooOldTest.java	(revision 18693)
+++ trunk/test/unit/org/openstreetmap/josm/plugins/PluginHandlerJOSMTooOldTest.java	(revision 18694)
@@ -18,7 +18,11 @@
 import java.util.stream.Collectors;
 
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
 import org.openstreetmap.josm.TestUtils;
 import org.openstreetmap.josm.data.Preferences;
@@ -27,42 +31,39 @@
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 import org.openstreetmap.josm.testutils.PluginServer;
+import org.openstreetmap.josm.testutils.annotations.AssumeRevision;
+import org.openstreetmap.josm.testutils.annotations.FullPreferences;
 import org.openstreetmap.josm.testutils.mockers.ExtendedDialogMocker;
 import org.openstreetmap.josm.testutils.mockers.HelpAwareOptionPaneMocker;
-
-import com.github.tomakehurst.wiremock.client.WireMock;
-import com.github.tomakehurst.wiremock.junit.WireMockRule;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
 /**
  * Test parts of {@link PluginHandler} class when the reported JOSM version is too old for the plugin.
  */
-public class PluginHandlerJOSMTooOldTest {
+@AssumeRevision("Revision: 6000\n")
+@FullPreferences
+class PluginHandlerJOSMTooOldTest {
     /**
      * Setup test.
      */
-    @Rule
+    @RegisterExtension
     @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules().preferences().main().assumeRevision(
-        "Revision: 6000\n"
-    );
+    static JOSMTestRules test = new JOSMTestRules().main();
 
     /**
      * Plugin server mock.
      */
-    @Rule
-    public WireMockRule pluginServerRule = new WireMockRule(
+    @RegisterExtension
+    static WireMockExtension pluginServerRule = WireMockExtension.newInstance().options(
         options().dynamicPort().usingFilesUnderDirectory(TestUtils.getTestDataRoot())
-    );
+    ).build();
 
     /**
      * Setup test.
      */
-    @Before
+    @BeforeEach
     public void setUp() {
         Config.getPref().putInt("pluginmanager.version", 999);
         Config.getPref().put("pluginmanager.lastupdate", "999");
         Config.getPref().putList("pluginmanager.sites",
-                Collections.singletonList(this.pluginServerRule.url("/plugins"))
+                Collections.singletonList(pluginServerRule.url("/plugins"))
         );
 
@@ -115,5 +116,5 @@
      */
     @Test
-    public void testUpdatePluginsDownloadBoth() throws IOException {
+    void testUpdatePluginsDownloadBoth(WireMockRuntimeInfo wireMockRuntimeInfo) throws IOException {
         TestUtils.assumeWorkingJMockit();
         final PluginServer pluginServer = new PluginServer(
@@ -121,5 +122,5 @@
             new PluginServer.RemotePlugin(this.referenceBazJarNew)
         );
-        pluginServer.applyToWireMockServer(this.pluginServerRule);
+        pluginServer.applyToWireMockServer(wireMockRuntimeInfo);
         Config.getPref().putList("plugins", Arrays.asList("dummy_plugin", "baz_plugin"));
 
@@ -162,7 +163,7 @@
         TestUtils.assertFileContentsEqual(this.referenceBazJarNew, this.targetBazJar);
 
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/dummy_plugin.v31772.jar")));
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/baz_plugin.v7.jar")));
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/dummy_plugin.v31772.jar")));
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/baz_plugin.v7.jar")));
 
         assertEquals(Config.getPref().getInt("pluginmanager.version", 111), 6000);
@@ -177,5 +178,5 @@
      */
     @Test
-    public void testUpdatePluginsSkipOne() throws IOException {
+    void testUpdatePluginsSkipOne(WireMockRuntimeInfo wireMockRuntimeInfo) throws IOException {
         TestUtils.assumeWorkingJMockit();
         final PluginServer pluginServer = new PluginServer(
@@ -183,5 +184,5 @@
             new PluginServer.RemotePlugin(this.referenceBazJarNew)
         );
-        pluginServer.applyToWireMockServer(this.pluginServerRule);
+        pluginServer.applyToWireMockServer(wireMockRuntimeInfo);
         Config.getPref().putList("plugins", Arrays.asList("dummy_plugin", "baz_plugin"));
 
@@ -232,7 +233,7 @@
         TestUtils.assertFileContentsEqual(this.referenceBazJarNew, this.targetBazJar);
 
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
-        this.pluginServerRule.verify(0, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/dummy_plugin.v31772.jar")));
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/baz_plugin.v7.jar")));
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
+        pluginServerRule.verify(0, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/dummy_plugin.v31772.jar")));
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/baz_plugin.v7.jar")));
 
         // shouldn't have been updated
@@ -249,5 +250,5 @@
      */
     @Test
-    public void testUpdatePluginsUnexpectedlyJOSMTooOld() throws IOException {
+    void testUpdatePluginsUnexpectedlyJOSMTooOld(WireMockRuntimeInfo wireMockRuntimeInfo) throws IOException {
         TestUtils.assumeWorkingJMockit();
         final PluginServer pluginServer = new PluginServer(
@@ -255,5 +256,5 @@
             new PluginServer.RemotePlugin(this.referenceBazJarNew, Collections.singletonMap("Plugin-Mainversion", "5500"))
         );
-        pluginServer.applyToWireMockServer(this.pluginServerRule);
+        pluginServer.applyToWireMockServer(wireMockRuntimeInfo);
         Config.getPref().putList("plugins", Collections.singletonList("baz_plugin"));
 
@@ -283,7 +284,7 @@
         TestUtils.assertFileContentsEqual(this.referenceBazJarNew, this.targetBazJar);
 
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
-        // questionably correct
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/baz_plugin.v7.jar")));
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
+        // questionably correct
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/baz_plugin.v7.jar")));
 
         // should have been updated
@@ -303,6 +304,6 @@
      */
     @Test
-    @JOSMTestRules.OverrideAssumeRevision("Revision: 7200\n")
-    public void testUpdatePluginsMultiVersionInsufficient() throws IOException {
+    @AssumeRevision("Revision: 7200\n")
+    void testUpdatePluginsMultiVersionInsufficient(WireMockRuntimeInfo wireMockRuntimeInfo) throws IOException {
         TestUtils.assumeWorkingJMockit();
 
@@ -310,8 +311,8 @@
             new PluginServer.RemotePlugin(this.referenceBazJarOld),
             new PluginServer.RemotePlugin(this.referenceQuxJarNewer, Collections.singletonMap(
-                "7499_Plugin-Url", "346;" + this.pluginServerRule.url("/dont/bother.jar")
+                "7499_Plugin-Url", "346;" + pluginServerRule.url("/dont/bother.jar")
             ))
         );
-        pluginServer.applyToWireMockServer(this.pluginServerRule);
+        pluginServer.applyToWireMockServer(wireMockRuntimeInfo);
         Config.getPref().putList("plugins", Arrays.asList("qux_plugin", "baz_plugin"));
 
@@ -343,8 +344,8 @@
         TestUtils.assertFileContentsEqual(this.referenceQuxJarNewer, this.targetQuxJar);
 
-        assertEquals(2, WireMock.getAllServeEvents().size());
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
-        // questionably correct
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/qux_plugin.v432.jar")));
+        assertEquals(2, pluginServerRule.getAllServeEvents().size());
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
+        // questionably correct
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugin/qux_plugin.v432.jar")));
 
         assertEquals(7200, Config.getPref().getInt("pluginmanager.version", 111));
Index: trunk/test/unit/org/openstreetmap/josm/plugins/PluginHandlerMultiVersionTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/plugins/PluginHandlerMultiVersionTest.java	(revision 18693)
+++ trunk/test/unit/org/openstreetmap/josm/plugins/PluginHandlerMultiVersionTest.java	(revision 18694)
@@ -17,7 +17,11 @@
 import java.util.stream.Collectors;
 
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
 import org.openstreetmap.josm.TestUtils;
 import org.openstreetmap.josm.data.Preferences;
@@ -26,39 +30,36 @@
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 import org.openstreetmap.josm.testutils.PluginServer;
+import org.openstreetmap.josm.testutils.annotations.AssumeRevision;
+import org.openstreetmap.josm.testutils.annotations.FullPreferences;
 import org.openstreetmap.josm.testutils.mockers.ExtendedDialogMocker;
-
-import com.github.tomakehurst.wiremock.client.WireMock;
-import com.github.tomakehurst.wiremock.junit.WireMockRule;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
 /**
  * Test parts of {@link PluginHandler} class with plugins that advertise multiple versions for compatibility.
  */
-public class PluginHandlerMultiVersionTest {
+@FullPreferences
+class PluginHandlerMultiVersionTest {
     /**
      * Setup test.
      */
-    @Rule
+    @RegisterExtension
     @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules().preferences().main();
+    static JOSMTestRules test = new JOSMTestRules().main();
 
     /**
      * Plugin server mock.
      */
-    @Rule
-    public WireMockRule pluginServerRule = new WireMockRule(
-        options().dynamicPort().usingFilesUnderDirectory(TestUtils.getTestDataRoot())
-    );
+    @RegisterExtension
+    static WireMockExtension pluginServerRule = WireMockExtension.newInstance()
+            .options(options().dynamicPort().usingFilesUnderDirectory(TestUtils.getTestDataRoot())).build();
 
     /**
      * Setup test.
      */
-    @Before
-    public void setUp() {
+    @BeforeEach
+    void setUp() {
         Config.getPref().putInt("pluginmanager.version", 999);
         Config.getPref().put("pluginmanager.lastupdate", "999");
         Config.getPref().putList("pluginmanager.sites",
-                Collections.singletonList(this.pluginServerRule.url("/plugins"))
+                Collections.singletonList(pluginServerRule.url("/plugins"))
         );
 
@@ -94,7 +95,7 @@
      * @throws Exception on failure
      */
-    @JOSMTestRules.OverrideAssumeRevision("Revision: 7501\n")
+    @AssumeRevision("Revision: 7501\n")
     @Test
-    public void testUpdatePluginsOneMultiVersion() throws Exception {
+    void testUpdatePluginsOneMultiVersion(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception {
         TestUtils.assumeWorkingJMockit();
 
@@ -109,7 +110,7 @@
             new PluginServer.RemotePlugin(this.referenceQuxJarNewest, attrOverrides)
         );
-        pluginServer.applyToWireMockServer(this.pluginServerRule);
+        pluginServer.applyToWireMockServer(wireMockRuntimeInfo);
         // need to actually serve this older jar from somewhere
-        this.pluginServerRule.stubFor(
+        wireMockRuntimeInfo.getWireMock().register(
             WireMock.get(WireMock.urlEqualTo(quxNewerServePath)).willReturn(
                 WireMock.aResponse().withStatus(200).withHeader("Content-Type", "application/java-archive").withBodyFile(
@@ -147,7 +148,7 @@
         TestUtils.assertFileContentsEqual(this.referenceQuxJarNewer, this.targetQuxJar);
 
-        assertEquals(2, WireMock.getAllServeEvents().size());
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo(quxNewerServePath)));
+        assertEquals(2, pluginServerRule.getAllServeEvents().size());
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo(quxNewerServePath)));
 
         assertEquals(7501, Config.getPref().getInt("pluginmanager.version", 111));
@@ -161,7 +162,7 @@
      * @throws Exception on failure
      */
-    @JOSMTestRules.OverrideAssumeRevision("Revision: 7000\n")
+    @AssumeRevision("Revision: 7000\n")
     @Test
-    public void testUpdatePluginsExistingVersionLatestPossible() throws Exception {
+    void testUpdatePluginsExistingVersionLatestPossible(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception {
         TestUtils.assumeWorkingJMockit();
 
@@ -175,5 +176,5 @@
             new PluginServer.RemotePlugin(this.referenceQuxJarNewest, attrOverrides)
         );
-        pluginServer.applyToWireMockServer(this.pluginServerRule);
+        pluginServer.applyToWireMockServer(wireMockRuntimeInfo);
         Config.getPref().putList("plugins", Arrays.asList("qux_plugin", "baz_plugin"));
 
@@ -207,6 +208,6 @@
 
         // only the plugins list should have been downloaded
-        assertEquals(1, WireMock.getAllServeEvents().size());
-        this.pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
+        assertEquals(1, pluginServerRule.getAllServeEvents().size());
+        pluginServerRule.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/plugins")));
 
         assertEquals(7000, Config.getPref().getInt("pluginmanager.version", 111));
Index: trunk/test/unit/org/openstreetmap/josm/testutils/PluginServer.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/testutils/PluginServer.java	(revision 18693)
+++ trunk/test/unit/org/openstreetmap/josm/testutils/PluginServer.java	(revision 18694)
@@ -16,15 +16,12 @@
 import java.util.stream.StreamSupport;
 
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-import org.openstreetmap.josm.TestUtils;
-import org.openstreetmap.josm.tools.Logging;
-
-import com.github.tomakehurst.wiremock.WireMockServer;
 import com.github.tomakehurst.wiremock.client.MappingBuilder;
 import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder;
 import com.github.tomakehurst.wiremock.client.WireMock;
 import com.github.tomakehurst.wiremock.core.Options;
-import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import org.openstreetmap.josm.TestUtils;
+import org.openstreetmap.josm.tools.Logging;
 
 public class PluginServer {
@@ -138,9 +135,9 @@
         }
 
-        public String getPluginURL(WireMockServer wireMockServer) {
+        public String getPluginURL(WireMockRuntimeInfo wireMock) {
             if (this.pluginURL != null) {
                 return this.pluginURL;
-            } else if (wireMockServer != null && this.getJarPathBeneathFilesDir() != null) {
-                return wireMockServer.url(this.getPluginURLPath());
+            } else if (wireMock != null && this.getJarPathBeneathFilesDir() != null) {
+                return wireMock.getHttpBaseUrl() + this.getPluginURLPath();
             }
             return "http://example.com" + this.getPluginURLPath();
@@ -156,9 +153,9 @@
         }
 
-        public String getRemotePluginsListSection(WireMockServer wireMockServer) {
+        public String getRemotePluginsListSection(WireMockRuntimeInfo wireMock) {
             return String.format(
                 "%s.jar;%s\n%s",
                 this.getName(),
-                this.getPluginURL(wireMockServer),
+                this.getPluginURL(wireMock),
                 this.getRemotePluginsListManifestSection()
             );
@@ -192,11 +189,12 @@
     }
 
-    public void applyToWireMockServer(WireMockServer wireMockServer) {
+    public void applyToWireMockServer(WireMockRuntimeInfo wireMock) {
+        final WireMock wireMockServer = wireMock.getWireMock();
         // first add the plugins list
-        wireMockServer.stubFor(
+        wireMockServer.register(
             WireMock.get(WireMock.urlEqualTo("/plugins")).willReturn(
                 WireMock.aResponse().withStatus(200).withHeader("Content-Type", "text/plain").withBody(
                     this.pluginList.stream().map(
-                        remotePlugin -> remotePlugin.getRemotePluginsListSection(wireMockServer)
+                        remotePlugin -> remotePlugin.getRemotePluginsListSection(wireMock)
                     ).collect(Collectors.joining())
                 )
@@ -210,5 +208,5 @@
 
             if (mappingBuilder != null && responseDefinitionBuilder != null) {
-                wireMockServer.stubFor(
+                wireMockServer.register(
                     remotePlugin.getMappingBuilder().willReturn(remotePlugin.getResponseDefinitionBuilder())
                 );
@@ -228,7 +226,7 @@
     }
 
-    public class PluginServerRule extends WireMockRule {
+    public class PluginServerRule extends WireMockExtension {
         public PluginServerRule(Options ruleOptions, boolean failOnUnmatchedRequests) {
-            super(ruleOptions, failOnUnmatchedRequests);
+            super(extensionOptions().options(ruleOptions).failOnUnmatchedRequests(failOnUnmatchedRequests));
         }
 
@@ -238,12 +236,6 @@
 
         @Override
-        public Statement apply(Statement base, Description description) {
-            return super.apply(new Statement() {
-                @Override
-                public void evaluate() throws Throwable {
-                    PluginServer.this.applyToWireMockServer(PluginServerRule.this);
-                    base.evaluate();
-                }
-            }, description);
+        protected void onBeforeEach(WireMockRuntimeInfo wireMockRuntimeInfo) {
+            PluginServer.this.applyToWireMockServer(wireMockRuntimeInfo);
         }
     }
Index: trunk/test/unit/org/openstreetmap/josm/testutils/annotations/AssertionsInEDT.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/testutils/annotations/AssertionsInEDT.java	(revision 18694)
+++ trunk/test/unit/org/openstreetmap/josm/testutils/annotations/AssertionsInEDT.java	(revision 18694)
@@ -0,0 +1,42 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.openstreetmap.josm.testutils.mockers.EDTAssertionMocker;
+
+/**
+ * Ensure that assertions in the edt are caught.
+ * The default mocker is {@link EDTAssertionMocker}. If you want to use a different one,
+ * you must use {@link org.junit.jupiter.api.extension.RegisterExtension} with
+ * {@link AssertionsExtension#setMocker(Runnable)}.
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@ExtendWith(AssertionsInEDT.AssertionsExtension.class)
+public @interface AssertionsInEDT {
+    class AssertionsExtension implements BeforeEachCallback {
+        private Runnable edtAssertionMockingRunnable = EDTAssertionMocker::new;
+        @Override
+        public void beforeEach(ExtensionContext context) throws Exception {
+            this.edtAssertionMockingRunnable.run();
+        }
+
+        /**
+         * Re-raise AssertionErrors thrown in the EDT where they would have normally been swallowed.
+         * @param edtAssertionMockingRunnable Runnable for initializing this functionality
+         *
+         * @return this instance, for easy chaining
+         */
+        public AssertionsExtension setMocker(Runnable edtAssertionMockingRunnable) {
+            this.edtAssertionMockingRunnable = edtAssertionMockingRunnable;
+            return this;
+        }
+    }
+}
Index: trunk/test/unit/org/openstreetmap/josm/testutils/annotations/AssumeRevision.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/testutils/annotations/AssumeRevision.java	(revision 18694)
+++ trunk/test/unit/org/openstreetmap/josm/testutils/annotations/AssumeRevision.java	(revision 18694)
@@ -0,0 +1,65 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils.annotations;
+
+import java.io.ByteArrayInputStream;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.openstreetmap.josm.TestUtils;
+import org.openstreetmap.josm.data.Version;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+/**
+ * Override this test's assumed JOSM version (as reported by {@link Version}).
+ * @see JOSMTestRules#assumeRevision(String)
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+@ExtendWith(AssumeRevision.SetRevisionExtension.class)
+public @interface AssumeRevision {
+    /**
+     * Returns overridden assumed JOSM version.
+     * @return overridden assumed JOSM version
+     */
+    String value();
+
+    /**
+     * The extension that sets up and restores version information
+     */
+    class SetRevisionExtension implements BeforeEachCallback, AfterEachCallback {
+
+        @Override
+        public void afterEach(ExtensionContext context) throws Exception {
+            Version lastVersion = context.getStore(ExtensionContext.Namespace.create(SetRevisionExtension.class))
+                    .get(Version.class, Version.class);
+            TestUtils.setPrivateStaticField(Version.class, "instance", lastVersion);
+        }
+
+        @Override
+        public void beforeEach(ExtensionContext context) throws Exception {
+            context.getStore(ExtensionContext.Namespace.create(SetRevisionExtension.class))
+                    .put(Version.class, Version.getInstance());
+            final String revisionString = AnnotationUtils.findFirstParentAnnotation(context, AssumeRevision.class)
+                    .map(AssumeRevision::value).orElseThrow(NullPointerException::new);
+            final Version replacementVersion = new MockVersion(revisionString);
+            TestUtils.setPrivateStaticField(Version.class, "instance", replacementVersion);
+        }
+
+        private static class MockVersion extends Version {
+            MockVersion(final String propertiesString) {
+                super.initFromRevisionInfo(
+                        new ByteArrayInputStream(propertiesString.getBytes(StandardCharsets.UTF_8))
+                );
+            }
+        }
+    }
+}
Index: trunk/test/unit/org/openstreetmap/josm/testutils/annotations/BasicPreferences.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/testutils/annotations/BasicPreferences.java	(revision 18693)
+++ trunk/test/unit/org/openstreetmap/josm/testutils/annotations/BasicPreferences.java	(revision 18694)
@@ -4,7 +4,9 @@
 import java.lang.annotation.Documented;
 import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
+import java.util.Map;
 
 import org.junit.jupiter.api.extension.AfterAllCallback;
@@ -15,8 +17,11 @@
 import org.junit.jupiter.api.extension.ExtensionContext;
 import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
+import org.junit.platform.commons.support.AnnotationSupport;
+import org.openstreetmap.josm.TestUtils;
 import org.openstreetmap.josm.data.Preferences;
 import org.openstreetmap.josm.data.preferences.JosmBaseDirectories;
 import org.openstreetmap.josm.data.preferences.JosmUrls;
 import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.spi.preferences.Setting;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 
@@ -32,4 +37,5 @@
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.TYPE, ElementType.METHOD})
+@Inherited
 @ExtendWith(BasicPreferences.BasicPreferencesExtension.class)
 public @interface BasicPreferences {
@@ -47,5 +53,5 @@
         @Override
         public void afterEach(ExtensionContext context) throws Exception {
-            if (context.getElement().isPresent() && context.getElement().get().isAnnotationPresent(BasicPreferences.class)) {
+            if (AnnotationSupport.isAnnotated(context.getElement(), BasicPreferences.class)) {
                 this.afterAll(context);
             }
@@ -57,5 +63,9 @@
             // Disable saving on put, just to avoid overwriting pref files
             pref.enableSaveOnPut(false);
-            pref.resetToDefault();
+            pref.resetToInitialState();
+            pref.enableSaveOnPut(false);
+            @SuppressWarnings("unchecked")
+            final Map<String, Setting<?>> defaultsMap = (Map<String, Setting<?>>) TestUtils.getPrivateField(pref, "defaultsMap");
+            defaultsMap.clear();
             Config.setPreferencesInstance(pref);
             Config.setBaseDirectoriesProvider(JosmBaseDirectories.getInstance());
@@ -70,5 +80,5 @@
         @Override
         public void beforeEach(ExtensionContext context) throws Exception {
-            if (AnnotationUtils.elementIsAnnotated(context.getElement(), BasicPreferences.class) || Config.getPref() == null) {
+            if (AnnotationSupport.isAnnotated(context.getElement(), BasicPreferences.class) || Config.getPref() == null) {
                 this.beforeAll(context);
             }
Index: trunk/test/unit/org/openstreetmap/josm/testutils/annotations/FullPreferences.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/testutils/annotations/FullPreferences.java	(revision 18693)
+++ trunk/test/unit/org/openstreetmap/josm/testutils/annotations/FullPreferences.java	(revision 18694)
@@ -7,15 +7,12 @@
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
-import java.util.Map;
 
-import org.junit.jupiter.api.extension.BeforeAllCallback;
 import org.junit.jupiter.api.extension.BeforeEachCallback;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.extension.ExtensionContext;
 import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
-import org.openstreetmap.josm.TestUtils;
 import org.openstreetmap.josm.data.Preferences;
+import org.openstreetmap.josm.data.preferences.JosmBaseDirectories;
 import org.openstreetmap.josm.spi.preferences.Config;
-import org.openstreetmap.josm.spi.preferences.Setting;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 import org.openstreetmap.josm.testutils.annotations.BasicPreferences.BasicPreferencesExtension;
@@ -37,11 +34,12 @@
      * Initialize preferences.
      */
-    class UsePreferencesExtension implements BeforeAllCallback, BeforeEachCallback {
+    class UsePreferencesExtension implements BeforeEachCallback {
         @Override
-        public void beforeAll(ExtensionContext context) throws Exception {
+        public void beforeEach(ExtensionContext context) throws Exception {
             Preferences pref = context.getStore(Namespace.create(BasicPreferencesExtension.class)).get("preferences", Preferences.class);
-            @SuppressWarnings("unchecked")
-            final Map<String, Setting<?>> defaultsMap = (Map<String, Setting<?>>) TestUtils.getPrivateField(pref, "defaultsMap");
-            defaultsMap.clear();
+            if (pref.getDirs() instanceof JosmBaseDirectories) {
+                ((JosmBaseDirectories) pref.getDirs()).clearMemos();
+            }
+            pref.enableSaveOnPut(false);
             pref.resetToInitialState();
             pref.enableSaveOnPut(false);
@@ -50,11 +48,4 @@
             Config.getPref().put("osm-server.url", "http://invalid");
         }
-
-        @Override
-        public void beforeEach(ExtensionContext context) throws Exception {
-            if (AnnotationUtils.elementIsAnnotated(context.getElement(), FullPreferences.class)) {
-                this.beforeAll(context);
-            }
-        }
     }
 }
Index: trunk/test/unit/org/openstreetmap/josm/testutils/annotations/I18n.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/testutils/annotations/I18n.java	(revision 18693)
+++ trunk/test/unit/org/openstreetmap/josm/testutils/annotations/I18n.java	(revision 18694)
@@ -7,4 +7,5 @@
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
+import java.util.Locale;
 
 import org.junit.jupiter.api.extension.AfterEachCallback;
@@ -13,4 +14,5 @@
 import org.junit.jupiter.api.extension.ExtensionContext;
 import org.junit.platform.commons.support.AnnotationSupport;
+import org.openstreetmap.josm.tools.LanguageInfo;
 
 /**
@@ -41,11 +43,16 @@
         public void beforeEach(ExtensionContext context) {
             String language = AnnotationSupport.findAnnotation(context.getElement(), I18n.class).map(I18n::value).orElse("en");
-            org.openstreetmap.josm.tools.I18n.set(language);
+            if (!Locale.getDefault().equals(LanguageInfo.getLocale(language, false))) {
+                org.openstreetmap.josm.tools.I18n.set(language);
+            }
         }
 
         @Override
         public void afterEach(ExtensionContext context) {
-            org.openstreetmap.josm.tools.I18n.set("en");
-            org.openstreetmap.josm.tools.I18n.set(org.openstreetmap.josm.tools.I18n.getOriginalLocale().getLanguage());
+            if (!Locale.ENGLISH.equals(Locale.getDefault())) {
+                org.openstreetmap.josm.tools.I18n.set("en");
+                org.openstreetmap.josm.tools.I18n.set(org.openstreetmap.josm.tools.I18n.getOriginalLocale().getLanguage());
+                Locale.setDefault(Locale.ENGLISH);
+            }
         }
     }
Index: trunk/test/unit/org/openstreetmap/josm/testutils/annotations/JosmDefaults.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/testutils/annotations/JosmDefaults.java	(revision 18694)
+++ trunk/test/unit/org/openstreetmap/josm/testutils/annotations/JosmDefaults.java	(revision 18694)
@@ -0,0 +1,88 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils.annotations;
+
+import java.awt.Window;
+import java.awt.event.WindowEvent;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Arrays;
+
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.openstreetmap.josm.actions.DeleteAction;
+import org.openstreetmap.josm.command.DeleteCommand;
+import org.openstreetmap.josm.data.Preferences;
+import org.openstreetmap.josm.data.UserIdentityManager;
+import org.openstreetmap.josm.data.osm.User;
+import org.openstreetmap.josm.gui.oauth.OAuthAuthorizationWizard;
+import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.io.OsmConnection;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.MemoryManagerTest;
+
+/**
+ * Default actions taken by {@link JOSMTestRules}. Automatically registered.
+ * Functionality that this provides may be moved to other classes.
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@ExtendWith(JosmDefaults.DefaultsExtension.class)
+public @interface JosmDefaults {
+    /**
+     * Default actions taken by {@link JOSMTestRules}. Automatically registered.
+     * Functionality that this provides may be moved to other classes.
+     */
+    class DefaultsExtension implements BeforeEachCallback, AfterEachCallback {
+        @Override
+        public void beforeEach(ExtensionContext context) throws Exception {
+            cleanUpFromJosmFixture();
+            // Assume anonymous user
+            if (!UserIdentityManager.getInstance().isAnonymous()) {
+                UserIdentityManager.getInstance().setAnonymous();
+            }
+            User.clearUserMap();
+            // Setup callbacks
+            DeleteCommand.setDeletionCallback(DeleteAction.defaultDeletionCallback);
+            OsmConnection.setOAuthAccessTokenFetcher(OAuthAuthorizationWizard::obtainAccessToken);
+        }
+
+        @Override
+        public void afterEach(ExtensionContext context) throws Exception {
+            MemoryManagerTest.resetState(false);
+
+            Window[] windows = Window.getWindows();
+            if (windows.length != 0) {
+                Logging.info(
+                        "Attempting to close {0} windows left open by tests: {1}",
+                        windows.length,
+                        Arrays.toString(windows)
+                );
+            }
+            GuiHelper.runInEDTAndWait(() -> {
+                for (Window window : windows) {
+                    window.dispatchEvent(new WindowEvent(window, WindowEvent.WINDOW_CLOSING));
+                    window.dispose();
+                }
+            });
+
+            // Parts of JOSM uses weak references - destroy them.
+            System.gc();
+        }
+
+        /**
+         * Clean up what test not using these test rules may have broken.
+         */
+        private void cleanUpFromJosmFixture() {
+            MemoryManagerTest.resetState(true);
+            JOSMTestRules.cleanLayerEnvironment();
+            Preferences.main().resetToInitialState();
+            Preferences.main().enableSaveOnPut(false);
+            System.gc();
+        }
+    }
+}
Index: trunk/test/unit/org/openstreetmap/josm/testutils/annotations/JosmHome.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/testutils/annotations/JosmHome.java	(revision 18693)
+++ trunk/test/unit/org/openstreetmap/josm/testutils/annotations/JosmHome.java	(revision 18694)
@@ -16,6 +16,6 @@
 import java.util.UUID;
 
-import org.junit.jupiter.api.extension.AfterAllCallback;
-import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.junit.jupiter.api.extension.ExtensionContext;
@@ -40,7 +40,7 @@
      * @author Taylor Smock
      */
-    class JosmHomeExtension implements BeforeAllCallback, AfterAllCallback {
+    class JosmHomeExtension implements BeforeEachCallback, AfterEachCallback {
         @Override
-        public void afterAll(ExtensionContext context) throws Exception {
+        public void afterEach(ExtensionContext context) throws Exception {
             Path tempDir = context.getStore(Namespace.create(JosmHome.class)).get("home", Path.class);
             Files.walkFileTree(tempDir, new SimpleFileVisitor<Path>() {
@@ -57,8 +57,9 @@
                 }
             });
+            System.clearProperty("josm.home");
         }
 
         @Override
-        public void beforeAll(ExtensionContext context) throws Exception {
+        public void beforeEach(ExtensionContext context) throws Exception {
             Path tempDir = Files.createTempDirectory(UUID.randomUUID().toString());
             context.getStore(Namespace.create(JosmHome.class)).put("home", tempDir);
Index: trunk/test/unit/org/openstreetmap/josm/testutils/annotations/Logging.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/testutils/annotations/Logging.java	(revision 18694)
+++ trunk/test/unit/org/openstreetmap/josm/testutils/annotations/Logging.java	(revision 18694)
@@ -0,0 +1,31 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+import java.util.logging.Handler;
+
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+/**
+ * Reset logging for each test
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface Logging {
+    class LoggingExtension implements BeforeEachCallback {
+
+        @Override
+        public void beforeEach(ExtensionContext context) throws Exception {
+            // Force log handlers to reacquire reference to (junit's fake) stdout/stderr
+            for (Handler handler : org.openstreetmap.josm.tools.Logging.getLogger().getHandlers()) {
+                if (handler instanceof org.openstreetmap.josm.tools.Logging.ReacquiringConsoleHandler) {
+                    handler.flush();
+                    ((org.openstreetmap.josm.tools.Logging.ReacquiringConsoleHandler) handler).reacquireOutputStream();
+                }
+            }
+            // Set log level to info
+            org.openstreetmap.josm.tools.Logging.setLogLevel(org.openstreetmap.josm.tools.Logging.LEVEL_INFO);
+        }
+    }
+}
Index: trunk/test/unit/org/openstreetmap/josm/testutils/annotations/Main.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/testutils/annotations/Main.java	(revision 18694)
+++ trunk/test/unit/org/openstreetmap/josm/testutils/annotations/Main.java	(revision 18694)
@@ -0,0 +1,81 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.openstreetmap.josm.JOSMFixture;
+import org.openstreetmap.josm.TestUtils;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.testutils.mockers.WindowlessMapViewStateMocker;
+import org.openstreetmap.josm.testutils.mockers.WindowlessNavigatableComponentMocker;
+
+/**
+ * The annotation for mocking map view and navigable components
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@HTTP
+@LayerManager
+@ExtendWith(Main.MainExtension.class)
+public @interface Main {
+    /**
+     * If a specific mocker is required, use {@link org.junit.jupiter.api.extension.RegisterExtension}.
+     */
+    class MainExtension implements BeforeEachCallback {
+        /**
+         * The mocker for the map view state
+         */
+        private Runnable mapViewStateMockingRunnable = WindowlessMapViewStateMocker::new;
+        /**
+         * The mocker for navigable components
+         */
+        private Runnable navigableComponentMockingRunnable = WindowlessNavigatableComponentMocker::new;
+
+        /**
+         * Set the specific map view mocker
+         *
+         * @param mapViewStateMockingRunnable The new mocker
+         * @return this, for easy chaining
+         */
+        public MainExtension setMapViewMocker(Runnable mapViewStateMockingRunnable) {
+            this.mapViewStateMockingRunnable = mapViewStateMockingRunnable;
+            return this;
+        }
+
+        /**
+         * Set the navigable component mocker
+         *
+         * @param navigableComponentMockingRunnable The new mocker
+         * @return this, for easy chaining
+         */
+        public MainExtension setNavigableComponentMocker(Runnable navigableComponentMockingRunnable) {
+            this.navigableComponentMockingRunnable = navigableComponentMockingRunnable;
+            return this;
+        }
+
+        @Override
+        public void beforeEach(ExtensionContext context) {
+            TestUtils.assumeWorkingJMockit();
+            // apply mockers to MapViewState and NavigableComponent whether we're headless or not
+            // as we generally don't create the josm main window even in non-headless mode.
+            if (this.mapViewStateMockingRunnable != null) {
+                this.mapViewStateMockingRunnable.run();
+            }
+            if (this.navigableComponentMockingRunnable != null) {
+                this.navigableComponentMockingRunnable.run();
+            }
+
+            new MainApplication();
+            JOSMFixture.initContentPane();
+            JOSMFixture.initMainPanel(true);
+            JOSMFixture.initToolbar();
+            JOSMFixture.initMainMenu();
+        }
+    }
+}
Index: trunk/test/unit/org/openstreetmap/josm/testutils/annotations/ThreadSync.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/testutils/annotations/ThreadSync.java	(revision 18694)
+++ trunk/test/unit/org/openstreetmap/josm/testutils/annotations/ThreadSync.java	(revision 18694)
@@ -0,0 +1,112 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils.annotations;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.management.ManagementFactory;
+import java.text.MessageFormat;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+import org.awaitility.Awaitility;
+import org.awaitility.Durations;
+import org.awaitility.core.ConditionTimeoutException;
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * Ensure threads are synced between tests. If you need to sync specific threads, use {@link ThreadSyncExtension}.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+@ExtendWith(ThreadSync.ThreadSyncExtension.class)
+public @interface ThreadSync {
+    /**
+     * The extension that actually performs thread syncs.
+     */
+    class ThreadSyncExtension implements AfterEachCallback {
+        private final Duration defaultDuration = Durations.TEN_MINUTES;
+        private final List<Consumer<Runnable>> threadExecutors = new ArrayList<>(Arrays.asList(GuiHelper::runInEDTAndWait,
+                MainApplication.worker::execute));
+        private final List<ForkJoinPool> forkJoinPools = new ArrayList<>(Collections.singletonList(ForkJoinPool.commonPool()));
+
+        @Override
+        public void afterEach(ExtensionContext context) throws Exception {
+            final List<String> deadlockedMessages = threadSync();
+            if (!deadlockedMessages.isEmpty()) {
+                fail(String.join(System.lineSeparator(), deadlockedMessages));
+            }
+        }
+
+        /**
+         * Register a thread executor (single threaded)
+         * @param executor The executor to register
+         */
+        public void registerThreadExecutor(Consumer<Runnable> executor) {
+            this.threadExecutors.add(executor);
+        }
+
+        /**
+         * Register a ForkJoin pool that should be quiescent on finish
+         * @param forkJoinPool The pool
+         */
+        public void registerForkJoinPool(ForkJoinPool forkJoinPool) {
+            this.forkJoinPools.add(forkJoinPool);
+        }
+
+        /**
+         * Force a thread sync of all registered threads
+         * @return A list of potentially deadlocked threads
+         */
+        public List<String> threadSync() {
+            final List<String> deadlockedMessages = new ArrayList<>();
+            for (Consumer<Runnable> workerThread : this.threadExecutors) {
+                // Sync the thread
+                final AtomicBoolean queueEmpty = new AtomicBoolean();
+                workerThread.accept(() -> queueEmpty.set(true));
+                try {
+                    Awaitility.await().atMost(defaultDuration).untilTrue(queueEmpty);
+                } catch (ConditionTimeoutException timeoutException) {
+                    Logging.trace(timeoutException);
+                    final long[] deadlocked = ManagementFactory.getThreadMXBean().findDeadlockedThreads();
+                    Arrays.sort(deadlocked);
+                    for (Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet()) {
+                        if (Arrays.binarySearch(deadlocked, entry.getKey().getId()) >= 0 || entry.getKey().getName().contains("main-worker")) {
+                            final StringBuilder builder = new StringBuilder();
+                            for (StackTraceElement element : entry.getValue()) {
+                                builder.append('\t').append(element);
+                            }
+                            deadlockedMessages.add(MessageFormat.format("Thread {0}-{1} may be deadlocked:\n{2}",
+                                    entry.getKey().getId(), entry.getKey().getName(), builder.toString()));
+                            entry.getKey().interrupt();
+                        }
+                    }
+                }
+            }
+            for (ForkJoinPool pool : this.forkJoinPools) {
+                assertTrue(pool.awaitQuiescence(defaultDuration.toMillis(), TimeUnit.MILLISECONDS));
+            }
+            this.forkJoinPools.removeIf(ForkJoinPool::isShutdown);
+            return deadlockedMessages;
+        }
+    }
+}
Index: trunk/test/unit/org/openstreetmap/josm/testutils/annotations/Timezone.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/testutils/annotations/Timezone.java	(revision 18694)
+++ trunk/test/unit/org/openstreetmap/josm/testutils/annotations/Timezone.java	(revision 18694)
@@ -0,0 +1,30 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.TimeZone;
+
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.openstreetmap.josm.tools.date.DateUtils;
+
+/**
+ * Set the timezone for a test
+ */
+
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@ExtendWith(Timezone.TimezoneExtension.class)
+public @interface Timezone {
+    class TimezoneExtension implements BeforeEachCallback {
+        @Override
+        public void beforeEach(ExtensionContext context) {
+            // All tests use the same timezone.
+            TimeZone.setDefault(DateUtils.UTC);
+        }
+    }
+}
