source: josm/trunk/test/unit/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJobTest.java@ 13742

Last change on this file since 13742 was 13742, checked in by wiktorn, 6 years ago

Checkstyle fixes

  • Property svn:eol-style set to native
File size: 21.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.cache;
3
4import static org.junit.Assert.assertArrayEquals;
5import static org.junit.Assert.assertEquals;
6import static org.junit.Assert.assertFalse;
7import static org.junit.Assert.assertTrue;
8
9import java.io.IOException;
10import java.net.MalformedURLException;
11import java.net.URL;
12import java.nio.charset.StandardCharsets;
13import java.util.concurrent.TimeUnit;
14
15import org.apache.commons.jcs.access.behavior.ICacheAccess;
16import org.apache.commons.jcs.engine.behavior.ICacheElement;
17import org.junit.Before;
18import org.junit.Rule;
19import org.junit.Test;
20import org.openstreetmap.josm.TestUtils;
21import org.openstreetmap.josm.data.cache.ICachedLoaderListener.LoadResult;
22import org.openstreetmap.josm.data.imagery.TileJobOptions;
23import org.openstreetmap.josm.testutils.JOSMTestRules;
24import org.openstreetmap.josm.tools.Logging;
25
26import com.github.tomakehurst.wiremock.client.WireMock;
27import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
28import com.github.tomakehurst.wiremock.junit.WireMockRule;
29import com.github.tomakehurst.wiremock.matching.UrlPattern;
30
31import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
32
33/**
34 * Unit tests for class {@link JCSCachedTileLoaderJob}.
35 */
36public class JCSCachedTileLoaderJobTest {
37
38 /**
39 * mocked tile server
40 */
41 @Rule
42 public WireMockRule tileServer = new WireMockRule(WireMockConfiguration.options()
43 .dynamicPort());
44
45 private static class TestCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, CacheEntry> {
46 private String url;
47 private String key;
48
49 TestCachedTileLoaderJob(String url, String key) {
50 this(url, key, (int) TimeUnit.DAYS.toSeconds(1));
51 }
52
53 TestCachedTileLoaderJob(String url, String key, int minimumExpiry) {
54 super(getCache(), new TileJobOptions(30000, 30000, null, minimumExpiry));
55
56 this.url = url;
57 this.key = key;
58 }
59
60 @Override
61 public String getCacheKey() {
62 return key;
63 }
64
65 @Override
66 public URL getUrl() {
67 try {
68 return new URL(url);
69 } catch (MalformedURLException e) {
70 throw new RuntimeException(e);
71 }
72 }
73
74 @Override
75 protected CacheEntry createCacheEntry(byte[] content) {
76 return new CacheEntry(content);
77 }
78 }
79
80 private static class Listener implements ICachedLoaderListener {
81 private CacheEntryAttributes attributes;
82 private boolean ready;
83 private LoadResult result;
84 private byte[] data;
85
86 @Override
87 public synchronized void loadingFinished(CacheEntry data, CacheEntryAttributes attributes, LoadResult result) {
88 this.attributes = attributes;
89 this.ready = true;
90 this.result = result;
91 if (data != null) {
92 this.data = data.content;
93 }
94 this.notifyAll();
95 }
96 }
97
98 /**
99 * Setup test.
100 */
101 @Rule
102 @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
103 public JOSMTestRules test = new JOSMTestRules().preferences();
104
105 /**
106 * Always clear cache before tests
107 * @throws Exception when clearing fails
108 */
109 @Before
110 public void clearCache() throws Exception {
111 getCache().clear();
112 }
113
114 /**
115 * Test status codes
116 * @throws InterruptedException in case of thread interruption
117 * @throws IOException in case of I/O error
118 */
119 @Test
120 public void testStatusCodes() throws IOException, InterruptedException {
121 doTestStatusCode(200);
122 // can't test for 3xx, as httpstat.us redirects finally to 200 page
123 doTestStatusCode(401);
124 doTestStatusCode(402);
125 doTestStatusCode(403);
126 doTestStatusCode(404);
127 doTestStatusCode(405);
128 doTestStatusCode(500);
129 doTestStatusCode(501);
130 doTestStatusCode(502);
131 }
132
133 /**
134 * Test unknown host
135 * @throws IOException in case of I/O error
136 */
137 @Test
138 public void testUnknownHost() throws IOException {
139 String key = "key_unknown_host";
140 TestCachedTileLoaderJob job = new TestCachedTileLoaderJob("http://unkownhost.unkownhost/unkown", key);
141 Listener listener = submitJob(job);
142 assertEquals(LoadResult.FAILURE, listener.result); // because response will be cached, and that is checked below
143 assertEquals("java.net.UnknownHostException: unkownhost.unkownhost", listener.attributes.getErrorMessage());
144
145 ICacheAccess<String, CacheEntry> cache = getCache();
146 CacheEntry e = new CacheEntry(new byte[]{0, 1, 2, 3});
147 CacheEntryAttributes attributes = new CacheEntryAttributes();
148 attributes.setExpirationTime(2);
149 cache.put(key, e, attributes);
150
151 job = new TestCachedTileLoaderJob("http://unkownhost.unkownhost/unkown", key);
152 listener = submitJob(job);
153 assertEquals(LoadResult.SUCCESS, listener.result);
154 assertFalse(job.isCacheElementValid());
155 }
156
157 private void doTestStatusCode(int responseCode) throws IOException {
158 TestCachedTileLoaderJob job = getStatusLoaderJob(responseCode);
159 Listener listener = submitJob(job);
160 assertEquals(responseCode, listener.attributes.getResponseCode());
161 }
162
163 private Listener submitJob(TestCachedTileLoaderJob job) throws IOException {
164 return submitJob(job, true);
165 }
166
167 private Listener submitJob(TestCachedTileLoaderJob job, boolean force) throws IOException {
168 Listener listener = new Listener();
169 job.submit(listener, force);
170 synchronized (listener) {
171 while (!listener.ready) {
172 try {
173 listener.wait();
174 } catch (InterruptedException e) {
175 // do nothing, wait
176 Logging.trace(e);
177 }
178 }
179 }
180 return listener;
181 }
182
183 /**
184 * That no request is made when entry is in cache and force == false
185 * @throws IOException exception
186 */
187 @Test
188 public void testNoRequestMadeWhenEntryInCache() throws IOException {
189 ICacheAccess<String, CacheEntry> cache = getCache();
190 long expires = TimeUnit.DAYS.toMillis(1);
191 long testStart = System.currentTimeMillis();
192 cache.put("test",
193 new CacheEntry("cached entry".getBytes(StandardCharsets.UTF_8)),
194 createEntryAttributes(expires, 200, testStart, "eTag")
195 );
196 createHeadGetStub(WireMock.urlEqualTo("/test"), expires, testStart, "eTag", "mock entry");
197
198 TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test");
199 Listener listener = submitJob(job, false);
200 tileServer.verify(0, WireMock.getRequestedFor(WireMock.anyUrl()));
201 assertArrayEquals("cached entry".getBytes(StandardCharsets.UTF_8), listener.data);
202 }
203
204 /**
205 * that request is made, when object is in cache, but force mode is used
206 * @throws IOException exception
207 */
208 @Test
209 public void testRequestMadeWhenEntryInCacheAndForce() throws IOException {
210 ICacheAccess<String, CacheEntry> cache = getCache();
211 long expires = TimeUnit.DAYS.toMillis(1);
212 long testStart = System.currentTimeMillis();
213 cache.put("test",
214 new CacheEntry("cached dummy".getBytes(StandardCharsets.UTF_8)),
215 createEntryAttributes(expires, 200, testStart + expires, "eTag")
216 );
217 createHeadGetStub(WireMock.urlEqualTo("/test"), expires, testStart, "eTag", "mock entry");
218
219 TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test");
220 Listener listener = submitJob(job, true);
221 tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
222 assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
223 }
224
225 /**
226 * Mock returns no cache-control / expires headers
227 * Expire time should be set to DEFAULT_EXPIRE_TIME
228 * @throws IOException exception
229 */
230 @Test
231 public void testSettingMinimumExpiryWhenNoExpires() throws IOException {
232 long testStart = System.currentTimeMillis();
233 tileServer.stubFor(
234 WireMock.get(WireMock.urlEqualTo("/test"))
235 .willReturn(WireMock.aResponse()
236 .withBody("mock entry")
237 )
238 );
239
240 TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test");
241 Listener listener = submitJob(job, false);
242 tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
243
244 assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - testStart) + " which is not larger than " +
245 JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME + " (DEFAULT_EXPIRE_TIME)",
246 listener.attributes.getExpirationTime() >= testStart + JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME);
247
248 assertTrue("Cache entry expiration is " +
249 (listener.attributes.getExpirationTime() - System.currentTimeMillis()) +
250 " which is not less than " +
251 JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME + " (DEFAULT_EXPIRE_TIME)",
252 listener.attributes.getExpirationTime() <= System.currentTimeMillis() + JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME
253 );
254
255 assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
256 }
257
258 /**
259 * Mock returns expires headers, but Cache-Control
260 * Expire time should be set to max-age
261 * @throws IOException exception
262 */
263 @Test
264 public void testSettingExpireByMaxAge() throws IOException {
265 long testStart = System.currentTimeMillis();
266 long expires = TimeUnit.DAYS.toSeconds(1);
267 tileServer.stubFor(
268 WireMock.get(WireMock.urlEqualTo("/test"))
269 .willReturn(WireMock.aResponse()
270 .withHeader("Cache-control", "max-age=" + expires)
271 .withBody("mock entry")
272 )
273 );
274
275 TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test");
276 Listener listener = submitJob(job, false);
277 tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
278
279 assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - testStart) + " which is not larger than " +
280 TimeUnit.SECONDS.toMillis(expires) + " (max-age)",
281 listener.attributes.getExpirationTime() >= testStart + TimeUnit.SECONDS.toMillis(expires));
282
283 assertTrue("Cache entry expiration is " +
284 (listener.attributes.getExpirationTime() - System.currentTimeMillis()) +
285 " which is not less than " +
286 TimeUnit.SECONDS.toMillis(expires) + " (max-age)",
287 listener.attributes.getExpirationTime() <= System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expires)
288 );
289
290 assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
291 }
292
293 /**
294 * mock returns expiration: JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME / 10
295 * minimum expire time: JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME / 2
296 * @throws IOException exception
297 */
298 @Test
299 public void testSettingMinimumExpiryByMinimumExpiryTimeLessThanDefault() throws IOException {
300 long testStart = System.currentTimeMillis();
301 int minimumExpiryTimeSeconds = (int) (JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME / 2);
302
303 createHeadGetStub(WireMock.urlEqualTo("/test"), (JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME / 10), testStart, "eTag", "mock entry");
304
305 TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test", minimumExpiryTimeSeconds);
306 Listener listener = submitJob(job, false);
307 tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
308 assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
309
310
311 assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - testStart) + " which is not larger than " +
312 TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds) + " (minimumExpireTime)",
313 listener.attributes.getExpirationTime() >= testStart + TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds));
314
315 assertTrue("Cache entry expiration is " +
316 (listener.attributes.getExpirationTime() - System.currentTimeMillis()) +
317 " which is not less than " +
318 TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds) + " (minimumExpireTime)",
319 listener.attributes.getExpirationTime() <= System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds)
320 );
321 }
322
323 /**
324 * mock returns expiration: JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME / 10
325 * minimum expire time: JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME * 2
326 * @throws IOException exception
327 */
328
329 @Test
330 public void testSettingMinimumExpiryByMinimumExpiryTimeGreaterThanDefault() throws IOException {
331 long testStart = System.currentTimeMillis();
332 int minimumExpiryTimeSeconds = (int) (JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME * 2);
333
334 createHeadGetStub(WireMock.urlEqualTo("/test"), (JCSCachedTileLoaderJob.DEFAULT_EXPIRE_TIME / 10), testStart, "eTag", "mock entry");
335
336 TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test", minimumExpiryTimeSeconds);
337 Listener listener = submitJob(job, false);
338 tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
339 assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
340
341
342 assertTrue("Cache entry expiration is " + (listener.attributes.getExpirationTime() - testStart) + " which is not larger than " +
343 TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds) + " (minimumExpireTime)",
344 listener.attributes.getExpirationTime() >= testStart + TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds));
345
346 assertTrue("Cache entry expiration is " +
347 (listener.attributes.getExpirationTime() - System.currentTimeMillis()) +
348 " which is not less than " +
349 TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds) + " (minimumExpireTime)",
350 listener.attributes.getExpirationTime() <= System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(minimumExpiryTimeSeconds)
351 );
352 }
353
354 /**
355 * Check if verifying cache entries using HEAD requests work properly
356 * @throws IOException exception
357 */
358 @Test
359 public void testCheckUsingHead() throws IOException {
360 ICacheAccess<String, CacheEntry> cache = getCache();
361 long expires = TimeUnit.DAYS.toMillis(1);
362 long testStart = System.currentTimeMillis();
363 cache.put("test",
364 new CacheEntry("cached dummy".getBytes(StandardCharsets.UTF_8)),
365 createEntryAttributes(-1 * expires, 200, testStart, "eTag--gzip") // Jetty adds --gzip to etags when compressing output
366 );
367
368 tileServer.stubFor(
369 WireMock.get(WireMock.urlEqualTo("/test"))
370 .willReturn(WireMock.aResponse()
371 .withHeader("Expires", TestUtils.getHTTPDate(testStart + expires))
372 .withHeader("Last-Modified", Long.toString(testStart))
373 .withHeader("ETag", "eTag") // Jetty adds "--gzip" suffix for compressed content
374 .withBody("mock entry")
375 )
376 );
377 tileServer.stubFor(
378 WireMock.head(WireMock.urlEqualTo("/test"))
379 .willReturn(WireMock.aResponse()
380 .withHeader("Expires", TestUtils.getHTTPDate(testStart + expires))
381 .withHeader("Last-Modified", Long.toString(testStart))
382 .withHeader("ETag", "eTag--gzip") // but doesn't add to uncompressed
383 )
384 );
385
386 TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test");
387 Listener listener = submitJob(job, false); // cache entry is expired, no need to force refetch
388 tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
389 assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
390
391 // cache entry should be retrieved from cache
392 listener = submitJob(job, false);
393 tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
394 assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
395
396 // invalidate entry in cache
397 ICacheElement<String, CacheEntry> cacheEntry = cache.getCacheElement("test");
398 CacheEntryAttributes attributes = (CacheEntryAttributes) cacheEntry.getElementAttributes();
399 attributes.setExpirationTime(testStart - TimeUnit.DAYS.toMillis(1));
400 cache.put("test", cacheEntry.getVal(), attributes);
401
402 // because cache entry is invalid - HEAD request shall be made
403 tileServer.verify(0, WireMock.headRequestedFor(WireMock.urlEqualTo("/test"))); // no head requests were made until now
404 listener = submitJob(job, false);
405 tileServer.verify(1, WireMock.headRequestedFor(WireMock.urlEqualTo("/test"))); // verify head requests were made
406 tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test"))); // verify no more get requests were made
407 assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
408 assertTrue(listener.attributes.getExpirationTime() >= testStart + expires);
409
410 // cache entry should be retrieved from cache
411 listener = submitJob(job, false); // cache entry is expired, no need to force refetch
412 tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
413 tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
414 assertArrayEquals("mock entry".getBytes(StandardCharsets.UTF_8), listener.data);
415 }
416
417 /**
418 * Check if server returns 304 - it will update cache attributes and not ask again for it
419 * @throws IOException exception
420 */
421 @Test
422 public void testCheckUsing304() throws IOException {
423 ICacheAccess<String, CacheEntry> cache = getCache();
424 long expires = TimeUnit.DAYS.toMillis(1);
425 long testStart = System.currentTimeMillis();
426 cache.put("test",
427 new CacheEntry("cached dummy".getBytes(StandardCharsets.UTF_8)),
428 createEntryAttributes(-1 * expires, 200, testStart, "eTag")
429 );
430
431 tileServer.stubFor(
432 WireMock.get(WireMock.urlEqualTo("/test"))
433 .willReturn(WireMock.status(304)
434 .withHeader("Expires", TestUtils.getHTTPDate(testStart + expires))
435 .withHeader("Last-Modified", Long.toString(testStart))
436 .withHeader("ETag", "eTag")
437 )
438 );
439
440 TestCachedTileLoaderJob job = new TestCachedTileLoaderJob(tileServer.url("/test"), "test");
441 Listener listener = submitJob(job, false);
442 tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test")));
443 assertArrayEquals("cached dummy".getBytes(StandardCharsets.UTF_8), listener.data);
444 assertTrue(testStart + expires <= listener.attributes.getExpirationTime());
445 listener = submitJob(job, false);
446 tileServer.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/test"))); // no more requests were made
447 }
448
449 private void createHeadGetStub(UrlPattern url, long expires, long lastModified, String eTag, String body) {
450 tileServer.stubFor(
451 WireMock.get(url)
452 .willReturn(WireMock.aResponse()
453 .withHeader("Expires", TestUtils.getHTTPDate(lastModified + expires))
454 .withHeader("Last-Modified", Long.toString(lastModified))
455 .withHeader("ETag", eTag)
456 .withBody(body)
457 )
458 );
459 tileServer.stubFor(
460 WireMock.head(url)
461 .willReturn(WireMock.aResponse()
462 .withHeader("Expires", TestUtils.getHTTPDate(lastModified + expires))
463 .withHeader("Last-Modified", Long.toString(lastModified))
464 .withHeader("ETag", eTag)
465 )
466 );
467 }
468
469 private CacheEntryAttributes createEntryAttributes(long maxAge, int responseCode, String eTag) {
470 long validTo = maxAge + System.currentTimeMillis();
471 return createEntryAttributes(maxAge, responseCode, validTo, eTag);
472 }
473
474 private CacheEntryAttributes createEntryAttributes(long expirationTime, int responseCode, long lastModification, String eTag) {
475 CacheEntryAttributes entryAttributes = new CacheEntryAttributes();
476 entryAttributes.setExpirationTime(lastModification + expirationTime);
477 entryAttributes.setResponseCode(responseCode);
478 entryAttributes.setLastModification(lastModification);
479 entryAttributes.setEtag(eTag);
480 return entryAttributes;
481 }
482
483 private static TestCachedTileLoaderJob getStatusLoaderJob(int responseCode) {
484 return new TestCachedTileLoaderJob("http://httpstat.us/" + responseCode, "key_" + responseCode);
485 }
486
487 private static ICacheAccess<String, CacheEntry> getCache() {
488 return JCSCacheManager.getCache("test");
489 }
490}
Note: See TracBrowser for help on using the repository browser.