/** * Copyright (c) 2012-2017, jcabi.com * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: 1) Redistributions of source code must retain the above * copyright notice, this list of conditions and the following * disclaimer. 2) Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. 3) Neither the name of the jcabi.com nor * the names of its contributors may be used to endorse or promote * products derived from this software without specific prior written * permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.jcabi.aspects; import java.security.SecureRandom; import java.util.Random; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; import org.junit.Test; /** * Test case for {@link Cacheable} annotation and its implementation. * @author Yegor Bugayenko (yegor@tpc2.com) * @version $Id: fdcd961b2402e7b90400934292a5a56df825203b $ * @checkstyle ClassDataAbstractionCoupling (500 lines) */ @SuppressWarnings({ "PMD.TooManyMethods", "PMD.DoNotUseThreads" }) public final class CacheableTest { /** * Random. * @checkstyle ConstantUsageCheck (3 lines) */ private static final Random RANDOM = new SecureRandom(); /** * Cacheable can cache calls. * @throws Exception If something goes wrong */ @Test public void cachesSimpleCall() throws Exception { final CacheableTest.Foo foo = new CacheableTest.Foo(1L); final String first = foo.get().toString(); MatcherAssert.assertThat(first, Matchers.equalTo(foo.get().toString())); foo.flush(); MatcherAssert.assertThat( foo.get().toString(), Matchers.not(Matchers.equalTo(first)) ); } /** * Cacheable can Asynchronous update. * @throws Exception If something goes wrong */ @Test public void asyncUpdateCacheSimpleCall() throws Exception { final CacheableTest.Foo foo = new CacheableTest.Foo(1L); final String first = foo.asyncGet().toString(); MatcherAssert.assertThat( first, Matchers.equalTo(foo.asyncGet().toString()) ); TimeUnit.SECONDS.sleep(2); MatcherAssert.assertThat( first, Matchers.equalTo(foo.asyncGet().toString()) ); TimeUnit.SECONDS.sleep(2); MatcherAssert.assertThat( first, Matchers.not(Matchers.equalTo(foo.asyncGet().toString())) ); } /** * Cacheable can cache static calls. * @throws Exception If something goes wrong */ @Test public void cachesSimpleStaticCall() throws Exception { final String first = CacheableTest.Foo.staticGet(); MatcherAssert.assertThat( first, Matchers.equalTo(CacheableTest.Foo.staticGet()) ); CacheableTest.Foo.staticFlush(); MatcherAssert.assertThat( CacheableTest.Foo.staticGet(), Matchers.not(Matchers.equalTo(first)) ); } /** * Cacheable can clean cache after timeout. * @throws Exception If something goes wrong */ @Test public void cleansCacheWhenExpired() throws Exception { final CacheableTest.Foo foo = new CacheableTest.Foo(1L); final String first = foo.get().toString(); TimeUnit.SECONDS.sleep((long) Tv.FIVE); MatcherAssert.assertThat( foo.get().toString(), Matchers.not(Matchers.equalTo(first)) ); } /** * Cacheable can cache just once. * @throws Exception If something goes wrong */ @Test public void cachesJustOnceInParallelThreads() throws Exception { final CacheableTest.Foo foo = new CacheableTest.Foo(1L); final Thread never = new Thread( new Runnable() { @Override public void run() { foo.never(); } } ); never.start(); final Set<String> values = new ConcurrentSkipListSet<String>(); final int threads = Runtime.getRuntime().availableProcessors() << 1; final CountDownLatch start = new CountDownLatch(1); final CountDownLatch done = new CountDownLatch(threads); final Callable<Boolean> task = new Callable<Boolean>() { @Override public Boolean call() throws Exception { start.await(1L, TimeUnit.SECONDS); values.add(foo.get().toString()); done.countDown(); return true; } }; final ExecutorService executor = Executors.newFixedThreadPool(threads); try { for (int pos = 0; pos < threads; ++pos) { executor.submit(task); } start.countDown(); done.await((long) Tv.THIRTY, TimeUnit.SECONDS); MatcherAssert.assertThat(values.size(), Matchers.equalTo(1)); never.interrupt(); } finally { executor.shutdown(); } } /** * Cacheable can flush with a static trigger. * @throws Exception If something goes wrong */ @Test public void flushesWithStaticTrigger() throws Exception { final CacheableTest.Bar bar = new CacheableTest.Bar(); MatcherAssert.assertThat( bar.get(), Matchers.not(Matchers.equalTo(bar.get())) ); } /** * Dummy class, for tests above. */ private static final class Foo { /** * Encapsulated long. */ private final transient long number; /** * Public ctor. * @param num Number to encapsulate */ Foo(final long num) { this.number = num; } @Override public int hashCode() { return this.get().hashCode(); } @Override public boolean equals(final Object obj) { return obj == this; } @Override @Cacheable(forever = true) @Loggable(Loggable.DEBUG) public String toString() { return Long.toString(this.number); } /** * Download some text. * @return Downloaded text */ @Cacheable(lifetime = 1, unit = TimeUnit.SECONDS) @Loggable(Loggable.DEBUG) public CacheableTest.Foo get() { return new CacheableTest.Foo(CacheableTest.RANDOM.nextLong()); } /** * Download some text. * @return Downloaded text */ @Cacheable(lifetime = 1, unit = TimeUnit.SECONDS, asyncUpdate = true) @Loggable(Loggable.DEBUG) public CacheableTest.Foo asyncGet() { return new CacheableTest.Foo(CacheableTest.RANDOM.nextLong()); } /** * Sleep forever, to abuse caching system. * @return The same object */ @Cacheable(lifetime = 1, unit = TimeUnit.SECONDS) @Loggable(Loggable.DEBUG) public CacheableTest.Foo never() { try { TimeUnit.HOURS.sleep(1L); } catch (final InterruptedException ex) { throw new IllegalStateException(ex); } return this; } /** * Flush it. */ @Cacheable.FlushBefore public void flush() { // nothing to do } /** * Download some text. * @return Downloaded text */ @Cacheable(lifetime = 1, unit = TimeUnit.SECONDS) public static String staticGet() { return Long.toString(CacheableTest.RANDOM.nextLong()); } /** * Flush it. */ @Cacheable.FlushBefore public static void staticFlush() { // nothing to do } } /** * Dummy class, for tests above. */ public static final class Bar { /** * Get some number. * @return The number */ @Cacheable(before = Bar.class) public long get() { return CacheableTest.RANDOM.nextLong(); } /** * Flush before? * @return TRUE if flush is required */ public static boolean flushBefore() { return true; } } }