View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.juneau.commons.collections;
18  
19  import static org.junit.jupiter.api.Assertions.*;
20  import static org.apache.juneau.commons.collections.CacheMode.*;
21  import static org.apache.juneau.junit.bct.BctAssertions.*;
22  
23  import java.util.concurrent.atomic.*;
24  
25  import org.apache.juneau.*;
26  import org.junit.jupiter.api.*;
27  
28  class Cache5_Test extends TestBase {
29  
30  	//====================================================================================================
31  	// a - Basic cache operations
32  	//====================================================================================================
33  
34  	@Test
35  	void a01_defaultSupplier_basic() {
36  		var callCount = new AtomicInteger();
37  		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
38  			.supplier((k1, k2, k3, k4, k5) -> {
39  				callCount.incrementAndGet();
40  				return k1 + ":" + k2 + ":" + k3 + ":" + k4 + ":" + k5;
41  			})
42  			.build();
43  
44  		var result1 = x.get("en", "US", "west", "formal", 1);
45  		var result2 = x.get("en", "US", "west", "formal", 1); // Cache hit
46  
47  		assertEquals("en:US:west:formal:1", result1);
48  		assertEquals("en:US:west:formal:1", result2);
49  		assertEquals(1, callCount.get());
50  		assertEquals(1, x.getCacheHits());
51  	}
52  
53  	@Test
54  	void a02_overrideSupplier() {
55  		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
56  			.supplier((k1, k2, k3, k4, k5) -> "DEFAULT")
57  			.build();
58  
59  		var result = x.get("en", "US", "west", "formal", 1, () -> "OVERRIDE");
60  
61  		assertEquals("OVERRIDE", result);
62  	}
63  
64  	@Test
65  	void a03_nullKeys() {
66  		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
67  			.supplier((k1, k2, k3, k4, k5) -> "value-" + k1 + "-" + k2 + "-" + k3 + "-" + k4 + "-" + k5)
68  			.build();
69  
70  		// Null keys are now allowed
71  		assertEquals("value-null-US-west-formal-1", x.get(null, "US", "west", "formal", 1));
72  		assertEquals("value-en-null-west-formal-1", x.get("en", null, "west", "formal", 1));
73  		assertEquals("value-en-US-null-formal-1", x.get("en", "US", null, "formal", 1));
74  		assertEquals("value-en-US-west-null-1", x.get("en", "US", "west", null, 1));
75  		assertEquals("value-en-US-west-formal-null", x.get("en", "US", "west", "formal", null));
76  		assertEquals("value-null-null-null-null-null", x.get(null, null, null, null, null));
77  
78  		// Cached values should be returned on subsequent calls
79  		assertEquals("value-null-US-west-formal-1", x.get(null, "US", "west", "formal", 1));
80  		assertEquals("value-en-null-west-formal-1", x.get("en", null, "west", "formal", 1));
81  	}
82  
83  	@Test
84  	void a04_disableCaching() {
85  		var callCount = new AtomicInteger();
86  		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
87  			.cacheMode(NONE)
88  			.supplier((k1, k2, k3, k4, k5) -> {
89  				callCount.incrementAndGet();
90  				return "value";
91  			})
92  			.build();
93  
94  		x.get("en", "US", "west", "formal", 1);
95  		x.get("en", "US", "west", "formal", 1);
96  
97  		assertEquals(2, callCount.get()); // Called twice
98  		assertEmpty(x);
99  	}
100 
101 	@Test
102 	void a04b_weakMode_basicCaching() {
103 		var callCount = new AtomicInteger();
104 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
105 			.cacheMode(WEAK)
106 			.supplier((k1, k2, k3, k4, k5) -> {
107 				callCount.incrementAndGet();
108 				return k1 + ":" + k2 + ":" + k3 + ":" + k4 + ":" + k5;
109 			})
110 			.build();
111 
112 		// First call - cache miss
113 		var result1 = x.get("en", "US", "west", "formal", 1);
114 
115 		// Second call - cache hit
116 		var result2 = x.get("en", "US", "west", "formal", 1);
117 
118 		assertEquals("en:US:west:formal:1", result1);
119 		assertEquals("en:US:west:formal:1", result2);
120 		assertSame(result1, result2);
121 		assertEquals(1, callCount.get()); // Supplier only called once
122 		assertSize(1, x);
123 		assertEquals(1, x.getCacheHits());
124 	}
125 
126 	@Test
127 	void a04c_weakMode_multipleKeys() {
128 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
129 			.cacheMode(WEAK)
130 			.supplier((k1, k2, k3, k4, k5) -> k1 + ":" + k2 + ":" + k3 + ":" + k4 + ":" + k5)
131 			.build();
132 
133 		x.get("en", "US", "west", "formal", 1);
134 		x.get("fr", "FR", "north", "formal", 2);
135 		x.get("de", "DE", "south", "formal", 3);
136 
137 		assertSize(3, x);
138 		assertEquals(0, x.getCacheHits());
139 
140 		// Verify all cached
141 		assertEquals("en:US:west:formal:1", x.get("en", "US", "west", "formal", 1));
142 		assertEquals("fr:FR:north:formal:2", x.get("fr", "FR", "north", "formal", 2));
143 		assertEquals("de:DE:south:formal:3", x.get("de", "DE", "south", "formal", 3));
144 		assertEquals(3, x.getCacheHits());
145 	}
146 
147 	@Test
148 	void a04d_weakMode_clear() {
149 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
150 			.cacheMode(WEAK)
151 			.supplier((k1, k2, k3, k4, k5) -> "value")
152 			.build();
153 
154 		x.get("en", "US", "west", "formal", 1);
155 		x.get("fr", "FR", "north", "formal", 2);
156 		assertSize(2, x);
157 
158 		x.clear();
159 		assertEmpty(x);
160 	}
161 
162 	@Test
163 	void a04e_weakMode_maxSize() {
164 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
165 			.cacheMode(WEAK)
166 			.maxSize(2)
167 			.supplier((k1, k2, k3, k4, k5) -> "value")
168 			.build();
169 
170 		x.get("en", "US", "west", "formal", 1);
171 		x.get("fr", "FR", "north", "formal", 2);
172 		assertSize(2, x);
173 
174 		// 3rd item doesn't trigger eviction yet
175 		x.get("de", "DE", "south", "formal", 3);
176 		assertSize(3, x);
177 
178 		// 4th item triggers eviction
179 		x.get("es", "ES", "east", "formal", 4);
180 		assertSize(1, x);
181 	}
182 
183 	@Test
184 	void a04f_weakMethod_basicCaching() {
185 		// Test the weak() convenience method
186 		var callCount = new AtomicInteger();
187 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
188 			.weak()
189 			.supplier((k1, k2, k3, k4, k5) -> {
190 				callCount.incrementAndGet();
191 				return k1 + ":" + k2 + ":" + k3 + ":" + k4 + ":" + k5;
192 			})
193 			.build();
194 
195 		// First call - cache miss
196 		var result1 = x.get("en", "US", "west", "formal", 1);
197 
198 		// Second call - cache hit
199 		var result2 = x.get("en", "US", "west", "formal", 1);
200 
201 		assertEquals("en:US:west:formal:1", result1);
202 		assertEquals("en:US:west:formal:1", result2);
203 		assertSame(result1, result2);
204 		assertEquals(1, callCount.get()); // Supplier only called once
205 		assertSize(1, x);
206 		assertEquals(1, x.getCacheHits());
207 	}
208 
209 	@Test
210 	void a04g_weakMethod_chaining() {
211 		// Test that weak() can be chained with other builder methods
212 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
213 			.weak()
214 			.maxSize(100)
215 			.supplier((k1, k2, k3, k4, k5) -> k1 + ":" + k2 + ":" + k3 + ":" + k4 + ":" + k5)
216 			.build();
217 
218 		var result = x.get("en", "US", "west", "formal", 1);
219 		assertEquals("en:US:west:formal:1", result);
220 		assertSize(1, x);
221 	}
222 
223 	@Test
224 	void a05_maxSize() {
225 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
226 			.maxSize(2)
227 			.supplier((k1, k2, k3, k4, k5) -> "value")
228 			.build();
229 
230 		x.get("en", "US", "west", "formal", 1);
231 		x.get("fr", "FR", "north", "formal", 2);
232 		assertSize(2, x);
233 
234 		x.get("de", "DE", "south", "formal", 3); // Doesn't exceed yet
235 		assertSize(3, x);
236 
237 		x.get("es", "ES", "east", "formal", 4); // Exceeds max (3 > 2), triggers clear
238 		assertSize(1, x); // Cleared
239 	}
240 
241 	@Test
242 	void a06_cacheHitsTracking() {
243 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
244 			.supplier((k1, k2, k3, k4, k5) -> "value")
245 			.build();
246 
247 		x.get("en", "US", "west", "formal", 1); // Miss
248 		assertEquals(0, x.getCacheHits());
249 
250 		x.get("en", "US", "west", "formal", 1); // Hit
251 		x.get("en", "US", "west", "formal", 1); // Hit
252 		assertEquals(2, x.getCacheHits());
253 	}
254 
255 	//====================================================================================================
256 	// b - put(), isEmpty(), containsKey()
257 	//====================================================================================================
258 
259 	@Test
260 	void b01_put() {
261 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class).build();
262 		var previous = x.put("en", "US", "west", "formal", 1, "value");
263 		assertNull(previous);
264 		assertEquals("value", x.get("en", "US", "west", "formal", 1, () -> "should not be called"));
265 	}
266 
267 	@Test
268 	void b01b_put_withNullValue() {
269 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class).build();
270 		x.put("en", "US", "west", "formal", 1, "value1");
271 		var previous = x.put("en", "US", "west", "formal", 1, null);
272 		assertEquals("value1", previous);
273 		assertFalse(x.containsKey("en", "US", "west", "formal", 1));
274 	}
275 
276 	@Test
277 	void b02_isEmpty() {
278 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class).build();
279 		assertTrue(x.isEmpty());
280 		x.put("en", "US", "west", "formal", 1, "value");
281 		assertFalse(x.isEmpty());
282 		x.clear();
283 		assertTrue(x.isEmpty());
284 	}
285 
286 	@Test
287 	void b03_containsKey() {
288 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class).build();
289 		assertFalse(x.containsKey("en", "US", "west", "formal", 1));
290 		x.put("en", "US", "west", "formal", 1, "value");
291 		assertTrue(x.containsKey("en", "US", "west", "formal", 1));
292 	}
293 
294 	//====================================================================================================
295 	// c - create(), disableCaching(), null values
296 	//====================================================================================================
297 
298 	@Test
299 	void c01_create() {
300 		var x = Cache5.<String, String, String, String, Integer, String>create()
301 			.supplier((k1, k2, k3, k4, k5) -> k1 + ":" + k2 + ":" + k3 + ":" + k4 + ":" + k5)
302 			.build();
303 		var result = x.get("en", "US", "west", "formal", 1);
304 		assertEquals("en:US:west:formal:1", result);
305 	}
306 
307 	@Test
308 	void c02_disableCaching() {
309 		var callCount = new AtomicInteger();
310 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
311 			.cacheMode(NONE)
312 			.supplier((k1, k2, k3, k4, k5) -> {
313 				callCount.incrementAndGet();
314 				return "value";
315 			})
316 			.build();
317 		x.get("en", "US", "west", "formal", 1);
318 		x.get("en", "US", "west", "formal", 1);
319 		assertEquals(2, callCount.get());
320 		assertTrue(x.isEmpty());
321 	}
322 
323 	@Test
324 	void c03_nullValue_notCached() {
325 		var callCount = new AtomicInteger();
326 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class).build();
327 		x.get("en", "US", "west", "formal", 1, () -> {
328 			callCount.incrementAndGet();
329 			return null;
330 		});
331 		x.get("en", "US", "west", "formal", 1, () -> {
332 			callCount.incrementAndGet();
333 			return null;
334 		});
335 		assertEquals(2, callCount.get());
336 		assertTrue(x.isEmpty());
337 	}
338 
339 	//====================================================================================================
340 	// d - remove() and containsValue()
341 	//====================================================================================================
342 
343 	@Test
344 	void d01_remove() {
345 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class).build();
346 		x.put("en", "US", "west", "formal", 1, "value1");
347 		var removed = x.remove("en", "US", "west", "formal", 1);
348 		assertEquals("value1", removed);
349 		assertFalse(x.containsKey("en", "US", "west", "formal", 1));
350 	}
351 
352 	@Test
353 	void d02_containsValue() {
354 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class).build();
355 		x.put("en", "US", "west", "formal", 1, "value1");
356 		assertTrue(x.containsValue("value1"));
357 		assertFalse(x.containsValue("value2"));
358 	}
359 
360 	@Test
361 	void d03_containsValue_nullValue() {
362 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class).build();
363 		// Null values can't be cached, so containsValue(null) should return false
364 		x.get("en", "US", "west", "formal", 1, () -> null);
365 		assertFalse(x.containsValue(null));
366 		// Also test with empty cache
367 		var x2 = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class).build();
368 		assertFalse(x2.containsValue(null));
369 	}
370 
371 	//====================================================================================================
372 	// e - logOnExit() builder methods
373 	//====================================================================================================
374 
375 	@Test
376 	void e01_logOnExit_withStringId() {
377 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
378 			.logOnExit("TestCache5")
379 			.supplier((k1, k2, k3, k4, k5) -> k1 + ":" + k2 + ":" + k3 + ":" + k4 + ":" + k5)
380 			.build();
381 		x.get("en", "US", "west", "formal", 1);
382 		assertSize(1, x);
383 	}
384 
385 	@Test
386 	void e02_logOnExit_withBoolean() {
387 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
388 			.logOnExit(true, "MyCache5")
389 			.supplier((k1, k2, k3, k4, k5) -> k1 + ":" + k2 + ":" + k3 + ":" + k4 + ":" + k5)
390 			.build();
391 		x.get("en", "US", "west", "formal", 1);
392 		assertSize(1, x);
393 	}
394 
395 	//====================================================================================================
396 	// f - Thread-local cache mode
397 	//====================================================================================================
398 
399 	@Test
400 	void f01_threadLocal_basicCaching() {
401 		var callCount = new AtomicInteger();
402 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
403 			.threadLocal()
404 			.supplier((k1, k2, k3, k4, k5) -> {
405 				callCount.incrementAndGet();
406 				return k1 + ":" + k2 + ":" + k3 + ":" + k4 + ":" + k5;
407 			})
408 			.build();
409 
410 		// First call - cache miss
411 		var result1 = x.get("en", "US", "west", "formal", 1);
412 
413 		// Second call - cache hit
414 		var result2 = x.get("en", "US", "west", "formal", 1);
415 
416 		assertEquals("en:US:west:formal:1", result1);
417 		assertEquals("en:US:west:formal:1", result2);
418 		assertSame(result1, result2);
419 		assertEquals(1, callCount.get()); // Supplier only called once
420 		assertSize(1, x);
421 		assertEquals(1, x.getCacheHits());
422 	}
423 
424 	@Test
425 	void f02_threadLocal_eachThreadHasOwnCache() throws Exception {
426 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
427 			.threadLocal()
428 			.build();
429 		var executor = java.util.concurrent.Executors.newFixedThreadPool(2);
430 		var threadValues = new java.util.concurrent.ConcurrentHashMap<Thread, String>();
431 
432 		// Each thread caches ("en", "US", "west", "formal", 1) with its own value
433 		var future1 = java.util.concurrent.CompletableFuture.runAsync(() -> {
434 			var value = x.get("en", "US", "west", "formal", 1, () -> "thread1-value");
435 			threadValues.put(Thread.currentThread(), value);
436 		}, executor);
437 
438 		var future2 = java.util.concurrent.CompletableFuture.runAsync(() -> {
439 			var value = x.get("en", "US", "west", "formal", 1, () -> "thread2-value");
440 			threadValues.put(Thread.currentThread(), value);
441 		}, executor);
442 
443 		java.util.concurrent.CompletableFuture.allOf(future1, future2).get(5, java.util.concurrent.TimeUnit.SECONDS);
444 
445 		// Verify both threads cached their own values
446 		assertEquals(2, threadValues.size());
447 		assertTrue(threadValues.containsValue("thread1-value"));
448 		assertTrue(threadValues.containsValue("thread2-value"));
449 
450 		executor.shutdown();
451 	}
452 
453 	@Test
454 	void f03_threadLocal_multipleKeys() {
455 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
456 			.threadLocal()
457 			.supplier((k1, k2, k3, k4, k5) -> k1 + ":" + k2 + ":" + k3 + ":" + k4 + ":" + k5)
458 			.build();
459 
460 		x.get("en", "US", "west", "formal", 1);
461 		x.get("fr", "FR", "east", "informal", 2);
462 		x.get("de", "DE", "north", "formal", 3);
463 
464 		assertSize(3, x);
465 		assertEquals(0, x.getCacheHits());
466 
467 		// Verify all cached
468 		assertEquals("en:US:west:formal:1", x.get("en", "US", "west", "formal", 1));
469 		assertEquals("fr:FR:east:informal:2", x.get("fr", "FR", "east", "informal", 2));
470 		assertEquals("de:DE:north:formal:3", x.get("de", "DE", "north", "formal", 3));
471 		assertEquals(3, x.getCacheHits());
472 	}
473 
474 	@Test
475 	void f04_threadLocal_clear() {
476 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
477 			.threadLocal()
478 			.supplier((k1, k2, k3, k4, k5) -> "value")
479 			.build();
480 
481 		x.get("en", "US", "west", "formal", 1);
482 		x.get("fr", "FR", "east", "informal", 2);
483 		assertSize(2, x);
484 
485 		x.clear();
486 		assertEmpty(x);
487 	}
488 
489 	@Test
490 	void f05_threadLocal_maxSize() {
491 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
492 			.threadLocal()
493 			.maxSize(2)
494 			.supplier((k1, k2, k3, k4, k5) -> "value")
495 			.build();
496 
497 		x.get("en", "US", "west", "formal", 1);
498 		x.get("fr", "FR", "east", "informal", 2);
499 		assertSize(2, x);
500 
501 		// 3rd item doesn't trigger eviction yet
502 		x.get("de", "DE", "north", "formal", 3);
503 		assertSize(3, x);
504 
505 		// 4th item triggers eviction
506 		x.get("es", "ES", "south", "informal", 4);
507 		assertSize(1, x);
508 	}
509 
510 	//====================================================================================================
511 	// g - Thread-local + weak mode combination
512 	//====================================================================================================
513 
514 	@Test
515 	void g01_threadLocal_weakMode_basicCaching() {
516 		var callCount = new AtomicInteger();
517 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
518 			.threadLocal()
519 			.cacheMode(WEAK)
520 			.supplier((k1, k2, k3, k4, k5) -> {
521 				callCount.incrementAndGet();
522 				return k1 + ":" + k2 + ":" + k3 + ":" + k4 + ":" + k5;
523 			})
524 			.build();
525 
526 		// First call - cache miss
527 		var result1 = x.get("en", "US", "west", "formal", 1);
528 
529 		// Second call - cache hit
530 		var result2 = x.get("en", "US", "west", "formal", 1);
531 
532 		assertEquals("en:US:west:formal:1", result1);
533 		assertEquals("en:US:west:formal:1", result2);
534 		assertSame(result1, result2);
535 		assertEquals(1, callCount.get()); // Supplier only called once
536 		assertSize(1, x);
537 		assertEquals(1, x.getCacheHits());
538 	}
539 
540 	@Test
541 	void g02_threadLocal_weakMode_eachThreadHasOwnCache() throws Exception {
542 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
543 			.threadLocal()
544 			.cacheMode(WEAK)
545 			.build();
546 		var executor = java.util.concurrent.Executors.newFixedThreadPool(2);
547 		var threadValues = new java.util.concurrent.ConcurrentHashMap<Thread, String>();
548 
549 		// Each thread caches ("en", "US", "west", "formal", 1) with its own value
550 		var future1 = java.util.concurrent.CompletableFuture.runAsync(() -> {
551 			var value = x.get("en", "US", "west", "formal", 1, () -> "thread1-value");
552 			threadValues.put(Thread.currentThread(), value);
553 		}, executor);
554 
555 		var future2 = java.util.concurrent.CompletableFuture.runAsync(() -> {
556 			var value = x.get("en", "US", "west", "formal", 1, () -> "thread2-value");
557 			threadValues.put(Thread.currentThread(), value);
558 		}, executor);
559 
560 		java.util.concurrent.CompletableFuture.allOf(future1, future2).get(5, java.util.concurrent.TimeUnit.SECONDS);
561 
562 		// Verify both threads cached their own values
563 		assertEquals(2, threadValues.size());
564 		assertTrue(threadValues.containsValue("thread1-value"));
565 		assertTrue(threadValues.containsValue("thread2-value"));
566 
567 		executor.shutdown();
568 	}
569 
570 	@Test
571 	void g03_threadLocal_weakMode_clear() {
572 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
573 			.threadLocal()
574 			.cacheMode(WEAK)
575 			.supplier((k1, k2, k3, k4, k5) -> "value")
576 			.build();
577 
578 		x.get("en", "US", "west", "formal", 1);
579 		x.get("fr", "FR", "east", "informal", 2);
580 		assertSize(2, x);
581 
582 		x.clear();
583 		assertEmpty(x);
584 	}
585 
586 	@Test
587 	void g04_threadLocal_weakMode_maxSize() {
588 		var x = Cache5.of(String.class, String.class, String.class, String.class, Integer.class, String.class)
589 			.threadLocal()
590 			.cacheMode(WEAK)
591 			.maxSize(2)
592 			.supplier((k1, k2, k3, k4, k5) -> "value")
593 			.build();
594 
595 		x.get("en", "US", "west", "formal", 1);
596 		x.get("fr", "FR", "east", "informal", 2);
597 		assertSize(2, x);
598 
599 		// 3rd item doesn't trigger eviction yet
600 		x.get("de", "DE", "north", "formal", 3);
601 		assertSize(3, x);
602 
603 		// 4th item triggers eviction
604 		x.get("es", "ES", "south", "informal", 4);
605 		assertSize(1, x);
606 	}
607 }
608