Ignore:
Timestamp:
2024-01-30T20:06:45+01:00 (4 months ago)
Author:
taylor.smock
Message:

See #23444: Fix an additional NPE found during testing ImproveWayAccuracyAction

The NPE could be reproduced by selecting a node with only an incomplete
parent way, and switching from select mode to improve way mode.

This additionally adds simple tests for ImproveWayAccuracyAction and refactors
some methods to have a lower complexity.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/test/unit/org/openstreetmap/josm/actions/mapmode/ImproveWayAccuracyActionTest.java

    r18963 r18965  
    22package org.openstreetmap.josm.actions.mapmode;
    33
     4import static org.junit.jupiter.api.Assertions.assertAll;
    45import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
    56import static org.junit.jupiter.api.Assertions.assertEquals;
    67import static org.junit.jupiter.api.Assertions.assertFalse;
     8import static org.junit.jupiter.api.Assertions.assertNotEquals;
    79import static org.junit.jupiter.api.Assertions.assertTrue;
    810
    911import java.awt.Graphics2D;
    1012import java.awt.Point;
     13import java.awt.Rectangle;
     14import java.awt.event.InputEvent;
    1115import java.awt.event.MouseEvent;
    1216import java.awt.image.BufferedImage;
    13 
     17import java.util.Collection;
     18import java.util.concurrent.atomic.AtomicReference;
     19
     20import org.apache.commons.lang3.function.TriConsumer;
    1421import org.junit.jupiter.api.Test;
    15 import org.junit.jupiter.api.extension.RegisterExtension;
    1622import org.openstreetmap.josm.TestUtils;
    1723import org.openstreetmap.josm.actions.mapmode.ImproveWayAccuracyAction.State;
     24import org.openstreetmap.josm.command.DeleteCommand;
    1825import org.openstreetmap.josm.data.Bounds;
    1926import org.openstreetmap.josm.data.coor.ILatLon;
     
    2128import org.openstreetmap.josm.data.osm.DataSet;
    2229import org.openstreetmap.josm.data.osm.Node;
     30import org.openstreetmap.josm.data.osm.OsmPrimitive;
     31import org.openstreetmap.josm.data.osm.Relation;
     32import org.openstreetmap.josm.data.osm.RelationToChildReference;
    2333import org.openstreetmap.josm.data.osm.Way;
     34import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
    2435import org.openstreetmap.josm.gui.MainApplication;
    2536import org.openstreetmap.josm.gui.MapFrame;
    2637import org.openstreetmap.josm.gui.MapView;
     38import org.openstreetmap.josm.gui.layer.Layer;
    2739import org.openstreetmap.josm.gui.layer.OsmDataLayer;
    2840import org.openstreetmap.josm.gui.util.GuiHelper;
    2941import org.openstreetmap.josm.testutils.annotations.Main;
    3042import org.openstreetmap.josm.testutils.annotations.Projection;
    31 import org.openstreetmap.josm.testutils.mockers.WindowlessMapViewStateMocker;
    32 import org.openstreetmap.josm.testutils.mockers.WindowlessNavigatableComponentMocker;
    33 
    34 import mockit.Mock;
    3543
    3644/**
    3745 * Unit tests for class {@link ImproveWayAccuracyAction}.
    3846 */
     47@Main
    3948@Projection
    4049class ImproveWayAccuracyActionTest {
    41     @RegisterExtension
    42     Main.MainExtension mainExtension = new Main.MainExtension().setMapViewMocker(SizedWindowlessMapViewStateMocker::new)
    43             .setNavigableComponentMocker(SizedWindowlessNavigatableComponentMocker::new);
    44 
    4550    private static final int WIDTH = 800;
    4651    private static final int HEIGHT = 600;
    47     private static class SizedWindowlessMapViewStateMocker extends WindowlessMapViewStateMocker {
    48         @Mock
    49         public int getWidth() {
    50             return WIDTH;
    51         }
    52 
    53         @Mock
    54         public int getHeight() {
    55             return HEIGHT;
    56         }
    57     }
    58 
    59     private static class SizedWindowlessNavigatableComponentMocker extends WindowlessNavigatableComponentMocker {
    60         @Mock
    61         public int getWidth() {
    62             return WIDTH;
    63         }
    64 
    65         @Mock
    66         public int getHeight() {
    67             return HEIGHT;
    68         }
    69     }
    70 
    71     private static MouseEvent generateEvent(MapView mapView, ILatLon location) {
     52
     53    private static class AlwaysDeleteCallback implements DeleteCommand.DeletionCallback {
     54        @Override
     55        public boolean checkAndConfirmOutlyingDelete(Collection<? extends OsmPrimitive> primitives, Collection<? extends OsmPrimitive> ignore) {
     56            return true;
     57        }
     58
     59        @Override
     60        public boolean confirmRelationDeletion(Collection<Relation> relations) {
     61            return true;
     62        }
     63
     64        @Override
     65        public boolean confirmDeletionFromRelation(Collection<RelationToChildReference> references) {
     66            return true;
     67        }
     68    }
     69
     70    private static void setupMapView(DataSet ds) {
     71        // setup a reasonable size for the edit window
     72        MainApplication.getMap().mapView.setBounds(new Rectangle(WIDTH, HEIGHT));
     73        if (ds.getDataSourceBoundingBox() != null) {
     74            MainApplication.getMap().mapView.zoomTo(ds.getDataSourceBoundingBox());
     75        } else {
     76            BoundingXYVisitor v = new BoundingXYVisitor();
     77            for (Layer l : MainApplication.getLayerManager().getLayers()) {
     78                l.visitBoundingBox(v);
     79            }
     80            MainApplication.getMap().mapView.zoomTo(v);
     81        }
     82    }
     83
     84    /**
     85     * Generate a mouse event
     86     * @param mapView The current map view
     87     * @param location The location to generate the event for
     88     * @param modifiers The modifiers for {@link MouseEvent} (see {@link InputEvent#getModifiersEx()})
     89     * @return The generated event
     90     */
     91    private static MouseEvent generateEvent(MapView mapView, ILatLon location, int modifiers) {
    7292        final Point p = mapView.getPoint(location);
    73         return new MouseEvent(mapView, 0, 0, 0, p.x, p.y, p.x, p.y, 1, false, MouseEvent.BUTTON1);
     93        return new MouseEvent(mapView, 0, 0, modifiers, p.x, p.y, p.x, p.y, 1, false, MouseEvent.BUTTON1);
    7494    }
    7595
     
    102122
    103123    @Test
     124    void testNonRegression23444Selection() {
     125        final DataSet dataSet = new DataSet();
     126        final OsmDataLayer layer = new OsmDataLayer(dataSet, "ImproveWayAccuracyActionTest#testNonRegression23444Selection", null);
     127        MainApplication.getLayerManager().addLayer(layer);
     128        final ImproveWayAccuracyAction mapMode = new ImproveWayAccuracyAction();
     129        final MapFrame map = MainApplication.getMap();
     130        final Way testWay = TestUtils.newWay("", new Node(1, 1), new Node(2, 1),
     131                new Node(3), new Node(4, 1), new Node(5, 1));
     132        testWay.firstNode().setCoor(new LatLon(0, 0));
     133        testWay.lastNode().setCoor(new LatLon(0.001, 0.001));
     134        testWay.getNode(1).setCoor(new LatLon(0.0001, 0.0001));
     135        testWay.getNode(3).setCoor(new LatLon(0.0009, 0.0009));
     136        dataSet.addPrimitiveRecursive(testWay);
     137        assertFalse(testWay.getNode(2).isLatLonKnown(), "The second node should not have valid coordinates");
     138        dataSet.setSelected(testWay.firstNode());
     139        assertTrue(map.selectMapMode(mapMode));
     140        assertEquals(mapMode, map.mapMode);
     141        // This is where the exception occurs; we shouldn't be setting the incomplete way as the target when we enter the mode.
     142        setupMapView(dataSet);
     143        assertDoesNotThrow(() -> GuiHelper.runInEDTAndWaitWithException(() -> {
     144            mapMode.mouseMoved(generateEvent(map.mapView, new LatLon(0.0001, 0.0001), 0));
     145        }));
     146    }
     147
     148    @Test
    104149    void testNonRegression23444() {
     150        testSimplifyWayAction((mapMode, map, testWay) -> {
     151            testWay.getNode(2).setCoor(null);
     152            assertFalse(testWay.getNode(2).isLatLonKnown(), "The second node should not have valid coordinates");
     153            mapMode.startSelecting();
     154            mapMode.mouseMoved(generateEvent(map.mapView, testWay.getNode(1), 0));
     155        });
     156    }
     157
     158    @Test
     159    void testAdd() {
     160        AtomicReference<Way> referenceWay = new AtomicReference<>();
     161        testSimplifyWayAction((mapMode, map, testWay) -> {
     162            // Add a node at 0.0001, 0.0005 (not on the direct line)
     163            mapMode.mouseMoved(generateEvent(map.mapView, new LatLon(0.0001, 0.0005), InputEvent.CTRL_DOWN_MASK));
     164            mapMode.mouseReleased(generateEvent(map.mapView, new LatLon(0.0001, 0.0005), InputEvent.CTRL_DOWN_MASK));
     165            referenceWay.set(testWay);
     166        });
     167        final Way testWay = referenceWay.get();
     168        // There should be a new node between nodes 1 and 2 (old node 2 is now node 3)
     169        assertAll(() -> assertEquals(6, testWay.getNodesCount()),
     170                () -> assertFalse(testWay.getNode(1).isNew()),
     171                () -> assertTrue(testWay.getNode(2).isNew()),
     172                () -> assertFalse(testWay.getNode(3).isNew()));
     173        // These aren't expected to be 0.0001 and 0.0005 exactly, due zoom and conversions between point and latlon.
     174        assertAll(() -> assertEquals(0.0001, testWay.getNode(2).lat(), 1e-5),
     175                () -> assertEquals(0.0005, testWay.getNode(2).lon(), 1e-5));
     176    }
     177
     178    @Test
     179    void testAddLock() {
     180        final AtomicReference<Way> referenceWay = new AtomicReference<>();
     181        testSimplifyWayAction((mapMode, map, testWay) -> {
     182            // Add a node at 0.0009, 0.0005 (not on the direct line) that is between nodes 1 and 2 but not 2 and 3.
     183            // First get the waysegment selected
     184            mapMode.mouseMoved(generateEvent(map.mapView, new LatLon(0.0001, 0.0005), InputEvent.CTRL_DOWN_MASK));
     185            // Then move to another location with ctrl+shift
     186            mapMode.mouseMoved(generateEvent(map.mapView, new LatLon(0.0009, 0.0005), InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK));
     187            // Finally, release the mouse with ctrl+shift
     188            mapMode.mouseReleased(generateEvent(map.mapView, new LatLon(0.0009, 0.0005),
     189                    InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK));
     190            referenceWay.set(testWay);
     191        });
     192        final Way testWay = referenceWay.get();
     193        // There should be a new node between nodes 1 and 2 (old node 2 is now node 3)
     194        assertAll(() -> assertEquals(6, testWay.getNodesCount()),
     195                () -> assertFalse(testWay.getNode(1).isNew()),
     196                () -> assertTrue(testWay.getNode(2).isNew()),
     197                () -> assertFalse(testWay.getNode(3).isNew()));
     198        // These aren't expected to be 0.0009 and 0.0005 exactly, due zoom and conversions between point and latlon.
     199        assertAll(() -> assertEquals(0.0009, testWay.getNode(2).lat(), 1e-5),
     200                () -> assertEquals(0.0005, testWay.getNode(2).lon(), 1e-5));
     201    }
     202
     203    @Test
     204    void testMove() {
     205        final AtomicReference<Way> referenceWay = new AtomicReference<>();
     206        testSimplifyWayAction((mapMode, map, testWay) -> {
     207            // Move node to 0.0001, 0.0005 (not on the direct line)
     208            // First get the waysegment selected
     209            mapMode.mouseMoved(generateEvent(map.mapView, new LatLon(0.0001, 0.0005), 0));
     210            // Finally, release the mouse
     211            mapMode.mouseReleased(generateEvent(map.mapView, new LatLon(0.0001, 0.0005), 0));
     212            referenceWay.set(testWay);
     213        });
     214        final Way testWay = referenceWay.get();
     215        assertEquals(5, testWay.getNodesCount());
     216        // These aren't expected to be 0.0001 and 0.0005 exactly, due zoom and conversions between point and latlon.
     217        assertAll(() -> assertEquals(0.0001, testWay.getNode(2).lat(), 1e-5),
     218                () -> assertEquals(0.0005, testWay.getNode(2).lon(), 1e-5));
     219    }
     220
     221    @Test
     222    void testMoveLock() {
     223        final AtomicReference<Way> referenceWay = new AtomicReference<>();
     224        testSimplifyWayAction((mapMode, map, testWay) -> {
     225            // Move node to 0.0001, 0.0005 (not on the direct line)
     226            // First get the waysegment selected
     227            mapMode.mouseMoved(generateEvent(map.mapView, new LatLon(0.0001, 0.0005), 0));
     228            // Then move to another location
     229            mapMode.mouseMoved(generateEvent(map.mapView, new LatLon(0.0009, 0.0005), InputEvent.SHIFT_DOWN_MASK));
     230            // Finally, release the mouse
     231            mapMode.mouseReleased(generateEvent(map.mapView, new LatLon(0.0009, 0.0005), InputEvent.SHIFT_DOWN_MASK));
     232            referenceWay.set(testWay);
     233        });
     234        final Way testWay = referenceWay.get();
     235        assertEquals(5, testWay.getNodesCount());
     236        // These aren't expected to be 0.0009 and 0.0005 exactly, due zoom and conversions between point and latlon.
     237        assertAll(() -> assertEquals(0.0009, testWay.getNode(2).lat(), 1e-5),
     238                () -> assertEquals(0.0005, testWay.getNode(2).lon(), 1e-5));
     239    }
     240
     241    @Test
     242    void testDelete() {
     243        DeleteCommand.setDeletionCallback(new AlwaysDeleteCallback());
     244        final AtomicReference<Way> referenceWay = new AtomicReference<>();
     245        testSimplifyWayAction((mapMode, map, testWay) -> {
     246            // Move node to 0.0001, 0.0005 (not on the direct line)
     247            // First get the waysegment selected
     248            mapMode.mouseMoved(generateEvent(map.mapView, new LatLon(0.0001, 0.0005), InputEvent.ALT_DOWN_MASK));
     249            // Finally, release the mouse
     250            mapMode.mouseReleased(generateEvent(map.mapView, new LatLon(0.0001, 0.0005), InputEvent.ALT_DOWN_MASK));
     251            referenceWay.set(testWay);
     252        });
     253        final Way testWay = referenceWay.get();
     254        assertEquals(4, testWay.getNodesCount());
     255        assertAll(testWay.getNodes().stream().map(n -> () -> assertNotEquals(3, n.getUniqueId())));
     256    }
     257
     258    @Test
     259    void testDeleteLock() {
     260        DeleteCommand.setDeletionCallback(new AlwaysDeleteCallback());
     261        final AtomicReference<Way> referenceWay = new AtomicReference<>();
     262        testSimplifyWayAction((mapMode, map, testWay) -> {
     263            // Move node to 0.0001, 0.0005 (not on the direct line)
     264            // First get the waysegment selected
     265            mapMode.mouseMoved(generateEvent(map.mapView, new LatLon(0.0001, 0.0005), 0));
     266            // Then move to another location
     267            mapMode.mouseMoved(generateEvent(map.mapView, new LatLon(0.0009, 0.0005), InputEvent.SHIFT_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
     268            // Finally, release the mouse
     269            mapMode.mouseReleased(generateEvent(map.mapView, new LatLon(0.0009, 0.0005), InputEvent.SHIFT_DOWN_MASK | InputEvent.ALT_DOWN_MASK));
     270            referenceWay.set(testWay);
     271        });
     272        final Way testWay = referenceWay.get();
     273        assertEquals(4, testWay.getNodesCount());
     274        assertAll(testWay.getNodes().stream().map(n -> () -> assertNotEquals(3, n.getUniqueId())));
     275    }
     276
     277    private void testSimplifyWayAction(TriConsumer<ImproveWayAccuracyAction, MapFrame, Way> runnable) {
    105278        final DataSet dataSet = new DataSet();
    106279        final OsmDataLayer layer = new OsmDataLayer(dataSet, "ImproveWayAccuracyActionT", null);
     
    115288        testWay.lastNode().setCoor(new LatLon(0.001, 0.001));
    116289        testWay.getNode(1).setCoor(new LatLon(0.0001, 0.0001));
     290        testWay.getNode(2).setCoor(new LatLon(0.0005, 0.0005));
    117291        testWay.getNode(3).setCoor(new LatLon(0.0009, 0.0009));
    118292        dataSet.addPrimitiveRecursive(testWay);
    119         assertFalse(testWay.getNode(2).isLatLonKnown(), "The second node should not have valid coordinates");
     293        setupMapView(dataSet);
    120294        final Graphics2D g2d = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB).createGraphics();
    121295        g2d.setClip(0, 0, WIDTH, HEIGHT);
     
    124298            assertDoesNotThrow(() -> map.mapView.paint(g2d), "The mapview should be able to handle a null coordinate node");
    125299            // Ensure that the test way is selected (and use the methods from the action to do so)
    126             GuiHelper.runInEDTAndWaitWithException(() -> {
    127                 map.mapView.zoomTo(new Bounds(0, 0, 0.001, 0.001));
    128                 // Get the target way selected (note: not selected in dataset -- use mapMode.mapReleased for that)
    129                 mapMode.mouseMoved(generateEvent(map.mapView, testWay.getNode(1)));
    130             });
    131             // mouseMoved shouldn't cause the way to get selected
    132             assertFalse(dataSet.getAllSelected().contains(testWay));
     300            assertDoesNotThrow(() -> GuiHelper.runInEDTAndWaitWithException(() -> {
     301                // Set the way as selected
     302                mapMode.mouseMoved(generateEvent(map.mapView, testWay.getNode(1), 0));
     303                mapMode.mouseReleased(generateEvent(map.mapView, testWay.getNode(1), 0));
     304                // And then run the test case
     305                runnable.accept(mapMode, map, testWay);
     306            }));
    133307
    134308            // Now check painting (where the problem should occur; the mapMode.paint call should be called as part of the map.mapView.paint call)
     
    136310            assertDoesNotThrow(() -> mapMode.paint(g2d, map.mapView, new Bounds(0, 0, 0.001, 0.001)));
    137311
    138             // Finally, check painting during selection
     312            // Then perform the action(s)
    139313            GuiHelper.runInEDTAndWaitWithException(() -> {
    140                 // Set the way as selected
    141                 mapMode.mouseReleased(generateEvent(map.mapView, new LatLon(0.0001, 0.0001)));
    142314                // Set the mouse location (unset in mouseReleased call)
    143                 mapMode.mouseMoved(generateEvent(map.mapView, new LatLon(0.0001, 0.0001)));
     315                // This is required by testNonRegression23444, and it doesn't hurt during the other tests
     316                mapMode.mouseMoved(generateEvent(map.mapView, new LatLon(0.0001, 0.0001), 0));
    144317            });
    145             // The way shouldn't be selected, since it isn't usable for the improve way tool
    146             assertFalse(dataSet.getAllSelected().contains(testWay));
    147318            // Now check painting again (just in case)
    148319            assertDoesNotThrow(() -> map.paint(g2d));
Note: See TracChangeset for help on using the changeset viewer.