source: josm/trunk/test/unit/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJobTest.java@ 14311

Last change on this file since 14311 was 14311, checked in by Don-vip, 6 years ago

see #16824 - better display of WMS errors

  • Property svn:eol-style set to native
File size: 13.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.imagery;
3
4import static org.junit.Assert.assertArrayEquals;
5import static org.junit.Assert.assertEquals;
6import static org.junit.Assert.assertTrue;
7
8import java.io.IOException;
9import java.net.MalformedURLException;
10import java.net.URL;
11import java.nio.charset.StandardCharsets;
12import java.util.concurrent.Executors;
13import java.util.concurrent.ThreadPoolExecutor;
14import java.util.concurrent.TimeUnit;
15import java.util.regex.Matcher;
16import java.util.regex.Pattern;
17
18import org.apache.commons.jcs.access.behavior.ICacheAccess;
19import org.junit.Before;
20import org.junit.Rule;
21import org.junit.Test;
22import org.openstreetmap.gui.jmapviewer.Tile;
23import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
24import org.openstreetmap.gui.jmapviewer.tilesources.TMSTileSource;
25import org.openstreetmap.josm.TestUtils;
26import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
27import org.openstreetmap.josm.data.cache.CacheEntryAttributes;
28import org.openstreetmap.josm.data.cache.JCSCacheManager;
29import org.openstreetmap.josm.testutils.JOSMTestRules;
30import org.openstreetmap.josm.tools.Logging;
31import org.openstreetmap.josm.tools.Utils;
32
33import com.github.tomakehurst.wiremock.client.WireMock;
34import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
35import com.github.tomakehurst.wiremock.junit.WireMockRule;
36
37import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
38
39/**
40 * Unit tests for class {@link TMSCachedTileLoaderJob}.
41 */
42public class TMSCachedTileLoaderJobTest {
43
44 /**
45 * Setup tests
46 */
47 @Rule
48 @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
49 public JOSMTestRules test = new JOSMTestRules().preferences();
50
51 /**
52 * mocked tile server
53 */
54 @Rule
55 public WireMockRule tileServer = new WireMockRule(WireMockConfiguration.options()
56 .dynamicPort());
57
58 @Before
59 public void clearCache() throws Exception {
60 getCache().clear();
61 }
62
63 private static ICacheAccess<String, BufferedImageCacheEntry> getCache() {
64 return JCSCacheManager.getCache("test");
65 }
66
67 private static class TestCachedTileLoaderJob extends TMSCachedTileLoaderJob {
68 private String url;
69 private String key;
70
71 TestCachedTileLoaderJob(TileLoaderListener listener, Tile tile, String key) throws IOException {
72 this(listener, tile, key, (int) TimeUnit.DAYS.toSeconds(1));
73 }
74
75 TestCachedTileLoaderJob(TileLoaderListener listener, Tile tile, String key, int minimumExpiry) throws IOException {
76 super(listener, tile, getCache(), new TileJobOptions(30000, 30000, null, minimumExpiry),
77 (ThreadPoolExecutor) Executors.newFixedThreadPool(1));
78
79 this.url = tile.getUrl();
80 this.key = key;
81 }
82
83 @Override
84 public URL getUrl() {
85 try {
86 return new URL(url);
87 } catch (MalformedURLException e) {
88 throw new RuntimeException(e);
89 }
90 }
91
92 @Override
93 protected BufferedImageCacheEntry createCacheEntry(byte[] content) {
94 return new BufferedImageCacheEntry(content);
95 }
96
97 public CacheEntryAttributes getAttributes() {
98 return attributes;
99 }
100
101 @Override
102 public boolean isObjectLoadable() {
103 // use implementation from grand parent, to avoid calling getImage on dummy data
104 if (cacheData == null) {
105 return false;
106 }
107 return cacheData.getContent().length > 0;
108 }
109 }
110
111 private static class Listener implements TileLoaderListener {
112 private CacheEntryAttributes attributes;
113 private boolean ready;
114 private byte[] data;
115
116 @Override
117 public synchronized void tileLoadingFinished(Tile tile, boolean success) {
118 ready = true;
119 this.notifyAll();
120 }
121 }
122
123 private static class MockTile extends Tile {
124 MockTile(String url) {
125 super(new MockTileSource(url), 0, 0, 0);
126 }
127 }
128
129 private static class MockTileSource extends TMSTileSource {
130 private final String url;
131
132 MockTileSource(String url) {
133 super(new ImageryInfo("mock"));
134 this.url = url;
135 }
136
137 @Override
138 public String getTileUrl(int zoom, int tilex, int tiley) throws IOException {
139 return url;
140 }
141 }
142
143 /**
144 * Tests that {@code TMSCachedTileLoaderJob#SERVICE_EXCEPTION_PATTERN} is correct.
145 */
146 @Test
147 public void testServiceExceptionPattern() {
148 testServiceException("missing parameters ['version', 'format']",
149 "<?xml version=\"1.0\"?>\n" +
150 "<!DOCTYPE ServiceExceptionReport SYSTEM \"http://schemas.opengis.net/wms/1.1.1/exception_1_1_1.dtd\">\n" +
151 "<ServiceExceptionReport version=\"1.1.1\">\n" +
152 " <ServiceException>missing parameters ['version', 'format']</ServiceException>\n" +
153 "</ServiceExceptionReport>");
154 testServiceException("Parameter 'layers' contains unacceptable layer names.",
155 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>\r\n" +
156 "<!DOCTYPE ServiceExceptionReport SYSTEM \"http://schemas.opengis.net/wms/1.1.1/exception_1_1_1.dtd\">\r\n" +
157 "<ServiceExceptionReport version=\"1.1.1\">\r\n" +
158 " <ServiceException code=\"LayerNotDefined\">\r\n" +
159 "Parameter 'layers' contains unacceptable layer names.\r\n" +
160 " </ServiceException>\r\n" +
161 "</ServiceExceptionReport>\r\n" +
162 "");
163 }
164
165 /**
166 * Tests that {@code TMSCachedTileLoaderJob#CDATA_PATTERN} is correct.
167 */
168 @Test
169 public void testCdataPattern() {
170 testCdata("received unsuitable wms request: no <grid> with suitable srs found for layer capitais",
171 "<![CDATA[\r\n" +
172 "received unsuitable wms request: no <grid> with suitable srs found for layer capitais\r\n" +
173 "]]>");
174 }
175
176 private static void testServiceException(String expected, String xml) {
177 test(TMSCachedTileLoaderJob.SERVICE_EXCEPTION_PATTERN, expected, xml);
178 }
179
180 private static void testCdata(String expected, String xml) {
181 test(TMSCachedTileLoaderJob.CDATA_PATTERN, expected, xml);
182 }
183
184 private static void test(Pattern pattern, String expected, String xml) {
185 Matcher m = pattern.matcher(xml);
186 assertTrue(xml, m.matches());
187 assertEquals(expected, Utils.strip(m.group(1)));
188 }
189
190 private TestCachedTileLoaderJob submitJob(MockTile tile, String key, boolean force) throws IOException {
191 return submitJob(tile, key, 0, force);
192 }
193
194 private TestCachedTileLoaderJob submitJob(MockTile tile, String key, int minimumExpiry, boolean force) throws IOException {
195 Listener listener = new Listener();
196 TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(listener, tile, key, minimumExpiry);
197 job.submit(force);
198 synchronized (listener) {
199 while (!listener.ready) {
200 try {
201 listener.wait();
202 } catch (InterruptedException e) {
203 // do nothing, wait
204 Logging.trace(e);
205 }
206 }
207 }
208 return job;
209 }
210
211 /**
212 * When tile server doesn't return any Expires/Cache-Control headers, expire should be at least MINIMUM_EXPIRES
213 * @throws IOException exception
214 */
215 @Test
216 public void testNoCacheHeaders() throws IOException {
217 long testStart = System.currentTimeMillis();
218 tileServer.stubFor(
219 WireMock.get(WireMock.urlEqualTo("/test"))
220 .willReturn(WireMock.aResponse()
221 .withBody("mock entry")
222 )
223 );
224
225 TestCachedTileLoaderJob job = submitJob(new MockTile(tileServer.url("/test")), "test", false);
226 assertExpirationAtLeast(testStart + TMSCachedTileLoaderJob.MINIMUM_EXPIRES.get(), job);
227 assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
228 job = submitJob(new MockTile(tileServer.url("/test")), "test", false); // submit another job for the same tile
229 // only one request to tile server should be made, second should come from cache
230 tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
231 assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
232 }
233
234 /**
235 * When tile server doesn't return any Expires/Cache-Control headers, expire should be at least minimumExpires parameter
236 * @throws IOException exception
237 */
238 @Test
239 public void testNoCacheHeadersMinimumExpires() throws IOException {
240 noCacheHeadersMinimumExpires((int) TimeUnit.MILLISECONDS.toSeconds(TMSCachedTileLoaderJob.MINIMUM_EXPIRES.get() * 2));
241 }
242
243 /**
244 * When tile server doesn't return any Expires/Cache-Control headers, expire should be at least minimumExpires parameter,
245 * which is larger than MAXIMUM_EXPIRES
246 * @throws IOException exception
247 */
248
249 @Test
250 public void testNoCacheHeadersMinimumExpiresLargerThanMaximum() throws IOException {
251 noCacheHeadersMinimumExpires((int) TimeUnit.MILLISECONDS.toSeconds(TMSCachedTileLoaderJob.MAXIMUM_EXPIRES.get() * 2));
252 }
253
254 private void noCacheHeadersMinimumExpires(int minimumExpires) throws IOException {
255 long testStart = System.currentTimeMillis();
256 tileServer.stubFor(
257 WireMock.get(WireMock.urlEqualTo("/test"))
258 .willReturn(WireMock.aResponse()
259 .withBody("mock entry")
260 )
261 );
262 TestCachedTileLoaderJob job = submitJob(new MockTile(tileServer.url("/test")), "test", minimumExpires, false);
263 assertExpirationAtLeast(testStart + minimumExpires, job);
264 assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
265 job = submitJob(new MockTile(tileServer.url("/test")), "test", false); // submit another job for the same tile
266 // only one request to tile server should be made, second should come from cache
267 tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
268 assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
269 }
270
271 /**
272 * When tile server returns Expires header shorter than MINIMUM_EXPIRES, we should cache if for at least MINIMUM_EXPIRES
273 * @throws IOException exception
274 */
275 @Test
276 public void testShortExpire() throws IOException {
277 long testStart = System.currentTimeMillis();
278 long expires = TMSCachedTileLoaderJob.MINIMUM_EXPIRES.get() / 2;
279 tileServer.stubFor(
280 WireMock.get(WireMock.urlEqualTo("/test"))
281 .willReturn(WireMock.aResponse()
282 .withHeader("Expires", TestUtils.getHTTPDate(testStart + expires))
283 .withBody("mock entry")
284 )
285 );
286 TestCachedTileLoaderJob job = submitJob(new MockTile(tileServer.url("/test")), "test", false);
287 assertExpirationAtLeast(testStart + TMSCachedTileLoaderJob.MINIMUM_EXPIRES.get(), job);
288 assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
289 job = submitJob(new MockTile(tileServer.url("/test")), "test", false); // submit another job for the same tile
290 // only one request to tile server should be made, second should come from cache
291 tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
292 assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
293 }
294
295 private void assertExpirationAtLeast(long duration, TestCachedTileLoaderJob job) {
296 assertTrue(
297 "Expiration time shorter by " +
298 -1 * (job.getAttributes().getExpirationTime() - duration) +
299 " than expected",
300 job.getAttributes().getExpirationTime() >= duration);
301 }
302
303 private void assertExpirationAtMost(long duration, TestCachedTileLoaderJob job) {
304 assertTrue(
305 "Expiration time longer by " +
306 (job.getAttributes().getExpirationTime() - duration) +
307 " than expected",
308 job.getAttributes().getExpirationTime() <= duration);
309 }
310
311 @Test
312 public void testLongExpire() throws IOException {
313 long testStart = System.currentTimeMillis();
314 long expires = TMSCachedTileLoaderJob.MAXIMUM_EXPIRES.get() * 2;
315 tileServer.stubFor(
316 WireMock.get(WireMock.urlEqualTo("/test"))
317 .willReturn(WireMock.aResponse()
318 .withHeader("Expires", TestUtils.getHTTPDate(testStart + expires))
319 .withBody("mock entry")
320 )
321 );
322 TestCachedTileLoaderJob job = submitJob(new MockTile(tileServer.url("/test")), "test", false);
323 // give 1 second margin
324 assertExpirationAtMost(testStart + TMSCachedTileLoaderJob.MAXIMUM_EXPIRES.get() + TimeUnit.SECONDS.toMillis(1), job);
325
326 assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
327 job = submitJob(new MockTile(tileServer.url("/test")), "test", false); // submit another job for the same tile
328 // only one request to tile server should be made, second should come from cache
329 tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
330 assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), job.get().getContent());
331 }
332
333}
Note: See TracBrowser for help on using the repository browser.