/*
* Copyright Terracotta, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ehcache.core;
import org.ehcache.Status;
import org.ehcache.core.spi.store.Store;
import org.ehcache.core.statistics.CacheOperationOutcomes;
import org.ehcache.core.spi.store.StoreAccessException;
import org.ehcache.core.spi.function.Function;
import org.ehcache.core.spi.function.NullaryFunction;
import org.ehcache.core.statistics.BulkOps;
import org.ehcache.spi.loaderwriter.BulkCacheWritingException;
import org.ehcache.spi.loaderwriter.CacheLoaderWriter;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Formatter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.ehcache.core.EhcacheBasicBulkUtil.KEY_SET_A;
import static org.ehcache.core.EhcacheBasicBulkUtil.KEY_SET_B;
import static org.ehcache.core.EhcacheBasicBulkUtil.KEY_SET_C;
import static org.ehcache.core.EhcacheBasicBulkUtil.copyWithout;
import static org.ehcache.core.EhcacheBasicBulkUtil.fanIn;
import static org.ehcache.core.EhcacheBasicBulkUtil.getEntryMap;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.everyItem;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isIn;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anySet;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
/**
* Provides testing of basic REMOVE_ALL operations on an {@code Ehcache}.
*
* @author Clifford W. Johnson
*/
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
public class EhcacheBasicRemoveAllTest extends EhcacheBasicCrudBase {
/**
* A Mockito {@code ArgumentCaptor} for the {@code Set} argument to the
* {@link Store#bulkCompute(Set, Function, NullaryFunction)
* Store.bulkCompute(Set, Function, NullaryFunction} method.
*/
@Captor
protected ArgumentCaptor<Set<String>> bulkComputeSetCaptor;
@Test
public void testRemoveAllNull() throws Exception {
final Map<String, String> originalStoreContent = getEntryMap(KEY_SET_A, KEY_SET_B);
final FakeStore fakeStore = new FakeStore(originalStoreContent);
this.store = spy(fakeStore);
final Ehcache<String, String> ehcache = this.getEhcache();
try {
ehcache.removeAll(null);
fail();
} catch (NullPointerException e) {
// Expected
}
assertThat(fakeStore.getEntryMap(), equalTo(originalStoreContent));
}
@Test
public void testRemoveAllNullKey() throws Exception {
final Map<String, String> originalStoreContent = getEntryMap(KEY_SET_A, KEY_SET_B);
final FakeStore fakeStore = new FakeStore(originalStoreContent);
this.store = spy(fakeStore);
final Set<String> keys = new LinkedHashSet<String>();
for (final String key : KEY_SET_A) {
keys.add(key);
if ("keyA2".equals(key)) {
keys.add(null);
}
}
final Ehcache<String, String> ehcache = this.getEhcache();
try {
ehcache.removeAll(keys);
fail();
} catch (NullPointerException e) {
// Expected
}
assertThat(fakeStore.getEntryMap(), equalTo(originalStoreContent));
}
/**
* Tests {@link EhcacheWithLoaderWriter#removeAll(Set)} for
* <ul>
* <li>empty request set</li>
* <li>populated {@code Store} (keys not relevant)</li>
* <li>no {@code CacheLoaderWriter}</li>
* </ul>
*/
@Test
public void testRemoveAllEmptyRequestNoWriter() throws Exception {
final Map<String, String> originalStoreContent = getEntryMap(KEY_SET_A, KEY_SET_B);
final FakeStore fakeStore = new FakeStore(originalStoreContent);
this.store = spy(fakeStore);
final Ehcache<String, String> ehcache = this.getEhcache();
ehcache.removeAll(Collections.<String>emptySet());
verify(this.store, never()).bulkCompute(eq(Collections.<String>emptySet()), getAnyEntryIterableFunction());
assertThat(fakeStore.getEntryMap(), equalTo(originalStoreContent));
verify(this.spiedResilienceStrategy, never()).removeAllFailure(eq(Collections.<String>emptySet()), any(StoreAccessException.class));
validateStats(ehcache, EnumSet.noneOf(CacheOperationOutcomes.RemoveOutcome.class));
validateStats(ehcache, EnumSet.of(CacheOperationOutcomes.RemoveAllOutcome.SUCCESS));
assertThat(ehcache.getBulkMethodEntries().get(BulkOps.REMOVE_ALL).intValue(), is(0));
}
/**
* Tests {@link EhcacheWithLoaderWriter#removeAll(Set)} for
* <ul>
* <li>non-empty request set</li>
* <li>populated {@code Store} - some keys overlap request</li>
* <li>no {@code CacheLoaderWriter}</li>
* </ul>
*/
@Test
public void testRemoveAllStoreSomeOverlapNoWriter() throws Exception {
final Map<String, String> originalStoreContent = getEntryMap(KEY_SET_A, KEY_SET_B);
final FakeStore fakeStore = new FakeStore(originalStoreContent);
this.store = spy(fakeStore);
final Ehcache<String, String> ehcache = this.getEhcache();
final Set<String> contentUpdates = fanIn(KEY_SET_A, KEY_SET_C);
ehcache.removeAll(contentUpdates);
verify(this.store, atLeast(1)).bulkCompute(this.bulkComputeSetCaptor.capture(), getAnyEntryIterableFunction());
assertThat(this.getBulkComputeArgs(), equalTo(contentUpdates));
assertThat(fakeStore.getEntryMap(), equalTo(copyWithout(originalStoreContent, contentUpdates)));
verifyZeroInteractions(this.spiedResilienceStrategy);
validateStats(ehcache, EnumSet.noneOf(CacheOperationOutcomes.RemoveOutcome.class));
validateStats(ehcache, EnumSet.of(CacheOperationOutcomes.RemoveAllOutcome.SUCCESS));
assertThat(ehcache.getBulkMethodEntries().get(BulkOps.REMOVE_ALL).intValue(), is(KEY_SET_A.size()));
}
/**
* Tests {@link EhcacheWithLoaderWriter#removeAll(Set)} for
* <ul>
* <li>non-empty request set</li>
* <li>populated {@code Store} - some keys overlap request</li>
* <li>{@link Store#bulkCompute} throws before accessing writer</li>
* <li>no {@code CacheLoaderWriter}</li>
* </ul>
*/
@Test
public void testRemoveAllStoreSomeOverlapStoreAccessExceptionBeforeNoWriter() throws Exception {
final Map<String, String> originalStoreContent = getEntryMap(KEY_SET_A, KEY_SET_B);
final FakeStore fakeStore = new FakeStore(originalStoreContent);
this.store = spy(fakeStore);
doThrow(new StoreAccessException("")).when(this.store)
.bulkCompute(getAnyStringSet(), getAnyEntryIterableFunction());
final Ehcache<String, String> ehcache = this.getEhcache();
final Set<String> contentUpdates = fanIn(KEY_SET_A, KEY_SET_C);
ehcache.removeAll(contentUpdates);
final InOrder ordered = inOrder(this.store, this.spiedResilienceStrategy);
ordered.verify(this.store, atLeast(1)).bulkCompute(this.bulkComputeSetCaptor.capture(), getAnyEntryIterableFunction());
assertThat(this.getBulkComputeArgs(), everyItem(isIn(contentUpdates)));
// ResilienceStrategy invoked; no assertions about Store content
ordered.verify(this.spiedResilienceStrategy)
.removeAllFailure(eq(contentUpdates), any(StoreAccessException.class));
validateStats(ehcache, EnumSet.noneOf(CacheOperationOutcomes.RemoveOutcome.class));
validateStats(ehcache, EnumSet.of(CacheOperationOutcomes.RemoveAllOutcome.FAILURE));
assertThat(ehcache.getBulkMethodEntries().get(BulkOps.REMOVE_ALL).intValue(), is(0));
}
/**
* Tests {@link EhcacheWithLoaderWriter#removeAll(Set)} for
* <ul>
* <li>non-empty request set</li>
* <li>populated {@code Store} - some keys overlap request</li>
* <li>{@link Store#bulkCompute} throws after accessing writer</li>
* <li>no {@code CacheLoaderWriter}</li>
* </ul>
*/
@Test
public void testRemoveAllStoreSomeOverlapStoreAccessExceptionAfterNoWriter() throws Exception {
final Map<String, String> originalStoreContent = getEntryMap(KEY_SET_A, KEY_SET_B);
final FakeStore fakeStore = new FakeStore(originalStoreContent, Collections.singleton("keyA3"));
this.store = spy(fakeStore);
final Ehcache<String, String> ehcache = this.getEhcache();
final Set<String> contentUpdates = fanIn(KEY_SET_A, KEY_SET_C);
ehcache.removeAll(contentUpdates);
final InOrder ordered = inOrder(this.store, this.spiedResilienceStrategy);
ordered.verify(this.store, atLeast(1)).bulkCompute(this.bulkComputeSetCaptor.capture(), getAnyEntryIterableFunction());
assertThat(this.getBulkComputeArgs(), everyItem(isIn(contentUpdates)));
// ResilienceStrategy invoked; no assertions about Store content
ordered.verify(this.spiedResilienceStrategy)
.removeAllFailure(eq(contentUpdates), any(StoreAccessException.class));
validateStats(ehcache, EnumSet.noneOf(CacheOperationOutcomes.RemoveOutcome.class));
validateStats(ehcache, EnumSet.of(CacheOperationOutcomes.RemoveAllOutcome.FAILURE));
assertThat(ehcache.getBulkMethodEntries().get(BulkOps.REMOVE_ALL).intValue(), is(0));
}
@Test
@SuppressWarnings("unchecked")
public void removeAllStoreCallsMethodTwice() throws Exception {
CacheLoaderWriter<String, String> cacheLoaderWriter = mock(CacheLoaderWriter.class);
final List<String> removed = new ArrayList<String>();
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
@SuppressWarnings("unchecked")
Iterable<String> i = (Iterable<String>) invocation.getArguments()[0];
for (String key : i) {
removed.add(key);
}
return null;
}
}).when(cacheLoaderWriter).deleteAll(any(Iterable.class));
final EhcacheWithLoaderWriter<String, String> ehcache = this.getEhcacheWithLoaderWriter(cacheLoaderWriter);
final ArgumentCaptor<Function<Iterable<? extends Map.Entry<? extends String, ? extends String>>, Iterable<? extends Map.Entry<? extends String, ? extends String>>>> functionArgumentCaptor = (ArgumentCaptor) ArgumentCaptor.forClass(Function.class);
when(store.bulkCompute(anySet(), functionArgumentCaptor.capture())).then(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Function<Iterable<? extends Map.Entry<? extends String, ? extends String>>, Iterable<? extends Map.Entry<? extends String, ? extends String>>> function = functionArgumentCaptor.getValue();
Iterable<? extends Map.Entry<? extends String, ? extends String>> arg = new HashMap<String, String>((Map) function.getClass().getDeclaredField("val$entriesToRemove").get(function)).entrySet();
function.apply(arg);
function.apply(arg);
return null;
}
});
Set<String> keys = new HashSet<String>() {{
add("1");
add("2");
}};
ehcache.removeAll(keys);
assertThat(removed.size(), is(2));
assertThat(removed.contains("1"), is(true));
assertThat(removed.contains("2"), is(true));
}
/**
* Gets an initialized {@link Ehcache Ehcache} instance
*
* @return a new {@code Ehcache} instance
*/
private Ehcache<String, String> getEhcache() {
final Ehcache<String, String> ehcache = new Ehcache<String, String>(CACHE_CONFIGURATION, this.store, cacheEventDispatcher, LoggerFactory.getLogger(Ehcache.class + "-" + "EhcacheBasicRemoveAllTest"));
ehcache.init();
assertThat("cache not initialized", ehcache.getStatus(), Matchers.is(Status.AVAILABLE));
this.spiedResilienceStrategy = this.setResilienceStrategySpy(ehcache);
return ehcache;
}
private EhcacheWithLoaderWriter<String, String> getEhcacheWithLoaderWriter(CacheLoaderWriter<? super String, String> cacheLoaderWriter) {
final EhcacheWithLoaderWriter<String, String> ehcache = new EhcacheWithLoaderWriter<String, String>(CACHE_CONFIGURATION, this.store, cacheLoaderWriter, cacheEventDispatcher, LoggerFactory.getLogger(Ehcache.class + "-" + "EhcacheBasicPutAllTest"));
ehcache.init();
assertThat("cache not initialized", ehcache.getStatus(), Matchers.is(Status.AVAILABLE));
this.spiedResilienceStrategy = this.setResilienceStrategySpy(ehcache);
return ehcache;
}
/**
* Returns a Mockito {@code any} Matcher for {@code java.util.Set<String>}.
*
* @return a Mockito {@code any} matcher for {@code Set<String>}
*/
@SuppressWarnings("unchecked")
static Set<? extends String> getAnyStringSet() {
return any(Set.class); // unchecked
}
/**
* Returns a Mockito {@code any} Matcher for a {@link Function} over a {@code Map.Entry} {@code Iterable}.
*
* @return a Mockito {@code any} matcher for {@code Function}
*/
@SuppressWarnings("unchecked")
static Function<Iterable<? extends Map.Entry<? extends String, ? extends String>>, Iterable<? extends Map.Entry<? extends String, ? extends String>>> getAnyEntryIterableFunction() {
return any(Function.class); // unchecked
}
/**
* Returns a Mockito {@code any} Matcher for a {@code String} {@code Iterable}.
*
* @return a Mockito {@code any} matcher for {@code Iterable}
*/
@SuppressWarnings("unchecked")
static Iterable<? extends String> getAnyStringIterable() {
return any(Iterable.class);
}
/**
* Collects all arguments captured by {@link #bulkComputeSetCaptor}.
*
* @return the argument values collected by {@link #bulkComputeSetCaptor}; the
* {@code Iterator} over the resulting {@code Set} returns the values
* in the order observed by the captor.
*/
private Set<String> getBulkComputeArgs() {
final Set<String> bulkComputeArgs = new LinkedHashSet<String>();
for (final Set<String> set : this.bulkComputeSetCaptor.getAllValues()) {
bulkComputeArgs.addAll(set);
}
return bulkComputeArgs;
}
/**
* Indicates whether or not {@link #dumpResults} should emit output.
*/
private static final boolean debugResults;
static {
debugResults = Boolean.parseBoolean(System.getProperty(EhcacheBasicRemoveAllTest.class.getName() + ".debug", "false"));
}
@Rule public TestName name = new TestName();
/**
* Writes a dump of test object details to {@code System.out} if, and only if, {@link #debugResults} is enabled.
*
* @param fakeStore the {@link org.ehcache.core.EhcacheBasicCrudBase.FakeStore FakeStore} instance used in the test
* @param originalStoreContent the original content provided to {@code fakeStore}
* @param fakeLoaderWriter the {@link org.ehcache.core.EhcacheBasicCrudBase.FakeCacheLoaderWriter FakeCacheLoaderWriter} instances used in the test
* @param originalWriterContent the original content provided to {@code fakeLoaderWriter}
* @param contentUpdates the {@code Set} provided to the {@link EhcacheWithLoaderWriter#removeAll(java.util.Set)} call in the test
* @param expectedFailures the {@code Set} of failing keys expected for the test
* @param expectedSuccesses the {@code Set} of successful keys expected for the test
* @param bcweSuccesses the {@code Set} from {@link BulkCacheWritingException#getSuccesses()}
* @param bcweFailures the {@code Map} from {@link BulkCacheWritingException#getFailures()}
*/
private void dumpResults(
final FakeStore fakeStore,
final Map<String, String> originalStoreContent,
final FakeCacheLoaderWriter fakeLoaderWriter,
final Map<String, String> originalWriterContent,
final Set<String> contentUpdates,
final Set<String> expectedFailures,
final Set<String> expectedSuccesses,
final Set<String> bcweSuccesses,
final Map<String, Exception> bcweFailures) {
if (!debugResults) {
return;
}
final StringBuilder sb = new StringBuilder(2048);
final Formatter fmt = new Formatter(sb);
fmt.format("Dumping results of %s:%n", this.name.getMethodName());
fmt.format(" Content Update Keys: %s%n", sort(contentUpdates));
fmt.format(" Original Store Keys : %s%n", sort(originalStoreContent.keySet()));
fmt.format(" Final Store Keys : %s%n", sort(fakeStore.getEntryMap().keySet()));
fmt.format(" Original Writer Keys: %s%n", sort(originalWriterContent.keySet()));
fmt.format(" Final Writer Keys : %s%n", sort(fakeLoaderWriter.getEntryMap().keySet()));
fmt.format(" Expected Successes: %s%n", sort(expectedSuccesses));
fmt.format(" Declared Successes: %s%n", sort(bcweSuccesses));
fmt.format(" Expected Failures: %s%n", sort(expectedFailures));
fmt.format(" Declared Failures: %s%n", sort(bcweFailures.keySet()));
System.err.flush();
System.out.append(sb);
System.out.flush();
}
private static List<String> sort(final Collection<String> input) {
final String[] sortArray = new String[input.size()];
input.toArray(sortArray);
Arrays.sort(sortArray);
return Arrays.asList(sortArray);
}
}