source: josm/trunk/test/unit/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerTest.java

Last change on this file was 18921, checked in by taylor.smock, 4 months ago

Fix #23308: Fix a false positive for "Water area inside water area" validation (patch by gaben, modified)

A coastline as an area follows the right-side rule like coastlines as a way.
This means that a water area inside the area, as defined for almost every other
area tag, may be valid, depending upon the directionality of the coastline way.

Modifications are as follows:

  • Look for water areas inside oceans (coastline is drawn in clockwise direction)
    • This is anticipated to be a rare occurrence since most coastlines are expected to be part of a large area.
  • Add non-regression test
  • Keep previous spacing for easier svn blame usage
  • Property svn:eol-style set to native
File size: 21.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.validation.tests;
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.assertTrue;
10
11import java.io.InputStream;
12import java.io.StringReader;
13import java.util.Collection;
14import java.util.HashSet;
15import java.util.Iterator;
16import java.util.LinkedHashSet;
17import java.util.List;
18import java.util.Set;
19
20import org.junit.jupiter.api.BeforeEach;
21import org.junit.jupiter.api.Test;
22import org.openstreetmap.josm.TestUtils;
23import org.openstreetmap.josm.command.ChangePropertyCommand;
24import org.openstreetmap.josm.command.ChangePropertyKeyCommand;
25import org.openstreetmap.josm.command.Command;
26import org.openstreetmap.josm.command.PseudoCommand;
27import org.openstreetmap.josm.command.SequenceCommand;
28import org.openstreetmap.josm.data.coor.LatLon;
29import org.openstreetmap.josm.data.osm.DataSet;
30import org.openstreetmap.josm.data.osm.IPrimitive;
31import org.openstreetmap.josm.data.osm.Node;
32import org.openstreetmap.josm.data.osm.OsmPrimitive;
33import org.openstreetmap.josm.data.osm.OsmUtils;
34import org.openstreetmap.josm.data.osm.Way;
35import org.openstreetmap.josm.data.preferences.sources.ExtendedSourceEntry;
36import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
37import org.openstreetmap.josm.data.validation.Severity;
38import org.openstreetmap.josm.data.validation.Test.TagTest;
39import org.openstreetmap.josm.data.validation.TestError;
40import org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker.ParseResult;
41import org.openstreetmap.josm.gui.mappaint.Environment;
42import org.openstreetmap.josm.gui.mappaint.MapPaintStyles;
43import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
44import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
45import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
46import org.openstreetmap.josm.io.OsmReader;
47import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
48import org.openstreetmap.josm.testutils.annotations.Projection;
49import org.openstreetmap.josm.testutils.annotations.Territories;
50import org.openstreetmap.josm.tools.Logging;
51
52/**
53 * JUnit Test of {@link MapCSSTagChecker}.
54 */
55@BasicPreferences
56@Projection
57@Territories
58class MapCSSTagCheckerTest {
59 /**
60 * Setup test.
61 */
62 @BeforeEach
63 public void setUp() {
64 MapCSSTagCheckerAsserts.clear();
65 }
66
67 static MapCSSTagChecker buildTagChecker(String css) throws ParseException {
68 final MapCSSTagChecker test = new MapCSSTagChecker();
69 Set<String> errors = new HashSet<>();
70 test.checks.putAll("test", MapCSSTagCheckerRule.readMapCSS(new StringReader(css), errors::add).parseChecks);
71 assertTrue(errors.isEmpty(), errors::toString);
72 return test;
73 }
74
75 /**
76 * Test {@code natural=marsh}.
77 * @throws ParseException if a parsing error occurs
78 */
79 @Test
80 void testNaturalMarsh() throws ParseException {
81 ParseResult result = MapCSSTagCheckerRule.readMapCSS(new StringReader(
82 "*[natural=marsh] {\n" +
83 " group: tr(\"deprecated\");\n" +
84 " throwWarning: tr(\"{0}={1} is deprecated\", \"{0.key}\", tag(\"natural\"));\n" +
85 " fixRemove: \"{0.key}\";\n" +
86 " fixAdd: \"natural=wetland\";\n" +
87 " fixAdd: \"wetland=marsh\";\n" +
88 "}"));
89 final List<MapCSSTagCheckerRule> checks = result.parseChecks;
90 assertEquals(1, checks.size());
91 assertTrue(result.parseErrors.isEmpty());
92 final MapCSSTagCheckerRule check = checks.get(0);
93 assertNotNull(check);
94 assertEquals("{0.key}=null is deprecated", check.getDescription(null, null));
95 assertEquals("fixRemove: <{0.key}>", check.fixCommands.get(0).toString());
96 assertEquals("fixAdd: natural=wetland", check.fixCommands.get(1).toString());
97 assertEquals("fixAdd: wetland=marsh", check.fixCommands.get(2).toString());
98 final OsmPrimitive n1 = OsmUtils.createPrimitive("node natural=marsh");
99 final OsmPrimitive n2 = OsmUtils.createPrimitive("node natural=wood");
100 new DataSet(n1, n2);
101 assertTrue(check.test(n1));
102
103 final Collection<TestError> errors = check.getErrorsForPrimitive(n1, check.whichSelectorMatchesPrimitive(n1), new Environment(), null);
104 assertEquals(1, errors.size());
105 TestError err = errors.iterator().next();
106 assertEquals("deprecated", err.getMessage());
107 assertEquals("natural=marsh is deprecated", err.getDescription());
108 assertEquals(Severity.WARNING, err.getSeverity());
109 assertEquals("Sequence: Fix of natural=marsh is deprecated", check.fixPrimitive(n1).getDescriptionText());
110 assertEquals("{natural=}", ((ChangePropertyCommand) check.fixPrimitive(n1).getChildren().iterator().next()).getTags().toString());
111 assertFalse(check.test(n2));
112 assertEquals("The key is natural and the value is marsh",
113 MapCSSTagCheckerRule.insertArguments(check.rule.selectors.get(0), "The key is {0.key} and the value is {0.value}", null));
114 }
115
116 /**
117 * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/10913">Bug #10913</a>.
118 * @throws ParseException if a parsing error occurs
119 */
120 @Test
121 void testTicket10913() throws ParseException {
122 final OsmPrimitive p = TestUtils.addFakeDataSet(TestUtils.newWay("highway=tertiary construction=yes"));
123 final MapCSSTagCheckerRule check = MapCSSTagCheckerRule.readMapCSS(new StringReader("way {" +
124 "throwError: \"error\";" +
125 "fixChangeKey: \"highway => construction\";\n" +
126 "fixAdd: \"highway=construction\";\n" +
127 "}")).parseChecks.get(0);
128 final Command command = check.fixPrimitive(p);
129 assertInstanceOf(SequenceCommand.class, command);
130 final Iterator<PseudoCommand> it = command.getChildren().iterator();
131 assertInstanceOf(ChangePropertyKeyCommand.class, it.next());
132 assertInstanceOf(ChangePropertyCommand.class, it.next());
133 }
134
135 /**
136 * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/9782">Bug #9782</a>.
137 * @throws ParseException if a parsing error occurs
138 */
139 @Test
140 void testTicket9782() throws ParseException {
141 final MapCSSTagChecker test = buildTagChecker("*[/.+_name/][!name] {" +
142 "throwWarning: tr(\"has {0} but not {1}\", \"{0.key}\", \"{1.key}\");}");
143 final OsmPrimitive p = OsmUtils.createPrimitive("way alt_name=Foo");
144 final Collection<TestError> errors = test.getErrorsForPrimitive(p, false);
145 assertEquals(1, errors.size());
146 assertEquals("has alt_name but not name", errors.iterator().next().getMessage());
147 assertEquals("3000_has alt_name but not name", errors.iterator().next().getIgnoreSubGroup());
148 }
149
150 /**
151 * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/10859">Bug #10859</a>.
152 * @throws ParseException if a parsing error occurs
153 */
154 @Test
155 void testTicket10859() throws ParseException {
156 final MapCSSTagChecker test = buildTagChecker("way[highway=footway][foot?!] {\n" +
157 " throwWarning: tr(\"{0} used with {1}\", \"{0.value}\", \"{1.tag}\");}");
158 final OsmPrimitive p = OsmUtils.createPrimitive("way highway=footway foot=no");
159 final Collection<TestError> errors = test.getErrorsForPrimitive(p, false);
160 assertEquals(1, errors.size());
161 assertEquals("footway used with foot=no", errors.iterator().next().getMessage());
162 assertEquals("3000_footway used with foot=no", errors.iterator().next().getIgnoreSubGroup());
163 }
164
165 /**
166 * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/13630">Bug #13630</a>.
167 * @throws ParseException if a parsing error occurs
168 */
169 @Test
170 void testTicket13630() throws ParseException {
171 ParseResult result = MapCSSTagCheckerRule.readMapCSS(new StringReader(
172 "node[crossing=zebra] {fixRemove: \"crossing=zebra\";}"));
173 assertTrue(result.parseChecks.isEmpty());
174 assertEquals(1, result.parseErrors.size());
175 }
176
177 /**
178 * Unit test of {@code min-josm-version} processing.
179 * @throws ParseException if a parsing error occurs
180 */
181 @Test
182 void testPreprocessing() throws ParseException {
183 final MapCSSTagChecker test = buildTagChecker(
184 "@supports (min-josm-version: 0) { *[foo] { throwWarning: \"!\"; } }\n" +
185 "@supports (min-josm-version: 2147483647) { *[bar] { throwWarning: \"!\"; } }");
186 assertEquals(1, test.getErrorsForPrimitive(OsmUtils.createPrimitive("way foo=1"), false).size());
187 assertEquals(0, test.getErrorsForPrimitive(OsmUtils.createPrimitive("way bar=1"), false).size());
188 }
189
190 /**
191 * Unit test of {@link MapCSSTagChecker#initialize}.
192 * @throws Exception if an error occurs
193 */
194 @Test
195 void testInit() throws Exception {
196 Logging.clearLastErrorAndWarnings();
197 MapCSSTagChecker c = new MapCSSTagChecker();
198 c.initialize();
199
200 assertTrue(Logging.getLastErrorAndWarnings().isEmpty(), "no warnings/errors are logged");
201
202 // to trigger MapCSSStyleIndex code
203 Node node = new Node(new LatLon(12, 34));
204 node.put("amenity", "drinking_water");
205 assertTrue(c.getErrorsForPrimitive(node, false).isEmpty());
206 }
207
208 /**
209 * Unit test for all {@link TagTest} assertions.
210 * @throws Exception if an error occurs
211 */
212 @Test
213 void testAssertions() throws Exception {
214 MapCSSTagChecker c = new MapCSSTagChecker();
215 Set<String> assertionErrors = new LinkedHashSet<>();
216
217 // initialize
218 for (ExtendedSourceEntry entry : ValidatorPrefHelper.INSTANCE.getDefault()) {
219 c.addMapCSS(entry.url, assertionErrors::add);
220 }
221
222 for (String msg : assertionErrors) {
223 Logging.error(msg);
224 }
225 assertTrue(assertionErrors.isEmpty(), "not all assertions included in the tests are met");
226 }
227
228 /**
229 * Checks that assertions work for country-specific checks.
230 * @throws ParseException if a parsing error occurs
231 */
232 @Test
233 void testAssertInsideCountry() throws ParseException {
234 final MapCSSTagChecker test = buildTagChecker(
235 "node[amenity=parking][inside(\"BR\")] {\n" +
236 " throwWarning: \"foo\";\n" +
237 " assertMatch: \"node amenity=parking\";\n" +
238 " assertNoMatch: \"node amenity=restaurant\";\n" +
239 "}");
240 assertNotNull(test);
241 }
242
243 /**
244 * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/17058">Bug #17058</a>.
245 * @throws ParseException if a parsing error occurs
246 */
247 @Test
248 void testTicket17058() throws ParseException {
249 final MapCSSTagChecker test = buildTagChecker(
250 "*[name =~ /(?i).*Straße.*/][inside(\"LI,CH\")] {\n" +
251 " throwError: tr(\"street name contains ß\");\n" +
252 " assertMatch: \"way name=Hauptstraße\";\n" +
253 " assertNoMatch: \"way name=Hauptstrasse\";\n" +
254 "}");
255 assertNotNull(test);
256 }
257
258 /**
259 * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/13762">Bug #13762</a>.
260 * @throws ParseException if a parsing error occurs
261 */
262 @Test
263 void testTicket13762() throws ParseException {
264 final ParseResult parseResult = MapCSSTagCheckerRule.readMapCSS(new StringReader("" +
265 "meta[lang=de] {\n" +
266 " title: \"Deutschlandspezifische Regeln\";" +
267 "}"));
268 assertTrue(parseResult.parseErrors.isEmpty());
269 }
270
271 /**
272 * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/14287">Bug #14287</a>.
273 * @throws Exception if an error occurs
274 */
275 @Test
276 void testTicket14287() throws Exception {
277 final MapCSSTagChecker test = buildTagChecker(
278 "node[amenity=parking] ∈ *[amenity=parking] {" +
279 " throwWarning: tr(\"{0} inside {1}\", \"amenity=parking\", \"amenity=parking\");" +
280 "}");
281 try (InputStream is = TestUtils.getRegressionDataStream(14287, "example.osm")) {
282 test.visit(OsmReader.parseDataSet(is, null).allPrimitives());
283 assertEquals(6, test.getErrors().size());
284 }
285 }
286
287 /**
288 * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/17053">Bug #17053</a>.
289 * @throws ParseException if a parsing error occurs
290 */
291 @Test
292 void testTicket17053() throws ParseException {
293 final MapCSSTagChecker test = buildTagChecker(
294 "way[highway=cycleway][cycleway=track] {\n" +
295 " throwWarning: tr(\"{0} with {1}\", \"{0.tag}\", \"{1.tag}\");\n" +
296 " -osmoseItemClassLevel: \"3032/30328/2\";\n" +
297 " -osmoseTags: list(\"tag\", \"highway\", \"cycleway\");\n" +
298 " fixRemove: \"cycleway\";\n" +
299 "}");
300 assertEquals(1, test.checks.size());
301 MapCSSTagCheckerRule check = test.checks.get("test").iterator().next();
302 assertEquals(1, check.fixCommands.size());
303 assertEquals(2, check.rule.declaration.instructions.size());
304 }
305
306 private void doTestNaturalWood(int ticket, String filename, int errorsCount, int setsCount) throws Exception {
307 final MapCSSTagChecker test = buildTagChecker(
308 "area:closed:areaStyle[tag(\"natural\") = parent_tag(\"natural\")] ⧉ area:closed:areaStyle[natural] {" +
309 " throwWarning: tr(\"Overlapping Identical Natural Areas\");" +
310 "}");
311 final MapCSSStyleSource style = new MapCSSStyleSource(
312 "area[natural=wood] {" +
313 " fill-color: woodarea#008000;" +
314 "}");
315 MapPaintStyles.addStyle(style);
316 try (InputStream is = TestUtils.getRegressionDataStream(ticket, filename)) {
317 test.visit(OsmReader.parseDataSet(is, null).allPrimitives());
318 List<TestError> errors = test.getErrors();
319 assertEquals(errorsCount, errors.size());
320 Set<Set<IPrimitive>> primitives = new HashSet<>();
321 for (TestError e : errors) {
322 primitives.add(new HashSet<>(e.getPrimitives()));
323 }
324 assertEquals(setsCount, primitives.size());
325 } finally {
326 MapPaintStyles.removeStyle(style);
327 }
328 }
329
330 /**
331 * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/12627">Bug #12627</a>.
332 * @throws Exception if an error occurs
333 */
334 @Test
335 void testTicket12627() throws Exception {
336 doTestNaturalWood(12627, "overlapping.osm", 1, 1);
337 }
338
339 /**
340 * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/14289">Bug #14289</a>.
341 * @throws Exception if an error occurs
342 */
343 @Test
344 void testTicket14289() throws Exception {
345 doTestNaturalWood(14289, "example2.osm", 3, 3);
346 }
347
348 /**
349 * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/15641">Bug #15641</a>.
350 * @throws ParseException if an error occurs
351 */
352 @Test
353 void testTicket15641() throws ParseException {
354 assertNotNull(buildTagChecker(
355 "relation[type=public_transport][public_transport=stop_area_group] > way {" +
356 " throwWarning: eval(count(parent_tags(public_transport)));" +
357 "}"));
358 }
359
360 /**
361 * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/17358">Bug #17358</a>.
362 * @throws ParseException if an error occurs
363 */
364 @Test
365 void testTicket17358() throws ParseException {
366 final Collection<TestError> errors = buildTagChecker(
367 "*[/^name/=~/Test/]{" +
368 " throwWarning: \"Key value match\";" +
369 "}").getErrorsForPrimitive(OsmUtils.createPrimitive("way name=Test St"), false);
370 assertEquals(1, errors.size());
371 }
372
373 /**
374 * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/17695">Bug #17695</a>.
375 * @throws Exception if an error occurs
376 */
377 @Test
378 void testTicket17695() throws Exception {
379 final MapCSSTagChecker test = buildTagChecker(
380 "*[building] ∈ *[building] {" +
381 "throwWarning: tr(\"Building inside building\");" +
382 "}");
383 try (InputStream is = TestUtils.getRegressionDataStream(17695, "bib2.osm")) {
384 test.visit(OsmReader.parseDataSet(is, null).allPrimitives());
385 List<TestError> errors = test.getErrors();
386 assertEquals(6, errors.size());
387 }
388 }
389
390 /**
391 * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/13165">Bug #13165</a>.
392 * @throws Exception if an error occurs
393 */
394 @Test
395 void testTicket13165() throws Exception {
396 final MapCSSTagChecker test = buildTagChecker(
397 "area:closed[tag(\"landuse\") = parent_tag(\"landuse\")] ⧉ area:closed[landuse] {"
398 + "throwWarning: tr(\"Overlapping Identical Landuses\");"
399 + "}");
400 try (InputStream is = TestUtils.getRegressionDataStream(13165, "13165.osm")) {
401 test.visit(OsmReader.parseDataSet(is, null).allPrimitives());
402 List<TestError> errors = test.getErrors();
403 assertEquals(3, errors.size());
404 }
405 }
406
407 /**
408 * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/13165">Bug #13165</a>.
409 * @throws Exception if an error occurs
410 */
411 @Test
412 void testTicket13165IncompleteMP() throws Exception {
413 final MapCSSTagChecker test = buildTagChecker(
414 "area:closed[tag(\"landuse\") = parent_tag(\"landuse\")] ⧉ area:closed[landuse] {"
415 + "throwWarning: tr(\"Overlapping Identical Landuses\");"
416 + "}");
417 try (InputStream is = TestUtils.getRegressionDataStream(13165, "13165-incomplete.osm.bz2")) {
418 test.visit(OsmReader.parseDataSet(is, null).allPrimitives());
419 List<TestError> errors = test.getErrors();
420 assertEquals(3, errors.size());
421 }
422 }
423
424 /**
425 * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/19053">Bug #19053</a> and
426 * <a href="https://josm.openstreetmap.de/ticket/20957">Bug #20957</a>
427 * - MapCSS rule with group.
428 * - MapCSS functions round, tag, *, /
429 * @throws ParseException if a parsing error occurs
430 */
431 @Test
432 void testTicket19053() throws ParseException {
433 final MapCSSTagChecker test = buildTagChecker(
434 "*[ele][ele =~ /^-?[0-9]+\\.[0-9][0-9][0-9]+$/] {"
435 + "throwWarning: tr(\"{0}\",\"{0.tag}\");"
436 + "fixAdd: concat(\"ele=\", round(tag(\"ele\")*100)/100);"
437 + "group: tr(\"Unnecessary amount of decimal places\");" + "}");
438 final OsmPrimitive p = OsmUtils.createPrimitive("node ele=12.123456");
439 new DataSet(p);
440 final Collection<TestError> errors = test.getErrorsForPrimitive(p, false);
441 assertEquals(1, errors.size());
442 assertEquals("Unnecessary amount of decimal places", errors.iterator().next().getMessage());
443 assertEquals("3000_ele=12.123456", errors.iterator().next().getIgnoreSubGroup());
444 assertEquals("3000_Unnecessary amount of decimal places", errors.iterator().next().getIgnoreGroup());
445 Command fix = errors.iterator().next().getFix();
446 assertNotNull(fix);
447 assertEquals("12.123456", p.get("ele"));
448 fix.executeCommand();
449 assertEquals("12.12", p.get("ele"));
450 }
451
452 /**
453 * A water area inside a coastline, where the coastline way is oriented away from the water area
454 * (the water area is not inside the ocean).
455 */
456 @Test
457 void testTicket23308() {
458 final MapCSSTagChecker test = new MapCSSTagChecker();
459 final Way innerWay = TestUtils.newWay("natural=water",
460 new Node(new LatLon(32.775, -117.238)),
461 new Node(new LatLon(32.774, -117.238)),
462 new Node(new LatLon(32.774, -117.237)),
463 new Node(new LatLon(32.775, -117.237)));
464 final Way outerWay = TestUtils.newWay("natural=coastline",
465 new Node(new LatLon(32.779, -117.232)),
466 new Node(new LatLon(32.777, -117.241)),
467 new Node(new LatLon(32.771, -117.240)),
468 new Node(new LatLon(32.771, -117.235)));
469 final DataSet ds = new DataSet();
470 ds.addPrimitiveRecursive(innerWay);
471 ds.addPrimitiveRecursive(outerWay);
472 innerWay.addNode(innerWay.firstNode());
473 outerWay.addNode(outerWay.firstNode());
474 assertDoesNotThrow(test::initialize);
475 test.startTest(NullProgressMonitor.INSTANCE);
476 test.visit(ds.allPrimitives());
477 test.endTest();
478 assertTrue(test.getErrors().isEmpty());
479 }
480}
Note: See TracBrowser for help on using the repository browser.