source: josm/trunk/test/unit/org/openstreetmap/josm/gui/dialogs/relation/GenericRelationEditorTest.java

Last change on this file was 18866, checked in by taylor.smock, 6 months ago

Fix #23196: DataIntegrityProblemException: Primitive must be part of the dataset

It turns out that FilterModel doesn't care whether a primitive's referrers are
in the dataset or not.

This additionally adds a non-regression test and modifies WindowMocker to cover
some more headless exceptions. This additionally lets us stop mocking
ExtendedDialog#setupDialog.

  • Property svn:eol-style set to native
File size: 14.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs.relation;
3
4import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
5import static org.junit.jupiter.api.Assertions.assertEquals;
6import static org.junit.jupiter.api.Assertions.assertFalse;
7import static org.junit.jupiter.api.Assertions.assertInstanceOf;
8import static org.junit.jupiter.api.Assertions.assertNotNull;
9import static org.junit.jupiter.api.Assertions.assertNull;
10import static org.junit.jupiter.api.Assertions.assertSame;
11import static org.junit.jupiter.api.Assertions.fail;
12import static org.openstreetmap.josm.tools.I18n.tr;
13
14import java.awt.Component;
15import java.awt.Container;
16import java.awt.event.MouseEvent;
17import java.awt.event.MouseListener;
18import java.awt.event.WindowEvent;
19import java.util.ArrayDeque;
20import java.util.Arrays;
21import java.util.Collection;
22import java.util.Collections;
23import java.util.concurrent.atomic.AtomicReference;
24import java.util.stream.Collectors;
25
26import javax.swing.Action;
27import javax.swing.JButton;
28import javax.swing.JLabel;
29import javax.swing.JOptionPane;
30import javax.swing.JPanel;
31
32import org.junit.jupiter.api.BeforeEach;
33import org.junit.jupiter.api.Test;
34import org.junit.platform.commons.support.ReflectionSupport;
35import org.openstreetmap.josm.TestUtils;
36import org.openstreetmap.josm.data.osm.DataSet;
37import org.openstreetmap.josm.data.osm.Node;
38import org.openstreetmap.josm.data.osm.OsmPrimitive;
39import org.openstreetmap.josm.data.osm.Relation;
40import org.openstreetmap.josm.data.osm.RelationMember;
41import org.openstreetmap.josm.data.osm.Way;
42import org.openstreetmap.josm.gui.ExtendedDialog;
43import org.openstreetmap.josm.gui.MainApplication;
44import org.openstreetmap.josm.gui.SideButton;
45import org.openstreetmap.josm.gui.dialogs.RelationListDialog;
46import org.openstreetmap.josm.gui.dialogs.relation.actions.AddSelectedAtStartAction;
47import org.openstreetmap.josm.gui.dialogs.relation.actions.IRelationEditorActionAccess;
48import org.openstreetmap.josm.gui.dialogs.relation.actions.PasteMembersAction;
49import org.openstreetmap.josm.gui.layer.OsmDataLayer;
50import org.openstreetmap.josm.gui.tagging.TagEditorPanel;
51import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
52import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
53import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetHandler;
54import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetLabel;
55import org.openstreetmap.josm.spi.preferences.Config;
56import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
57import org.openstreetmap.josm.testutils.annotations.Main;
58import org.openstreetmap.josm.testutils.annotations.Projection;
59import org.openstreetmap.josm.testutils.annotations.TaggingPresets;
60import org.openstreetmap.josm.testutils.mockers.ExtendedDialogMocker;
61import org.openstreetmap.josm.testutils.mockers.JOptionPaneSimpleMocker;
62import org.openstreetmap.josm.testutils.mockers.WindowMocker;
63import org.openstreetmap.josm.tools.JosmRuntimeException;
64
65import mockit.Invocation;
66import mockit.Mock;
67import mockit.MockUp;
68
69/**
70 * Unit tests of {@link GenericRelationEditor} class.
71 */
72@BasicPreferences
73@Main
74@Projection
75public class GenericRelationEditorTest {
76 /**
77 * Returns a new relation editor for unit tests.
78 * @param orig relation
79 * @param layer data layer
80 * @return new relation editor for unit tests
81 */
82 public static IRelationEditor newRelationEditor(final Relation orig, final OsmDataLayer layer) {
83 return new IRelationEditor() {
84 private Relation r = orig;
85
86 @Override
87 public void setRelation(Relation relation) {
88 r = relation;
89 }
90
91 @Override
92 public boolean isDirtyRelation() {
93 return false;
94 }
95
96 @Override
97 public Relation getRelationSnapshot() {
98 return r;
99 }
100
101 @Override
102 public Relation getRelation() {
103 return r;
104 }
105
106 @Override
107 public void reloadDataFromRelation() {
108 // Do nothing
109 }
110
111 @Override
112 public OsmDataLayer getLayer() {
113 return layer;
114 }
115 };
116 }
117
118 @BeforeEach
119 void setup() {
120 new PasteMembersActionMock();
121 new WindowMocker();
122 }
123
124 private static AtomicReference<RelationEditor> setupGuiMocks() {
125 AtomicReference<RelationEditor> editorReference = new AtomicReference<>();
126 new MockUp<RelationEditor>() {
127 @Mock public RelationEditor getEditor(Invocation invocation, OsmDataLayer layer, Relation r,
128 Collection<RelationMember> selectedMembers) {
129 editorReference.set(invocation.proceed(layer, r, selectedMembers));
130 return editorReference.get();
131 }
132 };
133 // We want to go through the `setVisible` code, just in case. So we have to mock the window location
134 new MockUp<GenericRelationEditor>() {
135 @Mock public void setVisible(boolean visible) {
136 // Do nothing. Ideally, we would just mock the awt methods called, but that would take a lot of mocking.
137 }
138 };
139 return editorReference;
140 }
141
142 /**
143 * Unit test of {@link GenericRelationEditor#addPrimitivesToRelation}.
144 */
145 @Test
146 void testAddPrimitivesToRelation() {
147 TestUtils.assumeWorkingJMockit();
148 final JOptionPaneSimpleMocker jopsMocker = new JOptionPaneSimpleMocker();
149
150 Relation r = TestUtils.addFakeDataSet(new Relation(1));
151 assertNull(GenericRelationEditor.addPrimitivesToRelation(r, Collections.<OsmPrimitive>emptyList()));
152 jopsMocker.getMockResultMap().put(
153 "<html>You are trying to add a relation to itself.<br><br>This generates a circular dependency of parent/child elements "
154 + "and is therefore discouraged.<br>Skipping relation 'incomplete'.</html>",
155 JOptionPane.OK_OPTION
156 );
157
158 assertNull(GenericRelationEditor.addPrimitivesToRelation(r, Collections.singleton(new Relation(1))));
159
160 assertEquals(1, jopsMocker.getInvocationLog().size());
161 Object[] invocationLogEntry = jopsMocker.getInvocationLog().get(0);
162 assertEquals(JOptionPane.OK_OPTION, (int) invocationLogEntry[0]);
163 assertEquals("Warning", invocationLogEntry[2]);
164
165 assertNotNull(GenericRelationEditor.addPrimitivesToRelation(r, Collections.singleton(new Node(1))));
166 assertNotNull(GenericRelationEditor.addPrimitivesToRelation(r, Collections.singleton(new Way(1))));
167 assertNotNull(GenericRelationEditor.addPrimitivesToRelation(r, Collections.singleton(new Relation(2))));
168
169 assertEquals(1, jopsMocker.getInvocationLog().size());
170 }
171
172 /**
173 * Unit test of {@code GenericRelationEditor#build*} methods.
174 * <p>
175 * This test only tests if they do not throw exceptions.
176 */
177 @Test
178 void testBuild() {
179 DataSet ds = new DataSet();
180 Relation relation = new Relation(1);
181 ds.addPrimitive(relation);
182 OsmDataLayer layer = new OsmDataLayer(ds, "test", null);
183 IRelationEditor re = newRelationEditor(relation, layer);
184
185 AutoCompletingTextField tfRole = GenericRelationEditor.buildRoleTextField(re);
186 assertNotNull(tfRole);
187
188 TagEditorPanel tagEditorPanel = new TagEditorPanel(relation, null);
189
190 JPanel top = GenericRelationEditor.buildTagEditorPanel(tagEditorPanel);
191 assertNotNull(top);
192 assertNotNull(tagEditorPanel.getModel());
193 }
194
195 @Test
196 void testNonRegression23091() throws Exception {
197 DataSet ds = new DataSet();
198 Relation relation = new Relation(1);
199 ds.addPrimitive(relation);
200 OsmDataLayer layer = new OsmDataLayer(ds, "test", null);
201
202 final GenericRelationEditor gr = new GenericRelationEditor(layer, relation, Collections.emptyList());
203 final IRelationEditorActionAccess iAccess = (IRelationEditorActionAccess)
204 ReflectionSupport.tryToReadFieldValue(GenericRelationEditor.class.getDeclaredField("actionAccess"), gr)
205 .get();
206 final TaggingPresetHandler handler = (TaggingPresetHandler)
207 ReflectionSupport.tryToReadFieldValue(MemberTableModel.class.getDeclaredField("presetHandler"), iAccess.getMemberTableModel())
208 .get();
209 final Collection<OsmPrimitive> selection = handler.getSelection();
210 assertEquals(1, selection.size());
211 assertSame(relation, selection.iterator().next(), "The selection should be the same");
212 }
213
214 /**
215 * Ensure that users can create new relations and modify them.
216 */
217 @Test
218 void testNonRegression23116() {
219 // Setup the mocks
220 final AtomicReference<RelationEditor> editorReference = setupGuiMocks();
221 // Set up the data
222 final DataSet dataSet = new DataSet();
223 MainApplication.getLayerManager().addLayer(new OsmDataLayer(dataSet, "GenericRelationEditorTest.testNonRegression23116", null));
224 dataSet.addPrimitive(TestUtils.newNode(""));
225 dataSet.setSelected(dataSet.allPrimitives());
226 final RelationListDialog relationListDialog = new RelationListDialog();
227 try {
228 final Action newAction = ((SideButton) getComponent(relationListDialog, 2, 0, 0)).getAction();
229 assertEquals("class org.openstreetmap.josm.gui.dialogs.RelationListDialog$NewAction",
230 newAction.getClass().toString());
231 // Now get the buttons we want to push
232 newAction.actionPerformed(null);
233 final GenericRelationEditor editor = assertInstanceOf(GenericRelationEditor.class, editorReference.get());
234 final JButton okAction = getComponent(editor, 0, 1, 0, 2, 0);
235 assertEquals(tr("Delete"), okAction.getText(), "OK is Delete until the relation actually has data");
236 assertNotNull(editor);
237 final TagEditorPanel tagEditorPanel = getComponent(editor, 0, 1, 0, 1, 0, 0, 1, 1);
238 // We need at least one tag for the action to not be "Delete".
239 tagEditorPanel.getModel().add("type", "someUnknownTypeHere");
240 final Action addAtStartAction = assertInstanceOf(AddSelectedAtStartAction.class,
241 ((JButton) getComponent(editor, 0, 1, 0, 1, 0, 0, 2, 0, 2, 1, 2, 0, 0)).getAction());
242 // Perform the actual test.
243 assertDoesNotThrow(() -> addAtStartAction.actionPerformed(null));
244 assertDoesNotThrow(() -> okAction.getAction().actionPerformed(null));
245 assertFalse(dataSet.getRelations().isEmpty());
246 assertSame(dataSet.getNodes().iterator().next(),
247 dataSet.getRelations().iterator().next().getMember(0).getNode());
248 } finally {
249 // This avoids an issue with the cleanup code and the mocks for this test
250 if (editorReference.get() != null) {
251 RelationDialogManager.getRelationDialogManager().windowClosed(new WindowEvent(editorReference.get(), 0));
252 }
253 }
254 }
255
256 /**
257 * Ensure that users can create new relations with a preset available and open the preset.
258 * See {@link TaggingPreset#showAndApply} for where a relation may exist without a dataset.
259 */
260 @BasicPreferences
261 @TaggingPresets
262 @Test
263 void testNonRegression23196() {
264 // This happens when the preset validator is enabled (Preferences -> `Tagging Presets` -> `Run data validator on user input`)
265 Config.getPref().putBoolean("taggingpreset.validator", true);
266 // Setup the mocks
267 final AtomicReference<RelationEditor> editorReference = setupGuiMocks();
268 new ExtendedDialogMocker(Collections.singletonMap("Change 1 object", "Apply Preset")) {
269 @Override
270 protected String getString(ExtendedDialog instance) {
271 return instance.getTitle();
272 }
273 };
274 // Set up the data
275 final DataSet dataSet = new DataSet();
276 final OsmDataLayer layer = new OsmDataLayer(dataSet, "GenericRelationEditorTest.testNonRegression23196", null);
277 MainApplication.getLayerManager().addLayer(layer);
278 dataSet.addPrimitive(TestUtils.newNode(""));
279 dataSet.setSelected(dataSet.allPrimitives());
280 try {
281 RelationEditor.getEditor(layer, TestUtils.newRelation("type=multipolygon"),
282 dataSet.getSelected().stream().map(p -> new RelationMember("", p)).collect(Collectors.toList()));
283 final GenericRelationEditor editor = assertInstanceOf(GenericRelationEditor.class, editorReference.get());
284 TaggingPresetLabel label = getComponentByNameOrText(TaggingPresetLabel.class, editor, "Relations/Multipolygon …");
285 final MouseEvent mouseEvent = new MouseEvent(label, 0, 0, 0, 0, 0, 0, false);
286 for (MouseListener listener : label.getMouseListeners()) {
287 assertDoesNotThrow(() -> listener.mouseClicked(mouseEvent));
288 }
289 } finally {
290 // This avoids an issue with the cleanup code and the mocks for this test
291 if (editorReference.get() != null) {
292 RelationDialogManager.getRelationDialogManager().windowClosed(new WindowEvent(editorReference.get(), 0));
293 }
294 }
295 }
296
297 private static <T extends Component> T getComponentByNameOrText(Class<T> clazz, Container parent, String name) {
298 final ArrayDeque<Component> componentDeque = new ArrayDeque<>(Collections.singletonList(parent));
299 while (!componentDeque.isEmpty()) {
300 final Component current = componentDeque.pop();
301 if (current instanceof Container) {
302 componentDeque.addAll(Arrays.asList(((Container) current).getComponents()));
303 }
304 if (clazz.isInstance(current)) {
305 T component = clazz.cast(current);
306 if (name.equals(component.getName())) {
307 return component;
308 } else if (component instanceof JLabel && name.equals(((JLabel) component).getText())) {
309 return component;
310 }
311 }
312 }
313 fail("Component with name " + name + " not found");
314 throw new JosmRuntimeException("This should never happen due to the fail line");
315 }
316
317 @SuppressWarnings("unchecked")
318 private static <T extends Container> T getComponent(Container parent, int... tree) {
319 Container current = parent;
320 for (int i : tree) {
321 current = (Container) current.getComponent(i);
322 }
323 return (T) current;
324 }
325
326 private static class PasteMembersActionMock extends MockUp<PasteMembersAction> {
327 @Mock
328 protected void updateEnabledState() {
329 // Do nothing
330 }
331 }
332}
Note: See TracBrowser for help on using the repository browser.