Changeset 19153 in josm for trunk/test/unit


Ignore:
Timestamp:
2024-07-25T22:54:48+02:00 (6 months ago)
Author:
taylor.smock
Message:

Fix #23821: Ensure that a new layer is loaded prior to loading additional data to that layer

This occurred due to a race condition, whereby the /load_and_zoom call would
return immediately prior to the download finishing for the new layer and the
next /load_and_zoom call merging onto a pre-existing layer.

This could be fixed in one of two different ways:

  1. Block the RemoteControl thread
  2. Have some method for ensuring that a new layer is loaded first

While we are effectively doing (1), it was easier to do (2) as well for testing
purposes. This means the RemoteControl thread could spin off a thread for each
request to /load_and_zoom and this particular issue should not reappear.

This does not control for cases where a user calls /load_and_zoom like so:

  1. new_layer=true + layer_name=first
  2. new_layer=true + layer_name=second
  3. new_layer=false + layer_name=first
  4. new_layer=false + layer_name=second

Both (1) and (2) will complete before (3) and (4) are run. However, both (3) and
(4) will be loaded into the last layer loaded.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/test/unit/org/openstreetmap/josm/io/remotecontrol/handler/LoadAndZoomHandlerTest.java

    r19152 r19153  
    55import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
    66import static org.junit.jupiter.api.Assertions.assertEquals;
     7import static org.junit.jupiter.api.Assertions.assertNotEquals;
    78import static org.junit.jupiter.api.Assertions.assertNotNull;
    89import static org.junit.jupiter.api.Assertions.assertNull;
     
    1314import java.nio.charset.StandardCharsets;
    1415import java.util.Collection;
     16import java.util.concurrent.ForkJoinPool;
     17import java.util.concurrent.ForkJoinTask;
     18import java.util.concurrent.atomic.AtomicBoolean;
    1519
    1620import com.github.tomakehurst.wiremock.client.WireMock;
    1721import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
    1822import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder;
     23import org.awaitility.Awaitility;
    1924import org.junit.jupiter.api.BeforeEach;
    2025import org.junit.jupiter.api.extension.ExtendWith;
     
    2429import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
    2530import org.openstreetmap.josm.gui.MainApplication;
     31import org.openstreetmap.josm.gui.layer.OsmDataLayer;
     32import org.openstreetmap.josm.gui.util.GuiHelper;
    2633import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler.RequestHandlerBadRequestException;
    2734import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
     
    4148class LoadAndZoomHandlerTest {
    4249    private static final String DEFAULT_BBOX_URL = "https://localhost/load_and_zoom?left=0&bottom=0&right=0.001&top=0.001";
     50    private static final String DEFAULT_BBOX_URL_2 = "https://localhost/load_and_zoom?left=0.00025&bottom=0.00025&right=0.00075&top=0.00125";
    4351    private static LoadAndZoomHandler newHandler(String url) throws RequestHandlerBadRequestException {
    4452        LoadAndZoomHandler req = new LoadAndZoomHandler();
     
    6573                        " <node id=\"3\" " + common + " lat=\"0.0002\" lon=\"0.0002\"/>\n" +
    6674                        "</osm>")));
     75        // The scientific notation is ok server-side.
     76        wireMockRuntimeInfo.getWireMock().register(WireMock.get("/api/0.6/map?bbox=2.5E-4,0.001,7.5E-4,0.00125")
     77                .willReturn(WireMock.aResponse().withBody("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
     78                        "<osm version=\"0.6\" generator=\"hand\" copyright=\"JOSM\" attribution=\"\" license=\"\">\n" +
     79                        " <bounds minlat=\"0.001\" minlon=\"0.00025\" maxlat=\"0.00125\" maxlon=\"0.00075\"/>\n" +
     80                        " <node id=\"4\" " + common + " lat=\"0.00111\" lon=\"0.00026\"/>\n" +
     81                        " <node id=\"5\" " + common + " lat=\"0.0011\" lon=\"0.00025\"/>\n" +
     82                        " <node id=\"6\" " + common + " lat=\"0.0012\" lon=\"0.000251\"/>\n" +
     83                        "</osm>")));
    6784    }
    6885
     
    141158        assertDoesNotThrow(handler::handle);
    142159        syncThreads();
    143         // The scientific notation is ok server-side.
    144         final String mapCall = "/api/0.6/map?bbox=2.5E-4,0.001,7.5E-4,0.00125";
    145         final String commonNode = "visible=\"true\" version=\"1\" changeset=\"1\" timestamp=\"2000-01-01T00:00:00Z\" user=\"tsmock\" uid=\"1\"";
    146         wireMockRuntimeInfo.getWireMock().register(WireMock.get(mapCall)
    147                 .willReturn(WireMock.aResponse().withBody("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
    148                         "<osm version=\"0.6\" generator=\"hand\" copyright=\"JOSM\" attribution=\"\" license=\"\">\n" +
    149                         " <bounds minlat=\"0.001\" minlon=\"0.00025\" maxlat=\"0.00125\" maxlon=\"0.00075\"/>\n" +
    150                         " <node id=\"4\" " + commonNode + " lat=\"0.00111\" lon=\"0.00026\"/>\n" +
    151                         " <node id=\"5\" " + commonNode + " lat=\"0.0011\" lon=\"0.00025\"/>\n" +
    152                         " <node id=\"6\" " + commonNode + " lat=\"0.0012\" lon=\"0.000251\"/>\n" +
    153                         "</osm>")));
    154         String request = "https://localhost/load_and_zoom?left=0.00025&bottom=0.00025&right=0.00075&top=0.00125";
    155         handler = newHandler(request);
    156         assertDoesNotThrow(handler::handle);
    157         syncThreads();
    158         wireMockRuntimeInfo.getWireMock().verifyThat(1, RequestPatternBuilder.newRequestPattern().withUrl(mapCall));
     160
     161        handler = newHandler(DEFAULT_BBOX_URL_2);
     162        assertDoesNotThrow(handler::handle);
     163        syncThreads();
     164        wireMockRuntimeInfo.getWireMock().verifyThat(1, RequestPatternBuilder.newRequestPattern()
     165                .withUrl("/api/0.6/map?bbox=2.5E-4,0.001,7.5E-4,0.00125"));
    159166        final DataSet ds = MainApplication.getLayerManager().getEditDataSet();
    160167        assertNotNull(ds);
     
    227234        assertEquals("here", ds.getChangeSetTags().get("is"));
    228235    }
     236
     237    /**
     238     * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/23821">#23821</a>
     239     * @throws RequestHandlerBadRequestException If there is an issue with the handler
     240     */
     241    @Test
     242    void testNonRegression23821() throws RequestHandlerBadRequestException {
     243        final AtomicBoolean block = new AtomicBoolean(false);
     244        final Runnable runnable = () -> {
     245            synchronized (block) {
     246                while (!block.get()) {
     247                    try {
     248                        block.wait();
     249                    } catch (InterruptedException e) {
     250                        Thread.currentThread().interrupt();;
     251                        throw new RuntimeException(e);
     252                    }
     253                }
     254            }
     255        };
     256        final DataSet wrongDataset = new DataSet();
     257        MainApplication.getLayerManager().addLayer(new OsmDataLayer(wrongDataset,
     258                "LoadAndZoomHandlerTest#testNonRegression23821", null));
     259        ForkJoinTask<?> task1;
     260        ForkJoinTask<?> task2;
     261        try {
     262            GuiHelper.runInEDT(runnable);
     263            MainApplication.worker.submit(runnable);
     264            // The processor makes a new handler for each request
     265            // It is single-threaded, so blocking on one handler would fix the problem with the other handler.
     266            // But we might as well work on multi-threading, since it is easier to test. :)
     267            final LoadAndZoomHandler handler1 = newHandler(DEFAULT_BBOX_URL + "&new_layer=true&layer_name=OSMData");
     268            final LoadAndZoomHandler handler2 = newHandler(DEFAULT_BBOX_URL_2 + "&new_layer=false&layer_name=OSMData");
     269            // Use a separate threads to avoid blocking on this thread
     270            final ForkJoinPool pool = ForkJoinPool.commonPool();
     271            task1 = pool.submit(() -> assertDoesNotThrow(handler1::handle));
     272
     273            // Make certain there is enough time for the first task to block
     274            Awaitility.await().until(() -> true);
     275            task2 = pool.submit(() -> assertDoesNotThrow(handler2::handle));
     276        } finally {
     277            // Unblock UI/worker threads
     278            synchronized (block) {
     279                block.set(true);
     280                block.notifyAll();
     281            }
     282        }
     283
     284        task1.join();
     285        task2.join();
     286
     287        syncThreads();
     288        assertEquals(2, MainApplication.getLayerManager().getLayers().size());
     289        final DataSet ds = MainApplication.getLayerManager().getEditDataSet();
     290        assertNotEquals(wrongDataset, ds);
     291        assertTrue(wrongDataset.isEmpty());
     292        assertEquals(6, ds.allPrimitives().size());
     293    }
    229294}
Note: See TracChangeset for help on using the changeset viewer.