/* * Licensed to the Apache Software Foundation (ASF) under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional information regarding * copyright ownership. The ASF licenses this file to you 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.apache.geode.cache.lucene.internal.repository; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import java.io.IOException; import java.io.Serializable; import java.util.Arrays; import java.util.HashSet; import java.util.Random; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.IntSupplier; import org.apache.geode.internal.cache.BucketAdvisor; import org.apache.geode.internal.cache.BucketRegion; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.queryparser.classic.ParseException; import org.apache.lucene.queryparser.classic.QueryParser; import org.junit.Before; import org.junit.Test; import org.junit.experimental.categories.Category; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.apache.geode.cache.Region; import org.apache.geode.cache.lucene.internal.LuceneIndexStats; import org.apache.geode.cache.lucene.internal.directory.RegionDirectory; import org.apache.geode.cache.lucene.internal.filesystem.ChunkKey; import org.apache.geode.cache.lucene.internal.filesystem.File; import org.apache.geode.cache.lucene.internal.filesystem.FileSystemStats; import org.apache.geode.cache.lucene.internal.repository.serializer.HeterogeneousLuceneSerializer; import org.apache.geode.cache.lucene.internal.repository.serializer.Type2; import org.apache.geode.test.junit.categories.IntegrationTest; /** * Test of the {@link IndexRepository} and everything below it. This tests that we can save gemfire * objects or PDXInstance objects into a lucene index and search for those objects later. */ @Category(IntegrationTest.class) public class IndexRepositoryImplJUnitTest { private IndexRepositoryImpl repo; private HeterogeneousLuceneSerializer mapper; private StandardAnalyzer analyzer = new StandardAnalyzer(); private IndexWriter writer; private Region region; private Region userRegion; private LuceneIndexStats stats; private FileSystemStats fileSystemStats; @Before public void setUp() throws IOException { ConcurrentHashMap<String, File> fileRegion = new ConcurrentHashMap<String, File>(); ConcurrentHashMap<ChunkKey, byte[]> chunkRegion = new ConcurrentHashMap<ChunkKey, byte[]>(); fileSystemStats = mock(FileSystemStats.class); RegionDirectory dir = new RegionDirectory(fileRegion, chunkRegion, fileSystemStats); IndexWriterConfig config = new IndexWriterConfig(analyzer); writer = new IndexWriter(dir, config); String[] indexedFields = new String[] {"s", "i", "l", "d", "f", "s2", "missing"}; mapper = new HeterogeneousLuceneSerializer(indexedFields); region = Mockito.mock(Region.class); userRegion = Mockito.mock(BucketRegion.class); BucketAdvisor bucketAdvisor = Mockito.mock(BucketAdvisor.class); Mockito.when(bucketAdvisor.isPrimary()).thenReturn(true); Mockito.when(((BucketRegion) userRegion).getBucketAdvisor()).thenReturn(bucketAdvisor); Mockito.when(((BucketRegion) userRegion).getBucketAdvisor().isPrimary()).thenReturn(true); stats = Mockito.mock(LuceneIndexStats.class); Mockito.when(userRegion.isDestroyed()).thenReturn(false); repo = new IndexRepositoryImpl(region, writer, mapper, stats, userRegion); } @Test public void testAddDocs() throws IOException, ParseException { repo.create("key1", new Type2("bacon maple bar", 1, 2L, 3.0, 4.0f, "Grape Ape doughnut")); repo.create("key2", new Type2("McMinnville Cream doughnut", 1, 2L, 3.0, 4.0f, "Captain my Captain doughnut")); repo.create("key3", new Type2("Voodoo Doll doughnut", 1, 2L, 3.0, 4.0f, "Toasted coconut doughnut")); repo.create("key4", new Type2("Portland Cream doughnut", 1, 2L, 3.0, 4.0f, "Captain my Captain doughnut")); repo.commit(); checkQuery("Cream", "s", "key2", "key4"); checkQuery("NotARealWord", "s"); } @Test public void testEmptyRepo() throws IOException, ParseException { checkQuery("NotARealWord", "s"); } @Test public void testUpdateAndRemoveStringKeys() throws IOException, ParseException { updateAndRemove("key1", "key2", "key3", "key4"); } @Test public void testUpdateAndRemoveBinaryKeys() throws IOException, ParseException { ByteWrapper key1 = randomKey(); ByteWrapper key2 = randomKey(); ByteWrapper key3 = randomKey(); ByteWrapper key4 = randomKey(); updateAndRemove(key1, key2, key3, key4); } @Test public void createShouldUpdateStats() throws IOException { repo.create("key1", new Type2("bar", 1, 2L, 3.0, 4.0f, "Grape Ape doughnut")); verify(stats, times(1)).startUpdate(); verify(stats, times(1)).endUpdate(anyLong()); } @Test public void updateShouldUpdateStats() throws IOException { repo.update("key1", new Type2("bacon maple bar", 1, 2L, 3.0, 4.0f, "Grape Ape doughnut")); verify(stats, times(1)).startUpdate(); verify(stats, times(1)).endUpdate(anyLong()); } @Test public void deleteShouldUpdateStats() throws IOException { repo.delete("key1"); verify(stats, times(1)).startUpdate(); verify(stats, times(1)).endUpdate(anyLong()); } @Test public void commitShouldUpdateStats() throws IOException { repo.commit(); verify(stats, times(1)).startCommit(); verify(stats, times(1)).endCommit(anyLong()); } @Test public void queryShouldUpdateStats() throws IOException, ParseException { repo.create("key2", new Type2("McMinnville Cream doughnut", 1, 2L, 3.0, 4.0f, "Captain my Captain doughnut")); repo.create("key4", new Type2("Portland Cream doughnut", 1, 2L, 3.0, 4.0f, "Captain my Captain doughnut")); repo.commit(); checkQuery("Cream", "s", "key2", "key4"); verify(stats, times(1)).startRepositoryQuery(); verify(stats, times(1)).endRepositoryQuery(anyLong(), eq(2)); } @Test public void addingDocumentsShouldUpdateDocumentsStat() throws IOException { repo.create("key1", new Type2("bar", 1, 2L, 3.0, 4.0f, "Grape Ape doughnut")); repo.commit(); ArgumentCaptor<IntSupplier> captor = ArgumentCaptor.forClass(IntSupplier.class); verify(stats).addDocumentsSupplier(captor.capture()); IntSupplier supplier = captor.getValue(); assertEquals(1, supplier.getAsInt()); } @Test public void cleanupShouldCloseWriter() throws IOException { repo.cleanup(); verify(stats).removeDocumentsSupplier(any()); assertFalse(writer.isOpen()); } private void updateAndRemove(Object key1, Object key2, Object key3, Object key4) throws IOException, ParseException { repo.create(key1, new Type2("bacon maple bar", 1, 2L, 3.0, 4.0f, "Grape Ape doughnut")); repo.create(key2, new Type2("McMinnville Cream doughnut", 1, 2L, 3.0, 4.0f, "Captain my Captain doughnut")); repo.create(key3, new Type2("Voodoo Doll doughnut", 1, 2L, 3.0, 4.0f, "Toasted coconut doughnut")); repo.create(key4, new Type2("Portland Cream doughnut", 1, 2L, 3.0, 4.0f, "Captain my Captain doughnut")); repo.commit(); repo.update(key3, new Type2("Boston Cream Pie", 1, 2L, 3.0, 4.0f, "Toasted coconut doughnut")); repo.delete(key4); repo.commit(); // BooleanQuery q = new BooleanQuery(); // q.add(new TermQuery(SerializerUtil.toKeyTerm("key3")), Occur.MUST_NOT); // writer.deleteDocuments(q); // writer.commit(); // Make sure the updates and deletes were applied checkQuery("doughnut", "s", key2); checkQuery("Cream", "s", key2, key3); } private ByteWrapper randomKey() { Random rand = new Random(); int size = rand.nextInt(2048) + 50; byte[] key = new byte[size]; rand.nextBytes(key); return new ByteWrapper(key); } private void checkQuery(String queryTerm, String queryField, Object... expectedKeys) throws IOException, ParseException { Set<Object> expectedSet = new HashSet<Object>(); expectedSet.addAll(Arrays.asList(expectedKeys)); QueryParser parser = new QueryParser(queryField, analyzer); KeyCollector collector = new KeyCollector(); repo.query(parser.parse(queryTerm), 100, collector); assertEquals(expectedSet, collector.results); } private static class KeyCollector implements IndexResultCollector { Set<Object> results = new HashSet<Object>(); @Override public void collect(Object key, float score) { results.add(key); } @Override public String getName() { return null; } @Override public int size() { return results.size(); } } /** * A wrapper around a byte array that implements equals, for comparison checks. */ private static class ByteWrapper implements Serializable { private byte[] bytes; public ByteWrapper(byte[] bytes) { super(); this.bytes = bytes; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + Arrays.hashCode(bytes); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ByteWrapper other = (ByteWrapper) obj; if (!Arrays.equals(bytes, other.bytes)) return false; return true; } } }