source: josm/trunk/test/unit/org/openstreetmap/josm/gui/preferences/imagery/ImageryPreferenceTestIT.java

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

Fix #16567: Upgrade to JUnit 5

JOSMTestRules and JOSMTestFixture can reset the default JOSM profile, which can
be unexpected for new contributors. This updates all tests to use JUnit 5 and
the new JUnit 5 annotations.

This also renames MapCSSStyleSourceFilterTest to MapCSSStyleSourceFilterPerformanceTest
to match the naming convention for performance tests and fixes some lint issues.

This was tested by running all tests individually and together.

  • Property svn:eol-style set to native
File size: 19.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.preferences.imagery;
3
4import static java.util.Collections.singletonList;
5import static java.util.Collections.synchronizedList;
6import static java.util.Collections.synchronizedMap;
7import static java.util.stream.Collectors.toList;
8import static org.junit.jupiter.api.Assertions.assertFalse;
9import static org.junit.jupiter.api.Assertions.assertTrue;
10import static org.junit.jupiter.api.Assumptions.assumeTrue;
11
12import java.io.ByteArrayInputStream;
13import java.io.IOException;
14import java.net.URL;
15import java.nio.charset.StandardCharsets;
16import java.util.ArrayList;
17import java.util.List;
18import java.util.Locale;
19import java.util.Map;
20import java.util.Objects;
21import java.util.Optional;
22import java.util.TreeMap;
23import java.util.concurrent.TimeUnit;
24import java.util.stream.Stream;
25
26import javax.imageio.ImageIO;
27
28import org.apache.commons.jcs3.access.CacheAccess;
29import org.junit.jupiter.api.AfterAll;
30import org.junit.jupiter.api.BeforeAll;
31import org.junit.jupiter.api.Disabled;
32import org.junit.jupiter.api.Timeout;
33import org.junit.jupiter.api.parallel.Execution;
34import org.junit.jupiter.api.parallel.ExecutionMode;
35import org.junit.jupiter.params.ParameterizedTest;
36import org.junit.jupiter.params.provider.Arguments;
37import org.junit.jupiter.params.provider.MethodSource;
38import org.openstreetmap.gui.jmapviewer.Coordinate;
39import org.openstreetmap.gui.jmapviewer.FeatureAdapter;
40import org.openstreetmap.gui.jmapviewer.TileXY;
41import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
42import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTileSource;
43import org.openstreetmap.gui.jmapviewer.tilesources.BingAerialTileSource;
44import org.openstreetmap.gui.jmapviewer.tilesources.ScanexTileSource;
45import org.openstreetmap.josm.TestUtils;
46import org.openstreetmap.josm.actions.AddImageryLayerAction;
47import org.openstreetmap.josm.actions.AddImageryLayerAction.LayerSelection;
48import org.openstreetmap.josm.data.Bounds;
49import org.openstreetmap.josm.data.coor.LatLon;
50import org.openstreetmap.josm.data.imagery.CoordinateConversion;
51import org.openstreetmap.josm.data.imagery.ImageryInfo;
52import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds;
53import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
54import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
55import org.openstreetmap.josm.data.imagery.JosmTemplatedTMSTileSource;
56import org.openstreetmap.josm.data.imagery.LayerDetails;
57import org.openstreetmap.josm.data.imagery.Shape;
58import org.openstreetmap.josm.data.imagery.TMSCachedTileLoaderJob;
59import org.openstreetmap.josm.data.imagery.TemplatedWMSTileSource;
60import org.openstreetmap.josm.data.imagery.TileJobOptions;
61import org.openstreetmap.josm.data.imagery.WMTSTileSource;
62import org.openstreetmap.josm.data.imagery.WMTSTileSource.WMTSGetCapabilitiesException;
63import org.openstreetmap.josm.data.projection.Projection;
64import org.openstreetmap.josm.data.projection.ProjectionRegistry;
65import org.openstreetmap.josm.data.projection.Projections;
66import org.openstreetmap.josm.io.imagery.ApiKeyProvider;
67import org.openstreetmap.josm.io.imagery.WMSImagery.WMSGetCapabilitiesException;
68import org.openstreetmap.josm.testutils.annotations.HTTPS;
69import org.openstreetmap.josm.testutils.annotations.I18n;
70import org.openstreetmap.josm.testutils.annotations.ProjectionNadGrids;
71import org.openstreetmap.josm.tools.HttpClient;
72import org.openstreetmap.josm.tools.HttpClient.Response;
73import org.openstreetmap.josm.tools.Logging;
74import org.openstreetmap.josm.tools.Utils;
75
76/**
77 * Integration tests of {@link ImageryPreference} class.
78 */
79@HTTPS
80@I18n
81@org.openstreetmap.josm.testutils.annotations.Projection
82@ProjectionNadGrids
83@Timeout(value = 40, unit = TimeUnit.MINUTES)
84public class ImageryPreferenceTestIT {
85
86 private static final String ERROR_SEP = " -> ";
87 private static final LatLon GREENWICH = new LatLon(51.47810, -0.00170);
88 private static final int DEFAULT_ZOOM = 12;
89
90 /** Entry to test */
91 private final Map<String, Map<ImageryInfo, List<String>>> errors = synchronizedMap(new TreeMap<>());
92 private final Map<String, Map<ImageryInfo, List<String>>> ignoredErrors = synchronizedMap(new TreeMap<>());
93 private static final Map<String, byte[]> workingURLs = synchronizedMap(new TreeMap<>());
94
95 private static TMSCachedTileLoaderJob helper;
96 private static final List<String> errorsToIgnore = new ArrayList<>();
97 private static final List<String> notIgnoredErrors = new ArrayList<>();
98
99 /**
100 * Setup test
101 * @throws IOException in case of I/O error
102 */
103 @BeforeAll
104 public static void beforeClass() throws IOException {
105 FeatureAdapter.registerApiKeyAdapter(ApiKeyProvider::retrieveApiKey);
106 helper = new TMSCachedTileLoaderJob(null, null, new CacheAccess<>(null), new TileJobOptions(0, 0, null, 0), null);
107 errorsToIgnore.addAll(TestUtils.getIgnoredErrorMessages(ImageryPreferenceTestIT.class));
108 notIgnoredErrors.addAll(errorsToIgnore);
109 }
110
111 /**
112 * Cleanup test
113 */
114 @AfterAll
115 public static void afterClass() {
116 for (String e : notIgnoredErrors) {
117 Logging.warn("Ignore line unused: " + e);
118 }
119 }
120
121 /**
122 * Returns list of imagery entries to test.
123 * @return list of imagery entries to test
124 */
125 public static List<Arguments> data() {
126 ImageryLayerInfo.instance.load(false);
127 return ImageryLayerInfo.instance.getDefaultLayers()
128 .stream()
129 //.filter(i -> "OGDLidarZH-DOM-2017".equals(i.getId())) // enable to test one specific entry
130 .map(i -> Arguments.of(i.getCountryCode().isEmpty() ? i.getId() : i.getCountryCode() + '-' + i.getId(), i))
131 .collect(toList());
132 }
133
134 private boolean addError(ImageryInfo info, String error) {
135 String errorMsg = error.replace('\n', ' ');
136 notIgnoredErrors.remove(errorMsg);
137 return addError(isIgnoredError(errorMsg) ? ignoredErrors : errors, info, errorMsg);
138 }
139
140 private static boolean isIgnoredError(String errorMsg) {
141 int idx = errorMsg.lastIndexOf(ERROR_SEP);
142 return isIgnoredSubstring(errorMsg) || (idx > -1 && isIgnoredSubstring(errorMsg.substring(idx + ERROR_SEP.length())));
143 }
144
145 private static boolean isIgnoredSubstring(String substring) {
146 return errorsToIgnore.parallelStream().anyMatch(substring::contains);
147 }
148
149 private static boolean addError(Map<String, Map<ImageryInfo, List<String>>> map, ImageryInfo info, String errorMsg) {
150 return map.computeIfAbsent(info.getCountryCode(), x -> synchronizedMap(new TreeMap<>()))
151 .computeIfAbsent(info, x -> synchronizedList(new ArrayList<>()))
152 .add(errorMsg);
153 }
154
155 private Optional<byte[]> checkUrl(ImageryInfo info, String url) {
156 if (!Utils.isEmpty(url)) {
157 if (workingURLs.containsKey(url)) {
158 return Optional.of(workingURLs.get(url));
159 }
160 try {
161 Response response = HttpClient.create(new URL(url))
162 .setHeaders(info.getCustomHttpHeaders())
163 .setConnectTimeout((int) TimeUnit.MINUTES.toMillis(1))
164 .setReadTimeout((int) TimeUnit.MINUTES.toMillis(5))
165 .connect();
166 if (response.getResponseCode() >= 400) {
167 addError(info, url + " -> HTTP " + response.getResponseCode());
168 } else if (response.getResponseCode() >= 300) {
169 Logging.warn(url + " -> HTTP " + response.getResponseCode());
170 }
171 try {
172 byte[] data = Utils.readBytesFromStream(response.getContent());
173 if (response.getResponseCode() < 300) {
174 workingURLs.put(url, data);
175 }
176 return Optional.of(data);
177 } catch (IOException e) {
178 if (response.getResponseCode() < 300) {
179 addError(info, url + ERROR_SEP + e);
180 }
181 } finally {
182 response.disconnect();
183 }
184 } catch (IOException e) {
185 addError(info, url + ERROR_SEP + e);
186 }
187 }
188 return Optional.empty();
189 }
190
191 private void checkLinkUrl(ImageryInfo info, String url) {
192 checkUrl(info, url).filter(x -> x.length == 0).ifPresent(x -> addError(info, url + " -> returned empty contents"));
193 }
194
195 private List<String> checkTileUrls(ImageryInfo info, List<AbstractTileSource> tileSources, ICoordinate center, int zoom)
196 throws IOException {
197 List<String> errors = new ArrayList<>();
198 for (AbstractTileSource tileSource : tileSources) {
199 String error = checkTileUrl(info, tileSource, center, zoom);
200 if (!error.isEmpty()) {
201 errors.add(error);
202 if (error.contains("This request used more time than allowed and has been forcefully stopped")) {
203 // No need to kill both remote server and our Jenkins instance... On error of this kind is enough to see there's a problem
204 break;
205 }
206 }
207 }
208 return errors;
209 }
210
211 private String checkTileUrl(ImageryInfo info, AbstractTileSource tileSource, ICoordinate center, int zoom)
212 throws IOException {
213 TileXY xy = tileSource.latLonToTileXY(center, zoom);
214 for (int i = 0; i < 3; i++) {
215 try {
216 String url = tileSource.getTileUrl(zoom, xy.getXIndex(), xy.getYIndex());
217 Optional<byte[]> optional = checkUrl(info, url);
218 String error = "";
219 if (optional.isPresent()) {
220 byte[] data = optional.get();
221 try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) {
222 if (ImageIO.read(bais) == null) {
223 error = addImageError(info, url, data, zoom, "did not return an image");
224 }
225 } catch (IOException e) {
226 error = addImageError(info, url, data, zoom, e.toString());
227 Logging.trace(e);
228 }
229 }
230 return error;
231 } catch (IOException e) {
232 // Try up to three times max to allow Bing source to initialize itself
233 // and avoid random network errors
234 Logging.trace(e);
235 if (i == 2) {
236 throw e;
237 }
238 try {
239 Thread.sleep(500);
240 } catch (InterruptedException ex) {
241 Logging.warn(ex);
242 }
243 }
244 }
245 return "";
246 }
247
248 private static String zoomMarker(int zoom) {
249 return " -> zoom " + zoom + ERROR_SEP;
250 }
251
252 private String addImageError(ImageryInfo info, String url, byte[] data, int zoom, String defaultMessage) {
253 // Check if we have received an error message
254 String error = helper.detectErrorMessage(new String(data, StandardCharsets.UTF_8));
255 String errorMsg = url + zoomMarker(zoom) + (error != null ? error.split("\\n", -1)[0] : defaultMessage);
256 addError(info, errorMsg);
257 return errorMsg;
258 }
259
260 private static LatLon getPointInShape(Shape shape) {
261 final Coordinate p1 = shape.getPoints().get(0);
262 final Bounds bounds = new Bounds(p1.getLat(), p1.getLon(), p1.getLat(), p1.getLon());
263 shape.getPoints().forEach(p -> bounds.extend(p.getLat(), p.getLon()));
264
265 final double w = bounds.getWidth();
266 final double h = bounds.getHeight();
267
268 final double x2 = bounds.getMinLon() + (w / 2.0);
269 final double y2 = bounds.getMinLat() + (h / 2.0);
270
271 final LatLon center = new LatLon(y2, x2);
272
273 // check to see if center is inside shape
274 if (shape.contains(center)) {
275 return center;
276 }
277
278 // if center position (C) is not inside shape, try naively some other positions as follows:
279 final double x1 = bounds.getMinLon() + (.25 * w);
280 final double x3 = bounds.getMinLon() + (.75 * w);
281 final double y1 = bounds.getMinLat() + (.25 * h);
282 final double y3 = bounds.getMinLat() + (.75 * h);
283 // +-----------+
284 // | 5 1 6 |
285 // | 4 C 2 |
286 // | 8 3 7 |
287 // +-----------+
288 return Stream.of(
289 new LatLon(y1, x2),
290 new LatLon(y2, x3),
291 new LatLon(y3, x2),
292 new LatLon(y2, x1),
293 new LatLon(y1, x1),
294 new LatLon(y1, x3),
295 new LatLon(y3, x3),
296 new LatLon(y3, x1)
297 ).filter(shape::contains).findFirst().orElse(center);
298 }
299
300 private static LatLon getCenter(ImageryBounds bounds) {
301 List<Shape> shapes = bounds.getShapes();
302 return !Utils.isEmpty(shapes) ? getPointInShape(shapes.get(0)) : bounds.getCenter();
303 }
304
305 private void checkEntry(ImageryInfo info) {
306 Logging.info("Checking "+ info);
307
308 if (info.getAttributionImageRaw() != null && info.getAttributionImage() == null) {
309 addError(info, "Can't fetch attribution image: " + info.getAttributionImageRaw());
310 }
311
312 checkLinkUrl(info, info.getAttributionImageURL());
313 checkLinkUrl(info, info.getAttributionLinkURL());
314 String eula = info.getEulaAcceptanceRequired();
315 if (eula != null) {
316 checkLinkUrl(info, eula.replaceAll("\\{lang\\}", ""));
317 }
318 checkLinkUrl(info, info.getPrivacyPolicyURL());
319 checkLinkUrl(info, info.getPermissionReferenceURL());
320 checkLinkUrl(info, info.getTermsOfUseURL());
321 if (info.getUrl().contains("{time}")) {
322 info.setDate("2020-01-01T00:00:00Z/2020-01-02T00:00:00Z");
323 }
324
325 try {
326 ImageryBounds bounds = info.getBounds();
327 // Some imagery sources do not define tiles at (0,0). So pickup Greenwich Royal Observatory for global sources
328 ICoordinate center = CoordinateConversion.llToCoor(bounds != null ? getCenter(bounds) : GREENWICH);
329 List<AbstractTileSource> tileSources = getTileSources(info);
330 // test min zoom and try to detect the correct value in case of error
331 int maxZoom = info.getMaxZoom() > 0 ? Math.min(DEFAULT_ZOOM, info.getMaxZoom()) : DEFAULT_ZOOM;
332 for (int zoom = info.getMinZoom(); zoom < maxZoom; zoom++) {
333 if (!isZoomError(checkTileUrls(info, tileSources, center, zoom))) {
334 break;
335 }
336 }
337 // checking max zoom for real is complex, see https://josm.openstreetmap.de/ticket/16073#comment:27
338 if (info.getMaxZoom() > 0 && info.getImageryType() != ImageryType.SCANEX) {
339 checkTileUrls(info, tileSources, center, Utils.clamp(DEFAULT_ZOOM, info.getMinZoom() + 1, info.getMaxZoom()));
340 }
341 } catch (IOException | RuntimeException | WMSGetCapabilitiesException e) {
342 addError(info, info.getUrl() + ERROR_SEP + e);
343 }
344
345 for (ImageryInfo mirror : info.getMirrors()) {
346 checkEntry(mirror);
347 }
348 }
349
350 private static boolean isZoomError(List<String> errors) {
351 return errors.stream().anyMatch(error -> {
352 String[] parts = error.split(ERROR_SEP, -1);
353 String lastPart = parts.length > 0 ? parts[parts.length - 1].toLowerCase(Locale.ENGLISH) : "";
354 return lastPart.contains("bbox")
355 || lastPart.contains("bounding box");
356 });
357 }
358
359 private static List<Projection> getProjections(ImageryInfo info) {
360 List<Projection> projs = info.getServerProjections().stream()
361 .map(Projections::getProjectionByCode).filter(Objects::nonNull).collect(toList());
362 return projs.isEmpty() ? singletonList(ProjectionRegistry.getProjection()) : projs;
363 }
364
365 private List<AbstractTileSource> getTileSources(ImageryInfo info)
366 throws IOException, WMSGetCapabilitiesException {
367 switch (info.getImageryType()) {
368 case BING:
369 return singletonList(new BingAerialTileSource(info));
370 case SCANEX:
371 return singletonList(new ScanexTileSource(info));
372 case TMS:
373 return singletonList(new JosmTemplatedTMSTileSource(info));
374 case WMS_ENDPOINT:
375 return getWmsTileSources(convertWmsEndpointToWms(info));
376 case WMS:
377 return getWmsTileSources(info);
378 case WMTS:
379 return getWmtsTileSources(info);
380 default:
381 throw new UnsupportedOperationException(info.toString());
382 }
383 }
384
385 private static List<AbstractTileSource> getWmsTileSources(ImageryInfo info) {
386 return getProjections(info).stream().map(proj -> new TemplatedWMSTileSource(info, proj)).collect(toList());
387 }
388
389 private List<AbstractTileSource> getWmtsTileSources(ImageryInfo info) {
390 return getProjections(info).stream().map(proj -> {
391 try {
392 return new WMTSTileSource(info, proj);
393 } catch (IOException | WMTSGetCapabilitiesException e) {
394 addError(info, info.getUrl() + ERROR_SEP + e);
395 return null;
396 }
397 }).filter(Objects::nonNull).collect(toList());
398 }
399
400 private static ImageryInfo convertWmsEndpointToWms(ImageryInfo info) throws IOException, WMSGetCapabilitiesException {
401 return Optional.ofNullable(AddImageryLayerAction.getWMSLayerInfo(
402 info, wms -> new LayerSelection(firstLeafLayer(wms.getLayers()), wms.getPreferredFormat(), true)))
403 .orElseThrow(() -> new IllegalStateException("Unable to convert WMS_ENDPOINT to WMS"));
404 }
405
406 private static List<LayerDetails> firstLeafLayer(List<LayerDetails> layers) {
407 for (LayerDetails layer : layers) {
408 boolean hasNoChildren = layer.getChildren().isEmpty();
409 if (hasNoChildren && layer.getName() != null) {
410 return singletonList(layer);
411 } else if (!hasNoChildren) {
412 return firstLeafLayer(layer.getChildren());
413 }
414 }
415 throw new IllegalArgumentException("Unable to find a valid WMS layer");
416 }
417
418 private static String format(String id, Map<String, Map<ImageryInfo, List<String>>> map) {
419 // #16567 - Shouldn't be necessary to print id if Ant worked properly
420 // See https://josm.openstreetmap.de/ticket/16567#comment:53
421 // See https://bz.apache.org/bugzilla/show_bug.cgi?id=64564
422 // See https://github.com/apache/ant/pull/121
423 return id + " => " + map.toString().replaceAll("\\}, ", "\n\\}, ").replaceAll(", ImageryInfo\\{", "\n ,ImageryInfo\\{");
424 }
425
426 /**
427 * Test that available imagery entry is valid.
428 *
429 * @param id The id of the imagery info to show as the test name
430 * @param info The imagery info to test
431 */
432 @Execution(ExecutionMode.CONCURRENT)
433 @ParameterizedTest(name = "{0}")
434 @MethodSource("data")
435 @Disabled("Takes a long time")
436 void testImageryEntryValidity(String id, ImageryInfo info) {
437 checkEntry(info);
438 assertTrue(errors.isEmpty(), format(id, errors));
439 assertFalse(workingURLs.isEmpty());
440 assumeTrue(ignoredErrors.isEmpty(), format(id, ignoredErrors));
441 }
442}
Note: See TracBrowser for help on using the repository browser.