mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-07-03 04:48:12 +02:00
One index per type and parallel indexing (#1781)
Before this change the search uses a single index which distinguishes types (repositories, users, etc.) with a field (_type).
But it has turned out that this could lead to problems, in particular if different types have the same field and uses different analyzers for those fields. The following links show even more problems of a combined index:
https://www.elastic.co/blog/index-vs-type
https://www.elastic.co/guide/en/elasticsearch/reference/6.0/removal-of-types.html
With this change every type becomes its own index and the SearchEngine gets an api to modify multiple indices at once to remove all documents from all indices, which are related to a specific repository, for example.
The search uses another new api to coordinate the indexing, the central work queue.
The central work queue is able to coordinate long-running or resource intensive tasks. It is able to run tasks in parallel, but can also run tasks which targets the same resources in sequence. The queue is also persistent and can restore queued tasks after restart.
Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
@@ -24,20 +24,23 @@
|
||||
|
||||
package sonia.scm.group;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.HandlerEventType;
|
||||
import sonia.scm.search.Id;
|
||||
import sonia.scm.search.Index;
|
||||
import sonia.scm.search.IndexLogStore;
|
||||
import sonia.scm.search.SearchEngine;
|
||||
import sonia.scm.search.SerializableIndexTask;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -54,6 +57,19 @@ class GroupIndexerTest {
|
||||
@InjectMocks
|
||||
private GroupIndexer indexer;
|
||||
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private Index<Group> index;
|
||||
|
||||
@Mock
|
||||
private IndexLogStore indexLogStore;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<SerializableIndexTask<Group>> captor;
|
||||
|
||||
private final Group astronauts = new Group("xml", "astronauts");
|
||||
private final Group planetCreators = new Group("xml", "planet-creators");
|
||||
|
||||
@Test
|
||||
void shouldReturnClass() {
|
||||
assertThat(indexer.getType()).isEqualTo(Group.class);
|
||||
@@ -64,58 +80,47 @@ class GroupIndexerTest {
|
||||
assertThat(indexer.getVersion()).isEqualTo(GroupIndexer.VERSION);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class UpdaterTests {
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private Index<Group> index;
|
||||
@Test
|
||||
void shouldReturnReIndexAllClass() {
|
||||
assertThat(indexer.getReIndexAllTask()).isEqualTo(GroupIndexer.ReIndexAll.class);
|
||||
}
|
||||
|
||||
private final Group group = new Group("xml", "astronauts");
|
||||
@Test
|
||||
void shouldCreateGroup() {
|
||||
indexer.createStoreTask(astronauts).update(index);
|
||||
|
||||
@BeforeEach
|
||||
void open() {
|
||||
when(searchEngine.forType(Group.class).getOrCreate()).thenReturn(index);
|
||||
}
|
||||
verify(index).store(Id.of(astronauts), GroupPermissions.read(astronauts).asShiroString(), astronauts);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStore() {
|
||||
indexer.open().store(group);
|
||||
@Test
|
||||
void shouldDeleteGroup() {
|
||||
indexer.createDeleteTask(astronauts).update(index);
|
||||
|
||||
verify(index).store(Id.of(group), "group:read:astronauts", group);
|
||||
}
|
||||
verify(index.delete()).byId(Id.of(astronauts));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteById() {
|
||||
indexer.open().delete(group);
|
||||
@Test
|
||||
void shouldReIndexAll() {
|
||||
when(groupManager.getAll()).thenReturn(Arrays.asList(astronauts, planetCreators));
|
||||
|
||||
verify(index.delete().byType()).byId(Id.of(group));
|
||||
}
|
||||
GroupIndexer.ReIndexAll reIndexAll = new GroupIndexer.ReIndexAll(indexLogStore, groupManager);
|
||||
reIndexAll.update(index);
|
||||
|
||||
@Test
|
||||
void shouldReIndexAll() {
|
||||
when(groupManager.getAll()).thenReturn(singletonList(group));
|
||||
verify(index.delete()).all();
|
||||
verify(index).store(Id.of(astronauts), GroupPermissions.read(astronauts).asShiroString(), astronauts);
|
||||
verify(index).store(Id.of(planetCreators), GroupPermissions.read(planetCreators).asShiroString(), planetCreators);
|
||||
}
|
||||
|
||||
indexer.open().reIndexAll();
|
||||
@Test
|
||||
void shouldHandleEvents() {
|
||||
GroupEvent event = new GroupEvent(HandlerEventType.DELETE, astronauts);
|
||||
|
||||
verify(index.delete().byType()).all();
|
||||
verify(index).store(Id.of(group), "group:read:astronauts", group);
|
||||
}
|
||||
indexer.handleEvent(event);
|
||||
|
||||
@Test
|
||||
void shouldHandleEvent() {
|
||||
GroupEvent event = new GroupEvent(HandlerEventType.DELETE, group);
|
||||
|
||||
indexer.handleEvent(event);
|
||||
|
||||
verify(index.delete().byType()).byId(Id.of(group));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCloseIndex() {
|
||||
indexer.open().close();
|
||||
|
||||
verify(index).close();
|
||||
}
|
||||
verify(searchEngine.forType(Group.class)).update(captor.capture());
|
||||
captor.getValue().update(index);
|
||||
verify(index.delete()).byId(Id.of(astronauts));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,20 +24,25 @@
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.HandlerEventType;
|
||||
import sonia.scm.search.Id;
|
||||
import sonia.scm.search.Index;
|
||||
import sonia.scm.search.IndexLogStore;
|
||||
import sonia.scm.search.SearchEngine;
|
||||
import sonia.scm.search.SerializableIndexTask;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -54,6 +59,15 @@ class RepositoryIndexerTest {
|
||||
@InjectMocks
|
||||
private RepositoryIndexer indexer;
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private Index<Repository> index;
|
||||
|
||||
@Mock
|
||||
private IndexLogStore indexLogStore;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<SerializableIndexTask<Repository>> captor;
|
||||
|
||||
@Test
|
||||
void shouldReturnRepositoryClass() {
|
||||
assertThat(indexer.getType()).isEqualTo(Repository.class);
|
||||
@@ -64,61 +78,65 @@ class RepositoryIndexerTest {
|
||||
assertThat(indexer.getVersion()).isEqualTo(RepositoryIndexer.VERSION);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class UpdaterTests {
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private Index<Repository> index;
|
||||
|
||||
private Repository repository;
|
||||
|
||||
@BeforeEach
|
||||
void open() {
|
||||
when(searchEngine.forType(Repository.class).getOrCreate()).thenReturn(index);
|
||||
repository = new Repository();
|
||||
repository.setId("42");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStoreRepository() {
|
||||
indexer.open().store(repository);
|
||||
|
||||
verify(index).store(Id.of(repository), "repository:read:42", repository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteByRepository() {
|
||||
indexer.open().delete(repository);
|
||||
|
||||
verify(index.delete().allTypes()).byRepository("42");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReIndexAll() {
|
||||
when(repositoryManager.getAll()).thenReturn(singletonList(repository));
|
||||
|
||||
indexer.open().reIndexAll();
|
||||
|
||||
verify(index.delete().allTypes()).byTypeName(Repository.class.getName());
|
||||
verify(index.delete().byType()).all();
|
||||
|
||||
verify(index).store(Id.of(repository), "repository:read:42", repository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleEvent() {
|
||||
RepositoryEvent event = new RepositoryEvent(HandlerEventType.DELETE, repository);
|
||||
|
||||
indexer.handleEvent(event);
|
||||
|
||||
verify(index.delete().allTypes()).byRepository("42");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCloseIndex() {
|
||||
indexer.open().close();
|
||||
|
||||
verify(index).close();
|
||||
}
|
||||
@Test
|
||||
void shouldReturnReIndexAllClass() {
|
||||
assertThat(indexer.getReIndexAllTask()).isEqualTo(RepositoryIndexer.ReIndexAll.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateRepository() {
|
||||
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||
|
||||
indexer.createStoreTask(heartOfGold).update(index);
|
||||
|
||||
verify(index).store(Id.of(heartOfGold), RepositoryPermissions.read(heartOfGold).asShiroString(), heartOfGold);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteRepository() {
|
||||
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||
|
||||
indexer.createDeleteTask(heartOfGold).update(index);
|
||||
|
||||
verify(index.delete()).byRepository(heartOfGold);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReIndexAll() {
|
||||
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||
Repository puzzle = RepositoryTestData.create42Puzzle();
|
||||
when(repositoryManager.getAll()).thenReturn(Arrays.asList(heartOfGold, puzzle));
|
||||
|
||||
RepositoryIndexer.ReIndexAll reIndexAll = new RepositoryIndexer.ReIndexAll(indexLogStore, repositoryManager);
|
||||
reIndexAll.update(index);
|
||||
|
||||
verify(index.delete()).all();
|
||||
verify(index).store(Id.of(heartOfGold), RepositoryPermissions.read(heartOfGold).asShiroString(), heartOfGold);
|
||||
verify(index).store(Id.of(puzzle), RepositoryPermissions.read(puzzle).asShiroString(), puzzle);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleDeleteEvents() {
|
||||
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||
RepositoryEvent event = new RepositoryEvent(HandlerEventType.DELETE, heartOfGold);
|
||||
|
||||
indexer.handleEvent(event);
|
||||
|
||||
verify(searchEngine.forIndices().forResource(heartOfGold)).batch(captor.capture());
|
||||
captor.getValue().update(index);
|
||||
verify(index.delete()).byRepository(heartOfGold);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleUpdateEvents() {
|
||||
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||
RepositoryEvent event = new RepositoryEvent(HandlerEventType.CREATE, heartOfGold);
|
||||
|
||||
indexer.handleEvent(event);
|
||||
|
||||
verify(searchEngine.forType(Repository.class)).update(captor.capture());
|
||||
captor.getValue().update(index);
|
||||
verify(index).store(Id.of(heartOfGold), RepositoryPermissions.read(heartOfGold).asShiroString(), heartOfGold);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,10 +24,12 @@
|
||||
|
||||
package sonia.scm.search;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.HandlerEventType;
|
||||
@@ -35,6 +37,7 @@ import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryEvent;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -42,18 +45,23 @@ import static org.mockito.Mockito.when;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class HandlerEventIndexSyncerTest {
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private SearchEngine searchEngine;
|
||||
|
||||
@Mock
|
||||
private Indexer<Repository> indexer;
|
||||
|
||||
@Mock
|
||||
private Indexer.Updater<Repository> updater;
|
||||
@BeforeEach
|
||||
void setUpIndexer() {
|
||||
lenient().when(indexer.getType()).thenReturn(Repository.class);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(value = HandlerEventType.class, mode = EnumSource.Mode.MATCH_ANY, names = "BEFORE_.*")
|
||||
void shouldIgnoreBeforeEvents(HandlerEventType type) {
|
||||
RepositoryEvent event = new RepositoryEvent(type, RepositoryTestData.create42Puzzle());
|
||||
|
||||
new HandlerEventIndexSyncer<>(indexer).handleEvent(event);
|
||||
new HandlerEventIndexSyncer<>(searchEngine, indexer).handleEvent(event);
|
||||
|
||||
verifyNoInteractions(indexer);
|
||||
}
|
||||
@@ -61,28 +69,29 @@ class HandlerEventIndexSyncerTest {
|
||||
@ParameterizedTest
|
||||
@EnumSource(value = HandlerEventType.class, mode = EnumSource.Mode.INCLUDE, names = {"CREATE", "MODIFY"})
|
||||
void shouldStore(HandlerEventType type) {
|
||||
when(indexer.open()).thenReturn(updater);
|
||||
SerializableIndexTask<Repository> store = index -> {};
|
||||
|
||||
Repository puzzle = RepositoryTestData.create42Puzzle();
|
||||
when(indexer.createStoreTask(puzzle)).thenReturn(store);
|
||||
|
||||
RepositoryEvent event = new RepositoryEvent(type, puzzle);
|
||||
|
||||
new HandlerEventIndexSyncer<>(indexer).handleEvent(event);
|
||||
new HandlerEventIndexSyncer<>(searchEngine, indexer).handleEvent(event);
|
||||
|
||||
verify(updater).store(puzzle);
|
||||
verify(updater).close();
|
||||
verify(searchEngine.forType(Repository.class)).update(store);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDelete() {
|
||||
when(indexer.open()).thenReturn(updater);
|
||||
SerializableIndexTask<Repository> delete = index -> {};
|
||||
|
||||
Repository puzzle = RepositoryTestData.create42Puzzle();
|
||||
when(indexer.createDeleteTask(puzzle)).thenReturn(delete);
|
||||
|
||||
RepositoryEvent event = new RepositoryEvent(HandlerEventType.DELETE, puzzle);
|
||||
new HandlerEventIndexSyncer<>(searchEngine, indexer).handleEvent(event);
|
||||
|
||||
new HandlerEventIndexSyncer<>(indexer).handleEvent(event);
|
||||
|
||||
verify(updater).delete(puzzle);
|
||||
verify(updater).close();
|
||||
verify(searchEngine.forType(Repository.class)).update(delete);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -32,8 +32,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.group.Group;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.web.security.AdministrationContext;
|
||||
import sonia.scm.web.security.PrivilegedAction;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
@@ -42,47 +40,39 @@ import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class IndexBootstrapListenerTest {
|
||||
|
||||
@Mock
|
||||
private AdministrationContext administrationContext;
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private SearchEngine searchEngine;
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private IndexLogStore indexLogStore;
|
||||
|
||||
@Test
|
||||
void shouldReIndexWithoutLog() {
|
||||
mockAdminContext();
|
||||
Indexer<Repository> indexer = indexer(Repository.class, 1);
|
||||
Indexer.Updater<Repository> updater = updater(indexer);
|
||||
|
||||
mockEmptyIndexLog(Repository.class);
|
||||
doInitialization(indexer);
|
||||
|
||||
verify(updater).reIndexAll();
|
||||
verify(updater).close();
|
||||
verify(indexLogStore.defaultIndex()).log(Repository.class, 1);
|
||||
verify(searchEngine.forType(Repository.class)).update(RepositoryReIndexAllTask.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReIndexIfVersionWasUpdated() {
|
||||
mockAdminContext();
|
||||
Indexer<User> indexer = indexer(User.class, 2);
|
||||
Indexer.Updater<User> updater = updater(indexer);
|
||||
|
||||
mockIndexLog(User.class, 1);
|
||||
doInitialization(indexer);
|
||||
|
||||
verify(updater).reIndexAll();
|
||||
verify(updater).close();
|
||||
verify(indexLogStore.defaultIndex()).log(User.class, 2);
|
||||
verify(searchEngine.forType(User.class)).update(UserReIndexAllTask.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -92,7 +82,7 @@ class IndexBootstrapListenerTest {
|
||||
mockIndexLog(Group.class, 3);
|
||||
doInitialization(indexer);
|
||||
|
||||
verify(indexer, never()).open();
|
||||
verifyNoInteractions(searchEngine);
|
||||
}
|
||||
|
||||
private <T> void mockIndexLog(Class<T> type, int version) {
|
||||
@@ -107,13 +97,6 @@ class IndexBootstrapListenerTest {
|
||||
when(indexLogStore.defaultIndex().get(type)).thenReturn(Optional.ofNullable(indexLog));
|
||||
}
|
||||
|
||||
private void mockAdminContext() {
|
||||
doAnswer(ic -> {
|
||||
PrivilegedAction action = ic.getArgument(0);
|
||||
action.run();
|
||||
return null;
|
||||
}).when(administrationContext).runAsAdmin(any(PrivilegedAction.class));
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private void doInitialization(Indexer... indexers) {
|
||||
@@ -125,7 +108,7 @@ class IndexBootstrapListenerTest {
|
||||
@SuppressWarnings("rawtypes")
|
||||
private IndexBootstrapListener listener(Indexer... indexers) {
|
||||
return new IndexBootstrapListener(
|
||||
administrationContext, indexLogStore, new HashSet<>(Arrays.asList(indexers))
|
||||
searchEngine, indexLogStore, new HashSet<>(Arrays.asList(indexers))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,15 +116,38 @@ class IndexBootstrapListenerTest {
|
||||
private <T> Indexer<T> indexer(Class<T> type, int version) {
|
||||
Indexer<T> indexer = mock(Indexer.class);
|
||||
when(indexer.getType()).thenReturn(type);
|
||||
when(indexer.getVersion()).thenReturn(version);
|
||||
lenient().when(indexer.getVersion()).thenReturn(version);
|
||||
lenient().when(indexer.getReIndexAllTask()).thenAnswer(ic -> {
|
||||
if (type == User.class) {
|
||||
return UserReIndexAllTask.class;
|
||||
}
|
||||
return RepositoryReIndexAllTask.class;
|
||||
});
|
||||
return indexer;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> Indexer.Updater<T> updater(Indexer<T> indexer) {
|
||||
Indexer.Updater<T> updater = mock(Indexer.Updater.class);
|
||||
when(indexer.open()).thenReturn(updater);
|
||||
return updater;
|
||||
public static class RepositoryReIndexAllTask extends Indexer.ReIndexAllTask<Repository> {
|
||||
|
||||
public RepositoryReIndexAllTask(IndexLogStore logStore, Class<Repository> type, int version) {
|
||||
super(logStore, type, version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Index<Repository> index) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static class UserReIndexAllTask extends Indexer.ReIndexAllTask<User> {
|
||||
|
||||
public UserReIndexAllTask(IndexLogStore logStore, Class<User> type, int version) {
|
||||
super(logStore, type, version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Index<User> index) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
188
scm-webapp/src/test/java/sonia/scm/search/IndexManagerTest.java
Normal file
188
scm-webapp/src/test/java/sonia/scm/search/IndexManagerTest.java
Normal file
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.search;
|
||||
|
||||
import org.apache.lucene.analysis.core.SimpleAnalyzer;
|
||||
import org.apache.lucene.document.Document;
|
||||
import org.apache.lucene.document.Field;
|
||||
import org.apache.lucene.document.TextField;
|
||||
import org.apache.lucene.index.IndexReader;
|
||||
import org.apache.lucene.index.IndexWriter;
|
||||
import org.assertj.core.api.AssertionsForClassTypes;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class IndexManagerTest {
|
||||
|
||||
private Path directory;
|
||||
|
||||
@Mock
|
||||
private AnalyzerFactory analyzerFactory;
|
||||
|
||||
@Mock
|
||||
private LuceneSearchableType searchableType;
|
||||
|
||||
@Mock
|
||||
private SCMContextProvider context;
|
||||
|
||||
@Mock
|
||||
private PluginLoader pluginLoader;
|
||||
|
||||
private IndexManager indexManager;
|
||||
|
||||
@BeforeEach
|
||||
void createIndexWriterFactory(@TempDir Path tempDirectory) {
|
||||
this.directory = tempDirectory;
|
||||
when(context.resolve(Paths.get("index"))).thenReturn(tempDirectory.resolve("index"));
|
||||
when(analyzerFactory.create(any(LuceneSearchableType.class), any(IndexOptions.class))).thenReturn(new SimpleAnalyzer());
|
||||
when(pluginLoader.getUberClassLoader()).thenReturn(IndexManagerTest.class.getClassLoader());
|
||||
indexManager = new IndexManager(context, pluginLoader, analyzerFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateNewIndex() throws IOException {
|
||||
try (IndexWriter writer = open(Songs.class, "new-index")) {
|
||||
addDoc(writer, "Trillian");
|
||||
}
|
||||
assertThat(directory.resolve("index").resolve("songs").resolve("new-index")).exists();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateNewIndexForEachType() throws IOException {
|
||||
try (IndexWriter writer = open(Songs.class, "new-index")) {
|
||||
addDoc(writer, "Trillian");
|
||||
}
|
||||
try (IndexWriter writer = open(Lyrics.class, "new-index")) {
|
||||
addDoc(writer, "Trillian");
|
||||
}
|
||||
assertThat(directory.resolve("index").resolve("songs").resolve("new-index")).exists();
|
||||
assertThat(directory.resolve("index").resolve("lyrics").resolve("new-index")).exists();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnAllCreatedIndices() throws IOException {
|
||||
try (IndexWriter writer = open(Songs.class, "special")) {
|
||||
addDoc(writer, "Trillian");
|
||||
}
|
||||
try (IndexWriter writer = open(Lyrics.class, "awesome")) {
|
||||
addDoc(writer, "Trillian");
|
||||
}
|
||||
|
||||
assertThat(indexManager.all())
|
||||
.anySatisfy(details -> {
|
||||
assertThat(details.getType()).isEqualTo(Songs.class);
|
||||
assertThat(details.getName()).isEqualTo("special");
|
||||
})
|
||||
.anySatisfy(details -> {
|
||||
assertThat(details.getType()).isEqualTo(Lyrics.class);
|
||||
assertThat(details.getName()).isEqualTo("awesome");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRestoreIndicesOnCreation() throws IOException {
|
||||
try (IndexWriter writer = open(Songs.class, "special")) {
|
||||
addDoc(writer, "Trillian");
|
||||
}
|
||||
try (IndexWriter writer = open(Lyrics.class, "awesome")) {
|
||||
addDoc(writer, "Trillian");
|
||||
}
|
||||
|
||||
assertThat(new IndexManager(context, pluginLoader, analyzerFactory).all())
|
||||
.anySatisfy(details -> {
|
||||
AssertionsForClassTypes.assertThat(details.getType()).isEqualTo(Songs.class);
|
||||
AssertionsForClassTypes.assertThat(details.getName()).isEqualTo("special");
|
||||
})
|
||||
.anySatisfy(details -> {
|
||||
AssertionsForClassTypes.assertThat(details.getType()).isEqualTo(Lyrics.class);
|
||||
AssertionsForClassTypes.assertThat(details.getName()).isEqualTo("awesome");
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private IndexWriter open(Class type, String indexName) throws IOException {
|
||||
lenient().when(searchableType.getType()).thenReturn(type);
|
||||
when(searchableType.getName()).thenReturn(type.getSimpleName().toLowerCase(Locale.ENGLISH));
|
||||
return indexManager.openForWrite(new IndexParams(indexName, searchableType, IndexOptions.defaults()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOpenExistingIndex() throws IOException {
|
||||
try (IndexWriter writer = open(Songs.class, "reused")) {
|
||||
addDoc(writer, "Dent");
|
||||
}
|
||||
try (IndexWriter writer = open(Songs.class, "reused")) {
|
||||
assertThat(writer.getFieldNames()).contains("hitchhiker");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseAnalyzerFromFactory() throws IOException {
|
||||
try (IndexWriter writer = open(Songs.class, "new-index")) {
|
||||
assertThat(writer.getAnalyzer()).isInstanceOf(SimpleAnalyzer.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOpenIndexForRead() throws IOException {
|
||||
try (IndexWriter writer = open(Songs.class, "idx-for-read")) {
|
||||
addDoc(writer, "Dent");
|
||||
}
|
||||
|
||||
try (IndexReader reader = indexManager.openForRead(searchableType, "idx-for-read")) {
|
||||
assertThat(reader.numDocs()).isOne();
|
||||
}
|
||||
}
|
||||
|
||||
private void addDoc(IndexWriter writer, String name) throws IOException {
|
||||
Document doc = new Document();
|
||||
doc.add(new TextField("hitchhiker", name, Field.Store.YES));
|
||||
writer.addDocument(doc);
|
||||
}
|
||||
|
||||
public static class Songs {
|
||||
}
|
||||
|
||||
public static class Lyrics {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.search;
|
||||
|
||||
import org.apache.lucene.analysis.core.SimpleAnalyzer;
|
||||
import org.apache.lucene.document.Document;
|
||||
import org.apache.lucene.document.Field;
|
||||
import org.apache.lucene.document.TextField;
|
||||
import org.apache.lucene.index.IndexReader;
|
||||
import org.apache.lucene.index.IndexWriter;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class IndexOpenerTest {
|
||||
|
||||
private Path directory;
|
||||
|
||||
@Mock
|
||||
private AnalyzerFactory analyzerFactory;
|
||||
|
||||
@Mock
|
||||
private LuceneSearchableType searchableType;
|
||||
|
||||
private IndexOpener indexOpener;
|
||||
|
||||
@BeforeEach
|
||||
void createIndexWriterFactory(@TempDir Path tempDirectory) {
|
||||
this.directory = tempDirectory;
|
||||
SCMContextProvider context = mock(SCMContextProvider.class);
|
||||
when(context.resolve(Paths.get("index"))).thenReturn(tempDirectory);
|
||||
when(analyzerFactory.create(any(LuceneSearchableType.class), any(IndexOptions.class))).thenReturn(new SimpleAnalyzer());
|
||||
indexOpener = new IndexOpener(context, analyzerFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateNewIndex() throws IOException {
|
||||
try (IndexWriter writer = open("new-index")) {
|
||||
addDoc(writer, "Trillian");
|
||||
}
|
||||
assertThat(directory.resolve("new-index")).exists();
|
||||
}
|
||||
|
||||
private IndexWriter open(String index) throws IOException {
|
||||
return indexOpener.openForWrite(new IndexParams(index, searchableType, IndexOptions.defaults()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOpenExistingIndex() throws IOException {
|
||||
try (IndexWriter writer = open("reused")) {
|
||||
addDoc(writer, "Dent");
|
||||
}
|
||||
try (IndexWriter writer = open("reused")) {
|
||||
assertThat(writer.getFieldNames()).contains("hitchhiker");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseAnalyzerFromFactory() throws IOException {
|
||||
try (IndexWriter writer = open("new-index")) {
|
||||
assertThat(writer.getAnalyzer()).isInstanceOf(SimpleAnalyzer.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOpenIndexForRead() throws IOException {
|
||||
try (IndexWriter writer = open("idx-for-read")) {
|
||||
addDoc(writer, "Dent");
|
||||
}
|
||||
|
||||
try (IndexReader reader = indexOpener.openForRead("idx-for-read")) {
|
||||
assertThat(reader.numDocs()).isOne();
|
||||
}
|
||||
}
|
||||
|
||||
private void addDoc(IndexWriter writer, String name) throws IOException {
|
||||
Document doc = new Document();
|
||||
doc.add(new TextField("hitchhiker", name, Field.Store.YES));
|
||||
writer.addDocument(doc);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.search;
|
||||
|
||||
import lombok.Value;
|
||||
import org.apache.lucene.analysis.standard.StandardAnalyzer;
|
||||
import org.apache.lucene.index.DirectoryReader;
|
||||
import org.apache.lucene.index.IndexWriter;
|
||||
import org.apache.lucene.index.IndexWriterConfig;
|
||||
import org.apache.lucene.store.ByteBuffersDirectory;
|
||||
import org.apache.lucene.store.Directory;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.in;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class IndexQueueTest {
|
||||
|
||||
private Directory directory;
|
||||
|
||||
private IndexQueue queue;
|
||||
|
||||
@BeforeEach
|
||||
void createQueue() throws IOException {
|
||||
directory = new ByteBuffersDirectory();
|
||||
IndexOpener opener = mock(IndexOpener.class);
|
||||
when(opener.openForWrite(any(IndexParams.class))).thenAnswer(ic -> {
|
||||
IndexWriterConfig config = new IndexWriterConfig(new StandardAnalyzer());
|
||||
config.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
|
||||
return new IndexWriter(directory, config);
|
||||
});
|
||||
|
||||
LuceneIndexFactory indexFactory = new LuceneIndexFactory(opener);
|
||||
queue = new IndexQueue(indexFactory);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void closeQueue() throws IOException {
|
||||
queue.close();
|
||||
directory.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWriteToIndex() throws Exception {
|
||||
try (Index<Account> index = getIndex(Account.class)) {
|
||||
index.store(Id.of("tricia"), null, new Account("tricia", "Trillian", "McMillan"));
|
||||
index.store(Id.of("dent"), null, new Account("dent", "Arthur", "Dent"));
|
||||
}
|
||||
assertDocCount(2);
|
||||
}
|
||||
|
||||
private <T> Index<T> getIndex(Class<T> type) {
|
||||
SearchableTypeResolver resolver = new SearchableTypeResolver(type);
|
||||
LuceneSearchableType searchableType = resolver.resolve(type);
|
||||
IndexParams indexParams = new IndexParams("default", searchableType, IndexOptions.defaults());
|
||||
return queue.getQueuedIndex(indexParams);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWriteMultiThreaded() throws Exception {
|
||||
ExecutorService executorService = Executors.newFixedThreadPool(4);
|
||||
for (int i = 0; i < 20; i++) {
|
||||
executorService.execute(new IndexNumberTask(i));
|
||||
}
|
||||
executorService.execute(() -> {
|
||||
try (Index<IndexedNumber> index = getIndex(IndexedNumber.class)) {
|
||||
index.delete().byType().byId(Id.of(String.valueOf(12)));
|
||||
}
|
||||
});
|
||||
executorService.shutdown();
|
||||
|
||||
assertDocCount(19);
|
||||
}
|
||||
|
||||
private void assertDocCount(int expectedCount) throws IOException {
|
||||
// wait until all tasks are finished
|
||||
await().until(() -> queue.getSize() == 0);
|
||||
try (DirectoryReader reader = DirectoryReader.open(directory)) {
|
||||
assertThat(reader.numDocs()).isEqualTo(expectedCount);
|
||||
}
|
||||
}
|
||||
|
||||
@Value
|
||||
@IndexedType
|
||||
public static class Account {
|
||||
@Indexed
|
||||
String username;
|
||||
@Indexed
|
||||
String firstName;
|
||||
@Indexed
|
||||
String lastName;
|
||||
}
|
||||
|
||||
@Value
|
||||
@IndexedType
|
||||
public static class IndexedNumber {
|
||||
@Indexed
|
||||
int value;
|
||||
}
|
||||
|
||||
public class IndexNumberTask implements Runnable {
|
||||
|
||||
private final int number;
|
||||
|
||||
public IndexNumberTask(int number) {
|
||||
this.number = number;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try (Index<IndexedNumber> index = getIndex(IndexedNumber.class)) {
|
||||
index.store(Id.of(String.valueOf(number)), null, new IndexedNumber(number));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.search;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LuceneIndexFactoryTest {
|
||||
|
||||
@Mock
|
||||
private IndexManager indexManager;
|
||||
|
||||
@InjectMocks
|
||||
private LuceneIndexFactory indexFactory;
|
||||
|
||||
@Test
|
||||
void shouldCallOpenOnCreation() {
|
||||
LuceneIndex<Repository> index = indexFactory.create(params("default", Repository.class));
|
||||
assertThat(index.getWriter().getUsageCounter()).isOne();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCallOpenOnReturn() {
|
||||
indexFactory.create(params("default", Repository.class));
|
||||
indexFactory.create(params("default", Repository.class));
|
||||
LuceneIndex<Repository> index = indexFactory.create(params("default", Repository.class));
|
||||
assertThat(index.getWriter().getUsageCounter()).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("AssertBetweenInconvertibleTypes")
|
||||
void shouldReturnDifferentIndexForDifferentTypes() {
|
||||
LuceneIndex<Repository> repository = indexFactory.create(params("default", Repository.class));
|
||||
LuceneIndex<User> user = indexFactory.create(params("default", User.class));
|
||||
|
||||
assertThat(repository).isNotSameAs(user);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnDifferentIndexForDifferentIndexNames() {
|
||||
LuceneIndex<Repository> def = indexFactory.create(params("default", Repository.class));
|
||||
LuceneIndex<Repository> other = indexFactory.create(params("other", Repository.class));
|
||||
|
||||
assertThat(def).isNotSameAs(other);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnSameIndex() {
|
||||
LuceneIndex<Repository> one = indexFactory.create(params("default", Repository.class));
|
||||
LuceneIndex<Repository> two = indexFactory.create(params("default", Repository.class));
|
||||
assertThat(one).isSameAs(two);
|
||||
}
|
||||
|
||||
private IndexParams params(String indexName, Class<?> type) {
|
||||
return new IndexParams(indexName, SearchableTypes.create(type), IndexOptions.defaults());
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ package sonia.scm.search;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import lombok.Value;
|
||||
import org.apache.lucene.analysis.standard.StandardAnalyzer;
|
||||
import org.apache.lucene.document.Document;
|
||||
import org.apache.lucene.index.DirectoryReader;
|
||||
import org.apache.lucene.index.IndexWriter;
|
||||
import org.apache.lucene.index.IndexWriterConfig;
|
||||
@@ -39,12 +38,26 @@ import org.apache.lucene.search.TopDocs;
|
||||
import org.apache.lucene.store.ByteBuffersDirectory;
|
||||
import org.apache.lucene.store.Directory;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.repository.RepositoryType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static sonia.scm.search.FieldNames.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.search.FieldNames.ID;
|
||||
import static sonia.scm.search.FieldNames.PERMISSION;
|
||||
import static sonia.scm.search.FieldNames.REPOSITORY;
|
||||
|
||||
class LuceneIndexTest {
|
||||
|
||||
@@ -77,15 +90,6 @@ class LuceneIndexTest {
|
||||
assertHits("value", "content", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStoreUidOfObject() throws IOException {
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
index.store(ONE, null, new Storable("Awesome content which should be indexed"));
|
||||
}
|
||||
|
||||
assertHits(UID, "one/storable", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStoreIdOfObject() throws IOException {
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
@@ -104,15 +108,6 @@ class LuceneIndexTest {
|
||||
assertHits(REPOSITORY, "4211", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStoreTypeOfObject() throws IOException {
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
index.store(ONE, null, new Storable("Some other text"));
|
||||
}
|
||||
|
||||
assertHits(TYPE, "storable", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteById() throws IOException {
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
@@ -120,131 +115,54 @@ class LuceneIndexTest {
|
||||
}
|
||||
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
index.delete().byType().byId(ONE);
|
||||
index.delete().byId(ONE);
|
||||
}
|
||||
|
||||
assertHits(ID, "one", 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteAllByType() throws IOException {
|
||||
void shouldDeleteByIdAndRepository() throws IOException {
|
||||
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
|
||||
Repository puzzle42 = RepositoryTestData.createHeartOfGold();
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
index.store(ONE, null, new Storable("content"));
|
||||
index.store(Id.of("two"), null, new Storable("content"));
|
||||
}
|
||||
|
||||
try (LuceneIndex<OtherStorable> index = createIndex(OtherStorable.class)) {
|
||||
index.store(Id.of("three"), null, new OtherStorable("content"));
|
||||
index.store(ONE.withRepository(heartOfGold), null, new Storable("content"));
|
||||
index.store(ONE.withRepository(puzzle42), null, new Storable("content"));
|
||||
}
|
||||
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
index.delete().byType().all();
|
||||
index.delete().byId(ONE.withRepository(heartOfGold));
|
||||
}
|
||||
|
||||
assertHits("value", "content", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteByIdAnyType() throws IOException {
|
||||
void shouldDeleteAll() throws IOException {
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
index.store(ONE, null, new Storable("Some text"));
|
||||
}
|
||||
|
||||
try (LuceneIndex<OtherStorable> index = createIndex(OtherStorable.class)) {
|
||||
index.store(ONE, null, new OtherStorable("Some other text"));
|
||||
index.store(ONE, null, new Storable("content"));
|
||||
index.store(TWO, null, new Storable("content"));
|
||||
}
|
||||
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
index.delete().byType().byId(ONE);
|
||||
index.delete().all();
|
||||
}
|
||||
|
||||
assertHits(ID, "one", 1);
|
||||
ScoreDoc[] docs = assertHits(ID, "one", 1);
|
||||
Document doc = doc(docs[0].doc);
|
||||
assertThat(doc.get("value")).isEqualTo("Some other text");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteByIdAndRepository() throws IOException {
|
||||
Id withRepository = ONE.withRepository("4211");
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
index.store(ONE, null, new Storable("Some other text"));
|
||||
index.store(withRepository, null, new Storable("New stuff"));
|
||||
}
|
||||
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
index.delete().byType().byId(withRepository);
|
||||
}
|
||||
|
||||
ScoreDoc[] docs = assertHits(ID, "one", 1);
|
||||
Document doc = doc(docs[0].doc);
|
||||
assertThat(doc.get("value")).isEqualTo("Some other text");
|
||||
assertHits("value", "content", 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteByRepository() throws IOException {
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
index.store(ONE.withRepository("4211"), null, new Storable("Some other text"));
|
||||
index.store(ONE.withRepository("4212"), null, new Storable("New stuff"));
|
||||
index.store(ONE.withRepository("4211"), null, new Storable("content"));
|
||||
index.store(TWO.withRepository("4212"), null, new Storable("content"));
|
||||
}
|
||||
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
index.delete().byType().byRepository("4212");
|
||||
index.delete().byRepository("4212");
|
||||
}
|
||||
|
||||
assertHits(ID, "one", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteByRepositoryAndType() throws IOException {
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
index.store(ONE.withRepository("4211"), null, new Storable("some text"));
|
||||
index.store(TWO.withRepository("4211"), null, new Storable("some text"));
|
||||
}
|
||||
|
||||
try (LuceneIndex<OtherStorable> index = createIndex(OtherStorable.class)) {
|
||||
index.store(ONE.withRepository("4211"), null, new OtherStorable("some text"));
|
||||
}
|
||||
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
index.delete().byType().byRepository("4211");
|
||||
}
|
||||
|
||||
ScoreDoc[] docs = assertHits("value", "text", 1);
|
||||
Document doc = doc(docs[0].doc);
|
||||
assertThat(doc.get(TYPE)).isEqualTo("otherStorable");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteAllByRepository() throws IOException {
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
index.store(ONE.withRepository("4211"), null, new Storable("some text"));
|
||||
index.store(TWO.withRepository("4211"), null, new Storable("some text"));
|
||||
}
|
||||
|
||||
try (LuceneIndex<OtherStorable> index = createIndex(OtherStorable.class)) {
|
||||
index.store(ONE.withRepository("4211"), null, new OtherStorable("some text"));
|
||||
}
|
||||
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
index.delete().allTypes().byRepository("4211");
|
||||
}
|
||||
|
||||
assertHits("value", "text", 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteAllByTypeName() throws IOException {
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
index.store(ONE, null, new Storable("some text"));
|
||||
index.store(TWO, null, new Storable("some text"));
|
||||
}
|
||||
|
||||
try (LuceneIndex<OtherStorable> index = createIndex(OtherStorable.class)) {
|
||||
index.delete().allTypes().byTypeName("storable");
|
||||
}
|
||||
|
||||
assertHits("value", "text", 0);
|
||||
assertHits("value", "content", 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -256,12 +174,69 @@ class LuceneIndexTest {
|
||||
assertHits(PERMISSION, "repo:4211:read", 1);
|
||||
}
|
||||
|
||||
private Document doc(int doc) throws IOException {
|
||||
try (DirectoryReader reader = DirectoryReader.open(directory)) {
|
||||
return reader.document(doc);
|
||||
@Test
|
||||
void shouldReturnDetails() {
|
||||
try (LuceneIndex<Storable> index = createIndex(Storable.class)) {
|
||||
IndexDetails details = index.getDetails();
|
||||
assertThat(details.getType()).isEqualTo(Storable.class);
|
||||
assertThat(details.getName()).isEqualTo("default");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ExceptionTests {
|
||||
|
||||
@Mock
|
||||
private IndexWriter writer;
|
||||
|
||||
private LuceneIndex<Storable> index;
|
||||
|
||||
@BeforeEach
|
||||
void setUpIndex() {
|
||||
index = createIndex(Storable.class, () -> writer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowSearchEngineExceptionOnStore() throws IOException {
|
||||
when(writer.updateDocument(any(), any())).thenThrow(new IOException("failed to store"));
|
||||
|
||||
Storable storable = new Storable("Some other text");
|
||||
assertThrows(SearchEngineException.class, () -> index.store(ONE, null, storable));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowSearchEngineExceptionOnDeleteById() throws IOException {
|
||||
when(writer.deleteDocuments(any(Term.class))).thenThrow(new IOException("failed to delete"));
|
||||
|
||||
Index.Deleter deleter = index.delete();
|
||||
assertThrows(SearchEngineException.class, () -> deleter.byId(ONE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowSearchEngineExceptionOnDeleteAll() throws IOException {
|
||||
when(writer.deleteAll()).thenThrow(new IOException("failed to delete"));
|
||||
|
||||
Index.Deleter deleter = index.delete();
|
||||
assertThrows(SearchEngineException.class, deleter::all);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowSearchEngineExceptionOnDeleteByRepository() throws IOException {
|
||||
when(writer.deleteDocuments(any(Term.class))).thenThrow(new IOException("failed to delete"));
|
||||
|
||||
Index.Deleter deleter = index.delete();
|
||||
assertThrows(SearchEngineException.class, () -> deleter.byRepository("42"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowSearchEngineExceptionOnClose() throws IOException {
|
||||
doThrow(new IOException("failed to delete")).when(writer).close();
|
||||
assertThrows(SearchEngineException.class, () -> index.close());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
private ScoreDoc[] assertHits(String field, String value, int expectedHits) throws IOException {
|
||||
try (DirectoryReader reader = DirectoryReader.open(directory)) {
|
||||
@@ -272,15 +247,25 @@ class LuceneIndexTest {
|
||||
}
|
||||
}
|
||||
|
||||
private <T> LuceneIndex<T> createIndex(Class<T> type) throws IOException {
|
||||
SearchableTypeResolver resolver = new SearchableTypeResolver(type);
|
||||
return new LuceneIndex<>(resolver.resolve(type), createWriter());
|
||||
private <T> LuceneIndex<T> createIndex(Class<T> type) {
|
||||
return createIndex(type, this::createWriter);
|
||||
}
|
||||
|
||||
private IndexWriter createWriter() throws IOException {
|
||||
private <T> LuceneIndex<T> createIndex(Class<T> type, Supplier<IndexWriter> writerFactor) {
|
||||
SearchableTypeResolver resolver = new SearchableTypeResolver(type);
|
||||
return new LuceneIndex<>(
|
||||
new IndexParams("default", resolver.resolve(type), IndexOptions.defaults()), writerFactor
|
||||
);
|
||||
}
|
||||
|
||||
private IndexWriter createWriter() {
|
||||
IndexWriterConfig config = new IndexWriterConfig(new StandardAnalyzer());
|
||||
config.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND);
|
||||
return new IndexWriter(directory, config);
|
||||
try {
|
||||
return new IndexWriter(directory, config);
|
||||
} catch (IOException ex) {
|
||||
throw new SearchEngineException("failed to open index writer", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Value
|
||||
@@ -290,11 +275,4 @@ class LuceneIndexTest {
|
||||
String value;
|
||||
}
|
||||
|
||||
@Value
|
||||
@IndexedType
|
||||
private static class OtherStorable {
|
||||
@Indexed
|
||||
String value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.search;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Injector;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LuceneInjectingIndexTaskTest {
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private LuceneIndexFactory indexFactory;
|
||||
|
||||
private static String capturedValue;
|
||||
|
||||
private final SearchableTypeResolver resolver = new SearchableTypeResolver(User.class);
|
||||
|
||||
@BeforeEach
|
||||
void cleanUp() {
|
||||
capturedValue = "notAsExpected";
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInjectAndUpdate() {
|
||||
Injector injector = createInjector();
|
||||
|
||||
LuceneSearchableType searchableType = resolver.resolve(User.class);
|
||||
IndexParams params = new IndexParams("default", searchableType, IndexOptions.defaults());
|
||||
|
||||
LuceneInjectingIndexTask task = new LuceneInjectingIndexTask(params, InjectingTask.class);
|
||||
injector.injectMembers(task);
|
||||
|
||||
task.run();
|
||||
|
||||
assertThat(capturedValue).isEqualTo("runAsExpected");
|
||||
}
|
||||
|
||||
private Injector createInjector() {
|
||||
return Guice.createInjector(new AbstractModule() {
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(SearchableTypeResolver.class).toInstance(resolver);
|
||||
bind(LuceneIndexFactory.class).toInstance(indexFactory);
|
||||
bind(String.class).toInstance("runAsExpected");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static class InjectingTask implements IndexTask<String> {
|
||||
|
||||
private final String value;
|
||||
|
||||
@Inject
|
||||
public InjectingTask(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Index<String> index) {
|
||||
capturedValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -42,7 +42,6 @@ import org.apache.lucene.index.IndexWriter;
|
||||
import org.apache.lucene.index.IndexWriterConfig;
|
||||
import org.apache.lucene.store.ByteBuffersDirectory;
|
||||
import org.apache.lucene.store.Directory;
|
||||
import org.apache.shiro.authz.AuthorizationException;
|
||||
import org.github.sdorra.jse.ShiroExtension;
|
||||
import org.github.sdorra.jse.SubjectAware;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -70,7 +69,7 @@ class LuceneQueryBuilderTest {
|
||||
private Directory directory;
|
||||
|
||||
@Mock
|
||||
private IndexOpener opener;
|
||||
private IndexManager opener;
|
||||
|
||||
@BeforeEach
|
||||
void setUpDirectory() {
|
||||
@@ -181,17 +180,6 @@ class LuceneQueryBuilderTest {
|
||||
assertThat(result.getTotalHits()).isOne();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreHitsOfOtherType() throws IOException {
|
||||
try (IndexWriter writer = writer()) {
|
||||
writer.addDocument(inetOrgPersonDoc("Arthur", "Dent", "Arthur Dent", "4211"));
|
||||
writer.addDocument(personDoc("Dent"));
|
||||
}
|
||||
|
||||
QueryResult result = query(InetOrgPerson.class, "Dent");
|
||||
assertThat(result.getTotalHits()).isOne();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowQueryParseExceptionOnInvalidQuery() throws IOException {
|
||||
try (IndexWriter writer = writer()) {
|
||||
@@ -251,17 +239,6 @@ class LuceneQueryBuilderTest {
|
||||
assertThat(result.getHits()).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnOnlyHitsOfTypeForExpertQuery() throws IOException {
|
||||
try (IndexWriter writer = writer()) {
|
||||
writer.addDocument(inetOrgPersonDoc("Ford", "Prefect", "Ford Prefect", "4211"));
|
||||
writer.addDocument(personDoc("Prefect"));
|
||||
}
|
||||
|
||||
QueryResult result = query(InetOrgPerson.class, "lastName:prefect");
|
||||
assertThat(result.getTotalHits()).isEqualTo(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnOnlyPermittedHits() throws IOException {
|
||||
try (IndexWriter writer = writer()) {
|
||||
@@ -302,10 +279,11 @@ class LuceneQueryBuilderTest {
|
||||
|
||||
QueryResult result;
|
||||
try (DirectoryReader reader = DirectoryReader.open(directory)) {
|
||||
when(opener.openForRead("default")).thenReturn(reader);
|
||||
SearchableTypeResolver resolver = new SearchableTypeResolver(Simple.class);
|
||||
LuceneSearchableType searchableType = resolver.resolve(Simple.class);
|
||||
when(opener.openForRead(searchableType, "default")).thenReturn(reader);
|
||||
LuceneQueryBuilder<Simple> builder = new LuceneQueryBuilder<>(
|
||||
opener, "default", resolver.resolve(Simple.class), new StandardAnalyzer()
|
||||
opener, "default", searchableType, new StandardAnalyzer()
|
||||
);
|
||||
result = builder.repository("cde").execute("content:awesome");
|
||||
}
|
||||
@@ -560,9 +538,9 @@ class LuceneQueryBuilderTest {
|
||||
|
||||
private <T> long count(Class<T> type, String queryString) throws IOException {
|
||||
try (DirectoryReader reader = DirectoryReader.open(directory)) {
|
||||
lenient().when(opener.openForRead("default")).thenReturn(reader);
|
||||
SearchableTypeResolver resolver = new SearchableTypeResolver(type);
|
||||
LuceneSearchableType searchableType = resolver.resolve(type);
|
||||
lenient().when(opener.openForRead(searchableType, "default")).thenReturn(reader);
|
||||
LuceneQueryBuilder<T> builder = new LuceneQueryBuilder<T>(
|
||||
opener, "default", searchableType, new StandardAnalyzer()
|
||||
);
|
||||
@@ -572,10 +550,11 @@ class LuceneQueryBuilderTest {
|
||||
|
||||
private <T> QueryResult query(Class<?> type, String queryString, Integer start, Integer limit) throws IOException {
|
||||
try (DirectoryReader reader = DirectoryReader.open(directory)) {
|
||||
lenient().when(opener.openForRead("default")).thenReturn(reader);
|
||||
SearchableTypeResolver resolver = new SearchableTypeResolver(type);
|
||||
LuceneSearchableType searchableType = resolver.resolve(type);
|
||||
LuceneQueryBuilder<T> builder = new LuceneQueryBuilder<T>(
|
||||
|
||||
lenient().when(opener.openForRead(searchableType, "default")).thenReturn(reader);
|
||||
LuceneQueryBuilder<T> builder = new LuceneQueryBuilder<>(
|
||||
opener, "default", searchableType, new StandardAnalyzer()
|
||||
);
|
||||
if (start != null) {
|
||||
@@ -597,14 +576,14 @@ class LuceneQueryBuilderTest {
|
||||
private Document simpleDoc(String content) {
|
||||
Document document = new Document();
|
||||
document.add(new TextField("content", content, Field.Store.YES));
|
||||
document.add(new StringField(FieldNames.TYPE, "simple", Field.Store.YES));
|
||||
// document.add(new StringField(FieldNames.TYPE, "simple", Field.Store.YES));
|
||||
return document;
|
||||
}
|
||||
|
||||
private Document permissionDoc(String content, String permission) {
|
||||
Document document = new Document();
|
||||
document.add(new TextField("content", content, Field.Store.YES));
|
||||
document.add(new StringField(FieldNames.TYPE, "simple", Field.Store.YES));
|
||||
// document.add(new StringField(FieldNames.TYPE, "simple", Field.Store.YES));
|
||||
document.add(new StringField(FieldNames.PERMISSION, permission, Field.Store.YES));
|
||||
return document;
|
||||
}
|
||||
@@ -612,7 +591,7 @@ class LuceneQueryBuilderTest {
|
||||
private Document repositoryDoc(String content, String repository) {
|
||||
Document document = new Document();
|
||||
document.add(new TextField("content", content, Field.Store.YES));
|
||||
document.add(new StringField(FieldNames.TYPE, "simple", Field.Store.YES));
|
||||
// document.add(new StringField(FieldNames.TYPE, "simple", Field.Store.YES));
|
||||
document.add(new StringField(FieldNames.REPOSITORY, repository, Field.Store.YES));
|
||||
return document;
|
||||
}
|
||||
@@ -624,14 +603,14 @@ class LuceneQueryBuilderTest {
|
||||
document.add(new TextField("displayName", displayName, Field.Store.YES));
|
||||
document.add(new TextField("carLicense", carLicense, Field.Store.YES));
|
||||
document.add(new StringField(FieldNames.ID, lastName, Field.Store.YES));
|
||||
document.add(new StringField(FieldNames.TYPE, "inetOrgPerson", Field.Store.YES));
|
||||
// document.add(new StringField(FieldNames.TYPE, "inetOrgPerson", Field.Store.YES));
|
||||
return document;
|
||||
}
|
||||
|
||||
private Document personDoc(String lastName) {
|
||||
Document document = new Document();
|
||||
document.add(new TextField("lastName", lastName, Field.Store.YES));
|
||||
document.add(new StringField(FieldNames.TYPE, "person", Field.Store.YES));
|
||||
// document.add(new StringField(FieldNames.TYPE, "person", Field.Store.YES));
|
||||
return document;
|
||||
}
|
||||
|
||||
@@ -644,14 +623,14 @@ class LuceneQueryBuilderTest {
|
||||
document.add(new StringField("boolValue", String.valueOf(boolValue), Field.Store.YES));
|
||||
document.add(new LongPoint("instantValue", instantValue.toEpochMilli()));
|
||||
document.add(new StoredField("instantValue", instantValue.toEpochMilli()));
|
||||
document.add(new StringField(FieldNames.TYPE, "types", Field.Store.YES));
|
||||
// document.add(new StringField(FieldNames.TYPE, "types", Field.Store.YES));
|
||||
return document;
|
||||
}
|
||||
|
||||
private Document denyDoc(String value) {
|
||||
Document document = new Document();
|
||||
document.add(new TextField("value", value, Field.Store.YES));
|
||||
document.add(new StringField(FieldNames.TYPE, "deny", Field.Store.YES));
|
||||
// document.add(new StringField(FieldNames.TYPE, "deny", Field.Store.YES));
|
||||
return document;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,12 +27,20 @@ package sonia.scm.search;
|
||||
import org.apache.shiro.authz.AuthorizationException;
|
||||
import org.github.sdorra.jse.ShiroExtension;
|
||||
import org.github.sdorra.jse.SubjectAware;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.work.CentralWorkQueue;
|
||||
import sonia.scm.work.Task;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
@@ -46,176 +54,329 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SubjectAware("trillian")
|
||||
@ExtendWith({MockitoExtension.class, ShiroExtension.class})
|
||||
class LuceneSearchEngineTest {
|
||||
|
||||
@Mock
|
||||
private IndexManager indexManager;
|
||||
|
||||
@Mock
|
||||
private SearchableTypeResolver resolver;
|
||||
|
||||
@Mock
|
||||
private IndexQueue indexQueue;
|
||||
|
||||
@Mock
|
||||
private LuceneQueryBuilderFactory queryBuilderFactory;
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private CentralWorkQueue centralWorkQueue;
|
||||
|
||||
@InjectMocks
|
||||
private LuceneSearchEngine searchEngine;
|
||||
|
||||
@Mock
|
||||
private LuceneSearchableType searchableType;
|
||||
|
||||
@Test
|
||||
void shouldDelegateGetSearchableTypes() {
|
||||
List<LuceneSearchableType> mockedTypes = Collections.singletonList(searchableType("repository"));
|
||||
when(resolver.getSearchableTypes()).thenReturn(mockedTypes);
|
||||
@Nested
|
||||
class GetSearchableTypesTests {
|
||||
|
||||
Collection<SearchableType> searchableTypes = searchEngine.getSearchableTypes();
|
||||
@Test
|
||||
void shouldDelegateGetSearchableTypes() {
|
||||
List<LuceneSearchableType> mockedTypes = Collections.singletonList(searchableType("repository"));
|
||||
when(resolver.getSearchableTypes()).thenReturn(mockedTypes);
|
||||
|
||||
assertThat(searchableTypes).containsAll(mockedTypes);
|
||||
Collection<SearchableType> searchableTypes = searchEngine.getSearchableTypes();
|
||||
|
||||
assertThat(searchableTypes).containsAll(mockedTypes);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(value = "dent", permissions = "user:list")
|
||||
void shouldExcludeTypesWithoutPermission() {
|
||||
LuceneSearchableType repository = searchableType("repository");
|
||||
LuceneSearchableType user = searchableType("user", "user:list");
|
||||
LuceneSearchableType group = searchableType("group", "group:list");
|
||||
List<LuceneSearchableType> mockedTypes = Arrays.asList(repository, user, group);
|
||||
when(resolver.getSearchableTypes()).thenReturn(mockedTypes);
|
||||
|
||||
Collection<SearchableType> searchableTypes = searchEngine.getSearchableTypes();
|
||||
|
||||
assertThat(searchableTypes).containsOnly(repository, user);
|
||||
}
|
||||
|
||||
private LuceneSearchableType searchableType(String name) {
|
||||
return searchableType(name, null);
|
||||
}
|
||||
|
||||
private LuceneSearchableType searchableType(String name, String permission) {
|
||||
LuceneSearchableType searchableType = mock(LuceneSearchableType.class);
|
||||
lenient().when(searchableType.getName()).thenReturn(name);
|
||||
when(searchableType.getPermission()).thenReturn(Optional.ofNullable(permission));
|
||||
return searchableType;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SubjectAware(value = "dent", permissions = "user:list")
|
||||
void shouldExcludeTypesWithoutPermission() {
|
||||
LuceneSearchableType repository = searchableType("repository");
|
||||
LuceneSearchableType user = searchableType("user", "user:list");
|
||||
LuceneSearchableType group = searchableType("group", "group:list");
|
||||
List<LuceneSearchableType> mockedTypes = Arrays.asList(repository, user, group);
|
||||
when(resolver.getSearchableTypes()).thenReturn(mockedTypes);
|
||||
@Nested
|
||||
class SearchTests {
|
||||
|
||||
Collection<SearchableType> searchableTypes = searchEngine.getSearchableTypes();
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void shouldDelegateSearchWithDefaults() {
|
||||
LuceneQueryBuilder<Repository> mockedBuilder = mock(LuceneQueryBuilder.class);
|
||||
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
||||
|
||||
assertThat(searchableTypes).containsOnly(repository, user);
|
||||
IndexParams params = new IndexParams("default", searchableType, IndexOptions.defaults());
|
||||
when(queryBuilderFactory.<Repository>create(params)).thenReturn(mockedBuilder);
|
||||
|
||||
QueryBuilder<Repository> queryBuilder = searchEngine.forType(Repository.class).search();
|
||||
|
||||
assertThat(queryBuilder).isSameAs(mockedBuilder);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void shouldDelegateSearch() {
|
||||
IndexOptions options = IndexOptions.naturalLanguage(Locale.GERMAN);
|
||||
|
||||
LuceneQueryBuilder<Repository> mockedBuilder = mock(LuceneQueryBuilder.class);
|
||||
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
||||
|
||||
IndexParams params = new IndexParams("idx", searchableType, options);
|
||||
when(queryBuilderFactory.<Repository>create(params)).thenReturn(mockedBuilder);
|
||||
|
||||
QueryBuilder<Repository> queryBuilder = searchEngine.forType(Repository.class).withIndex("idx").withOptions(options).search();
|
||||
|
||||
assertThat(queryBuilder).isSameAs(mockedBuilder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWithoutRequiredPermission() {
|
||||
when(searchableType.getPermission()).thenReturn(Optional.of("repository:read"));
|
||||
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
||||
|
||||
SearchEngine.ForType<Repository> forType = searchEngine.forType(Repository.class);
|
||||
assertThrows(AuthorizationException.class, forType::search);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
@SubjectAware(permissions = "repository:read")
|
||||
void shouldNotFailWithRequiredPermission() {
|
||||
when(searchableType.getPermission()).thenReturn(Optional.of("repository:read"));
|
||||
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
||||
|
||||
LuceneQueryBuilder<Object> mockedBuilder = mock(LuceneQueryBuilder.class);
|
||||
when(queryBuilderFactory.create(any())).thenReturn(mockedBuilder);
|
||||
|
||||
SearchEngine.ForType<Repository> forType = searchEngine.forType(Repository.class);
|
||||
assertThat(forType.search()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWithTypeNameWithoutRequiredPermission() {
|
||||
when(searchableType.getPermission()).thenReturn(Optional.of("repository:read"));
|
||||
when(resolver.resolveByName("repository")).thenReturn(searchableType);
|
||||
|
||||
SearchEngine.ForType<Object> forType = searchEngine.forType("repository");
|
||||
assertThrows(AuthorizationException.class, forType::search);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
@SubjectAware(permissions = "repository:read")
|
||||
void shouldNotFailWithTypeNameAndRequiredPermission() {
|
||||
when(searchableType.getPermission()).thenReturn(Optional.of("repository:read"));
|
||||
when(resolver.resolveByName("repository")).thenReturn(searchableType);
|
||||
|
||||
LuceneQueryBuilder<Object> mockedBuilder = mock(LuceneQueryBuilder.class);
|
||||
when(queryBuilderFactory.create(any())).thenReturn(mockedBuilder);
|
||||
|
||||
SearchEngine.ForType<Object> forType = searchEngine.forType("repository");
|
||||
assertThat(forType.search()).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
private LuceneSearchableType searchableType(String name) {
|
||||
return searchableType(name, null);
|
||||
@Nested
|
||||
class IndexTests {
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Task> taskCaptor;
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private CentralWorkQueue.Enqueue enqueue;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(centralWorkQueue.append()).thenReturn(enqueue);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSubmitSimpleTask() {
|
||||
mockType();
|
||||
|
||||
searchEngine.forType(Repository.class).update(index -> {});
|
||||
|
||||
verifyTaskSubmitted(LuceneSimpleIndexTask.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSubmitInjectingTask() {
|
||||
mockType();
|
||||
|
||||
searchEngine.forType(Repository.class).update(DummyIndexTask.class);
|
||||
|
||||
verifyTaskSubmitted(LuceneInjectingIndexTask.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLockTypeAndDefaultIndex() {
|
||||
mockType();
|
||||
|
||||
searchEngine.forType(Repository.class).update(DummyIndexTask.class);
|
||||
|
||||
verify(enqueue).locks("repository-default-index");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLockTypeAndIndex() {
|
||||
mockType();
|
||||
|
||||
searchEngine.forType(Repository.class).withIndex("sample").update(DummyIndexTask.class);
|
||||
|
||||
verify(enqueue).locks("repository-sample-index");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLockSpecificResource() {
|
||||
mockType();
|
||||
|
||||
searchEngine.forType(Repository.class).forResource("one").update(DummyIndexTask.class);
|
||||
|
||||
verify(enqueue).locks("repository-default-index", "one");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLockMultipleSpecificResources() {
|
||||
mockType();
|
||||
|
||||
searchEngine.forType(Repository.class)
|
||||
.forResource("one")
|
||||
.forResource("two")
|
||||
.update(DummyIndexTask.class);
|
||||
|
||||
verify(enqueue).locks("repository-default-index", "one");
|
||||
verify(enqueue).locks("repository-default-index", "two");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBatchSimpleTask() {
|
||||
mockDetails(new LuceneIndexDetails(Repository.class, "default"));
|
||||
|
||||
searchEngine.forIndices().batch(index -> {});
|
||||
|
||||
verifyTaskSubmitted(LuceneSimpleIndexTask.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBatchAndLock() {
|
||||
mockDetails(new LuceneIndexDetails(Repository.class, "default"));
|
||||
|
||||
searchEngine.forIndices().batch(index -> {});
|
||||
|
||||
verify(enqueue).locks("repository-default-index");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBatchAndLockSpecificResource() {
|
||||
mockDetails(new LuceneIndexDetails(Repository.class, "default"));
|
||||
|
||||
searchEngine.forIndices().forResource("one").batch(index -> {});
|
||||
|
||||
verify(enqueue).locks("repository-default-index", "one");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBatchAndLockMultipleSpecificResources() {
|
||||
mockDetails(new LuceneIndexDetails(Repository.class, "default"));
|
||||
|
||||
searchEngine.forIndices().forResource("one").forResource("two").batch(index -> {});
|
||||
|
||||
verify(enqueue).locks("repository-default-index", "one");
|
||||
verify(enqueue).locks("repository-default-index", "two");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBatchInjectingTask() {
|
||||
mockDetails(new LuceneIndexDetails(Repository.class, "default"));
|
||||
|
||||
searchEngine.forIndices().batch(DummyIndexTask.class);
|
||||
|
||||
verifyTaskSubmitted(LuceneInjectingIndexTask.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBatchMultipleTasks() {
|
||||
mockDetails(
|
||||
new LuceneIndexDetails(Repository.class, "default"),
|
||||
new LuceneIndexDetails(User.class, "default")
|
||||
);
|
||||
|
||||
searchEngine.forIndices().batch(index -> {});
|
||||
|
||||
verify(enqueue.runAsAdmin(), times(2)).enqueue(any(Task.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFilterWithPredicate() {
|
||||
mockDetails(
|
||||
new LuceneIndexDetails(Repository.class, "default"),
|
||||
new LuceneIndexDetails(User.class, "default")
|
||||
);
|
||||
|
||||
searchEngine.forIndices()
|
||||
.matching(details -> details.getType() == Repository.class)
|
||||
.batch(index -> {});
|
||||
|
||||
verify(enqueue.runAsAdmin()).enqueue(any(Task.class));
|
||||
}
|
||||
|
||||
private <T extends IndexDetails> void mockDetails(LuceneIndexDetails... details) {
|
||||
for (LuceneIndexDetails detail : details) {
|
||||
mockType(detail.getType());
|
||||
}
|
||||
when(indexManager.all()).thenAnswer(ic -> Arrays.asList(details));
|
||||
}
|
||||
|
||||
private void verifyTaskSubmitted(Class<? extends Task> typeOfTask) {
|
||||
verify(enqueue.runAsAdmin()).enqueue(taskCaptor.capture());
|
||||
|
||||
Task task = taskCaptor.getValue();
|
||||
assertThat(task).isInstanceOf(typeOfTask);
|
||||
}
|
||||
|
||||
private void mockType() {
|
||||
mockType(Repository.class);
|
||||
}
|
||||
|
||||
private void mockType(Class<?> type){
|
||||
LuceneSearchableType searchableType = mock(LuceneSearchableType.class);
|
||||
lenient().when(searchableType.getType()).thenAnswer(ic -> type);
|
||||
lenient().when(searchableType.getName()).thenReturn(type.getSimpleName().toLowerCase(Locale.ENGLISH));
|
||||
lenient().when(resolver.resolve(type)).thenReturn(searchableType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private LuceneSearchableType searchableType(String name, String permission) {
|
||||
LuceneSearchableType searchableType = mock(LuceneSearchableType.class);
|
||||
lenient().when(searchableType.getName()).thenReturn(name);
|
||||
when(searchableType.getPermission()).thenReturn(Optional.ofNullable(permission));
|
||||
return searchableType;
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void shouldDelegateGetOrCreateWithDefaultIndex() {
|
||||
Index<Repository> index = mock(Index.class);
|
||||
public static class DummyIndexTask implements IndexTask<Repository> {
|
||||
|
||||
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
||||
IndexParams params = new IndexParams("default", searchableType, IndexOptions.defaults());
|
||||
when(indexQueue.<Repository>getQueuedIndex(params)).thenReturn(index);
|
||||
@Override
|
||||
public void update(Index<Repository> index) {
|
||||
|
||||
Index<Repository> idx = searchEngine.forType(Repository.class).getOrCreate();
|
||||
assertThat(idx).isSameAs(index);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void shouldDelegateGetOrCreateIndexWithDefaults() {
|
||||
Index<Repository> index = mock(Index.class);
|
||||
|
||||
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
||||
IndexParams params = new IndexParams("idx", searchableType, IndexOptions.defaults());
|
||||
when(indexQueue.<Repository>getQueuedIndex(params)).thenReturn(index);
|
||||
|
||||
Index<Repository> idx = searchEngine.forType(Repository.class).withIndex("idx").getOrCreate();
|
||||
assertThat(idx).isSameAs(index);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void shouldDelegateGetOrCreateIndex() {
|
||||
Index<Repository> index = mock(Index.class);
|
||||
IndexOptions options = IndexOptions.naturalLanguage(Locale.ENGLISH);
|
||||
|
||||
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
||||
IndexParams params = new IndexParams("default", searchableType, options);
|
||||
when(indexQueue.<Repository>getQueuedIndex(params)).thenReturn(index);
|
||||
|
||||
Index<Repository> idx = searchEngine.forType(Repository.class).withOptions(options).getOrCreate();
|
||||
assertThat(idx).isSameAs(index);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void shouldDelegateSearchWithDefaults() {
|
||||
LuceneQueryBuilder<Repository> mockedBuilder = mock(LuceneQueryBuilder.class);
|
||||
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
||||
|
||||
IndexParams params = new IndexParams("default", searchableType, IndexOptions.defaults());
|
||||
when(queryBuilderFactory.<Repository>create(params)).thenReturn(mockedBuilder);
|
||||
|
||||
QueryBuilder<Repository> queryBuilder = searchEngine.forType(Repository.class).search();
|
||||
|
||||
assertThat(queryBuilder).isSameAs(mockedBuilder);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void shouldDelegateSearch() {
|
||||
IndexOptions options = IndexOptions.naturalLanguage(Locale.GERMAN);
|
||||
|
||||
LuceneQueryBuilder<Repository> mockedBuilder = mock(LuceneQueryBuilder.class);
|
||||
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
||||
|
||||
IndexParams params = new IndexParams("idx", searchableType, options);
|
||||
when(queryBuilderFactory.<Repository>create(params)).thenReturn(mockedBuilder);
|
||||
|
||||
QueryBuilder<Repository> queryBuilder = searchEngine.forType(Repository.class).withIndex("idx").withOptions(options).search();
|
||||
|
||||
assertThat(queryBuilder).isSameAs(mockedBuilder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWithoutRequiredPermission() {
|
||||
when(searchableType.getPermission()).thenReturn(Optional.of("repository:read"));
|
||||
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
||||
|
||||
SearchEngine.ForType<Repository> forType = searchEngine.forType(Repository.class);
|
||||
assertThrows(AuthorizationException.class, forType::search);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
@SubjectAware(permissions = "repository:read")
|
||||
void shouldNotFailWithRequiredPermission() {
|
||||
when(searchableType.getPermission()).thenReturn(Optional.of("repository:read"));
|
||||
when(resolver.resolve(Repository.class)).thenReturn(searchableType);
|
||||
|
||||
LuceneQueryBuilder<Object> mockedBuilder = mock(LuceneQueryBuilder.class);
|
||||
when(queryBuilderFactory.create(any())).thenReturn(mockedBuilder);
|
||||
|
||||
SearchEngine.ForType<Repository> forType = searchEngine.forType(Repository.class);
|
||||
assertThat(forType.search()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWithTypeNameWithoutRequiredPermission() {
|
||||
when(searchableType.getPermission()).thenReturn(Optional.of("repository:read"));
|
||||
when(resolver.resolveByName("repository")).thenReturn(searchableType);
|
||||
|
||||
SearchEngine.ForType<Object> forType = searchEngine.forType("repository");
|
||||
assertThrows(AuthorizationException.class, forType::search);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
@SubjectAware(permissions = "repository:read")
|
||||
void shouldNotFailWithTypeNameAndRequiredPermission() {
|
||||
when(searchableType.getPermission()).thenReturn(Optional.of("repository:read"));
|
||||
when(resolver.resolveByName("repository")).thenReturn(searchableType);
|
||||
|
||||
LuceneQueryBuilder<Object> mockedBuilder = mock(LuceneQueryBuilder.class);
|
||||
when(queryBuilderFactory.create(any())).thenReturn(mockedBuilder);
|
||||
|
||||
SearchEngine.ForType<Object> forType = searchEngine.forType("repository");
|
||||
assertThat(forType.search()).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.search;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Injector;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LuceneSimpleIndexTaskTest {
|
||||
|
||||
@Mock
|
||||
private LuceneIndexFactory indexFactory;
|
||||
|
||||
@Mock
|
||||
private LuceneIndex<Repository> index;
|
||||
|
||||
private final SearchableTypeResolver resolver = new SearchableTypeResolver(Repository.class);
|
||||
|
||||
@Test
|
||||
void shouldUpdate() {
|
||||
Injector injector = createInjector();
|
||||
|
||||
LuceneSearchableType searchableType = resolver.resolve(Repository.class);
|
||||
|
||||
IndexParams params = new IndexParams("default", searchableType, IndexOptions.defaults());
|
||||
|
||||
AtomicReference<Index<?>> ref = new AtomicReference<>();
|
||||
LuceneSimpleIndexTask task = new LuceneSimpleIndexTask(params, ref::set);
|
||||
injector.injectMembers(task);
|
||||
|
||||
when(indexFactory.create(params)).then(ic -> index);
|
||||
|
||||
task.run();
|
||||
|
||||
assertThat(index).isSameAs(ref.get());
|
||||
verify(index).close();
|
||||
}
|
||||
|
||||
private Injector createInjector() {
|
||||
return Guice.createInjector(new AbstractModule() {
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(SearchableTypeResolver.class).toInstance(resolver);
|
||||
bind(LuceneIndexFactory.class).toInstance(indexFactory);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.search;
|
||||
|
||||
import org.apache.lucene.document.Document;
|
||||
import org.apache.lucene.index.IndexWriter;
|
||||
import org.apache.lucene.index.Term;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SharableIndexWriterTest {
|
||||
|
||||
@Mock
|
||||
private IndexWriter underlyingWriter;
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void shouldCreateIndexOnOpen() {
|
||||
Supplier<IndexWriter> supplier = mock(Supplier.class);
|
||||
|
||||
SharableIndexWriter writer = new SharableIndexWriter(supplier);
|
||||
verifyNoInteractions(supplier);
|
||||
|
||||
writer.open();
|
||||
verify(supplier).get();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void shouldOpenWriterOnlyOnce() {
|
||||
Supplier<IndexWriter> supplier = mock(Supplier.class);
|
||||
|
||||
SharableIndexWriter writer = new SharableIndexWriter(supplier);
|
||||
writer.open();
|
||||
writer.open();
|
||||
writer.open();
|
||||
|
||||
verify(supplier).get();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIncreaseUsageCounter() {
|
||||
SharableIndexWriter writer = new SharableIndexWriter(() -> underlyingWriter);
|
||||
writer.open();
|
||||
writer.open();
|
||||
writer.open();
|
||||
|
||||
assertThat(writer.getUsageCounter()).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDecreaseUsageCounter() throws IOException {
|
||||
SharableIndexWriter writer = new SharableIndexWriter(() -> underlyingWriter);
|
||||
writer.open();
|
||||
writer.open();
|
||||
writer.open();
|
||||
|
||||
assertThat(writer.getUsageCounter()).isEqualTo(3);
|
||||
|
||||
writer.close();
|
||||
writer.close();
|
||||
|
||||
assertThat(writer.getUsageCounter()).isOne();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotCloseWriterIfUsageCounterIsGreaterZero() throws IOException {
|
||||
SharableIndexWriter writer = new SharableIndexWriter(() -> underlyingWriter);
|
||||
writer.open();
|
||||
writer.open();
|
||||
writer.open();
|
||||
|
||||
writer.close();
|
||||
writer.close();
|
||||
|
||||
verify(underlyingWriter, never()).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCloseIfUsageCounterIsZero() throws IOException {
|
||||
SharableIndexWriter writer = new SharableIndexWriter(() -> underlyingWriter);
|
||||
writer.open();
|
||||
writer.open();
|
||||
|
||||
writer.close();
|
||||
writer.close();
|
||||
|
||||
verify(underlyingWriter).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void shouldReOpen() throws IOException {
|
||||
Supplier<IndexWriter> supplier = mock(Supplier.class);
|
||||
when(supplier.get()).thenReturn(underlyingWriter);
|
||||
|
||||
SharableIndexWriter writer = new SharableIndexWriter(supplier);
|
||||
writer.open();
|
||||
|
||||
writer.close();
|
||||
writer.open();
|
||||
|
||||
verify(supplier, times(2)).get();
|
||||
verify(underlyingWriter).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDelegateUpdates() throws IOException {
|
||||
SharableIndexWriter writer = new SharableIndexWriter(() -> underlyingWriter);
|
||||
writer.open();
|
||||
|
||||
Term term = new Term("field", "value");
|
||||
Document document = new Document();
|
||||
writer.updateDocument(term, document);
|
||||
|
||||
verify(underlyingWriter).updateDocument(term, document);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDelegateDeleteAll() throws IOException {
|
||||
SharableIndexWriter writer = new SharableIndexWriter(() -> underlyingWriter);
|
||||
writer.open();
|
||||
|
||||
writer.deleteAll();
|
||||
|
||||
verify(underlyingWriter).deleteAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDelegateDeletes() throws IOException {
|
||||
SharableIndexWriter writer = new SharableIndexWriter(() -> underlyingWriter);
|
||||
writer.open();
|
||||
|
||||
Term term = new Term("field", "value");
|
||||
writer.deleteDocuments(term);
|
||||
|
||||
verify(underlyingWriter).deleteDocuments(term);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ConcurrencyTests {
|
||||
|
||||
private ExecutorService executorService;
|
||||
|
||||
private final AtomicInteger openCounter = new AtomicInteger();
|
||||
private final AtomicInteger commitCounter = new AtomicInteger();
|
||||
private final AtomicInteger closeCounter = new AtomicInteger();
|
||||
|
||||
private final AtomicInteger invocations = new AtomicInteger();
|
||||
|
||||
private SharableIndexWriter writer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
executorService = Executors.newFixedThreadPool(4);
|
||||
writer = new SharableIndexWriter(() -> {
|
||||
openCounter.incrementAndGet();
|
||||
return underlyingWriter;
|
||||
});
|
||||
|
||||
doAnswer(ic -> commitCounter.incrementAndGet()).when(underlyingWriter).commit();
|
||||
doAnswer(ic -> closeCounter.incrementAndGet()).when(underlyingWriter).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("java:S2925") // sleep is ok to simulate some work
|
||||
void shouldKeepIndexOpen() {
|
||||
AtomicBoolean fail = new AtomicBoolean(false);
|
||||
for (int i = 0; i < 50; i++) {
|
||||
executorService.submit(() -> {
|
||||
writer.open();
|
||||
try {
|
||||
Thread.sleep(25);
|
||||
writer.deleteAll();
|
||||
writer.close();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
fail.set(true);
|
||||
} catch (IOException e) {
|
||||
fail.set(true);
|
||||
} finally {
|
||||
invocations.incrementAndGet();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
executorService.shutdown();
|
||||
|
||||
await().atMost(2, TimeUnit.SECONDS).until(() -> invocations.get() == 50);
|
||||
|
||||
assertThat(fail.get()).isFalse();
|
||||
|
||||
// It should be one, but it is possible that tasks finish before new added to the queue.
|
||||
// This behaviour depends heavily on the cpu's of the machine which executes this test.
|
||||
assertThat(openCounter.get()).isPositive().isLessThan(10);
|
||||
// should be 49, but see comment above
|
||||
assertThat(commitCounter.get()).isGreaterThan(40);
|
||||
// should be 1, but see comment above
|
||||
assertThat(closeCounter.get()).isPositive().isLessThan(10);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.security;
|
||||
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.UnavailableSecurityManagerException;
|
||||
import org.apache.shiro.mgt.DefaultSecurityManager;
|
||||
import org.apache.shiro.mgt.SecurityManager;
|
||||
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||
import org.github.sdorra.jse.ShiroExtension;
|
||||
import org.github.sdorra.jse.SubjectAware;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import sonia.scm.security.Impersonator.Session;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class ImpersonatorTest {
|
||||
|
||||
private SecurityManager securityManager = new DefaultSecurityManager();
|
||||
|
||||
private Impersonator impersonator;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
impersonator = new Impersonator(securityManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBindAndRestoreNonWebThread() {
|
||||
try (Session session = impersonator.impersonate(principal("dent"))) {
|
||||
assertPrincipal("dent");
|
||||
}
|
||||
assertThrows(UnavailableSecurityManagerException.class, SecurityUtils::getSubject);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private SimplePrincipalCollection principal(String principal) {
|
||||
return new SimplePrincipalCollection(principal, "test");
|
||||
}
|
||||
|
||||
private void assertPrincipal(String principal) {
|
||||
assertThat(SecurityUtils.getSubject().getPrincipal()).isEqualTo(principal);
|
||||
}
|
||||
|
||||
@Nested
|
||||
@ExtendWith(ShiroExtension.class)
|
||||
class WithSecurityManager {
|
||||
|
||||
@Test
|
||||
@SubjectAware("trillian")
|
||||
void shouldBindAndRestoreWebThread() {
|
||||
assertPrincipal("trillian");
|
||||
|
||||
try (Session session = impersonator.impersonate(principal("slarti"))) {
|
||||
assertPrincipal("slarti");
|
||||
}
|
||||
|
||||
assertPrincipal("trillian");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.update.index;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RemoveCombinedIndexTest {
|
||||
|
||||
private Path home;
|
||||
|
||||
@Mock
|
||||
private SCMContextProvider contextProvider;
|
||||
|
||||
@InjectMocks
|
||||
private RemoveCombinedIndex updateStep;
|
||||
|
||||
@BeforeEach
|
||||
void setUp(@TempDir Path home) {
|
||||
this.home = home;
|
||||
when(contextProvider.resolve(any())).then(
|
||||
ic -> home.resolve(ic.getArgument(0, Path.class))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRemoveIndexDirectory() throws IOException {
|
||||
Path indexDirectory = home.resolve("index");
|
||||
Path specificIndexDirectory = indexDirectory.resolve("repository").resolve("default");
|
||||
Files.createDirectories(specificIndexDirectory);
|
||||
Path helloTxt = specificIndexDirectory.resolve("hello.txt");
|
||||
Files.write(helloTxt, "hello".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
updateStep.doUpdate();
|
||||
|
||||
assertThat(helloTxt).doesNotExist();
|
||||
assertThat(indexDirectory).doesNotExist();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRemoveIndexLogDirectory() throws IOException {
|
||||
Path logDirectory = home.resolve("var").resolve("data").resolve("index-log");
|
||||
Files.createDirectories(logDirectory);
|
||||
Path helloXml = logDirectory.resolve("hello.xml");
|
||||
Files.write(helloXml, "<hello>world</hello>".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
updateStep.doUpdate();
|
||||
|
||||
assertThat(helloXml).doesNotExist();
|
||||
assertThat(logDirectory).doesNotExist();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,20 +24,23 @@
|
||||
|
||||
package sonia.scm.user;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.HandlerEventType;
|
||||
import sonia.scm.search.Id;
|
||||
import sonia.scm.search.Index;
|
||||
import sonia.scm.search.IndexLogStore;
|
||||
import sonia.scm.search.SearchEngine;
|
||||
import sonia.scm.search.SerializableIndexTask;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -54,6 +57,16 @@ class UserIndexerTest {
|
||||
@InjectMocks
|
||||
private UserIndexer indexer;
|
||||
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private Index<User> index;
|
||||
|
||||
@Mock
|
||||
private IndexLogStore indexLogStore;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<SerializableIndexTask<User>> captor;
|
||||
|
||||
@Test
|
||||
void shouldReturnType() {
|
||||
assertThat(indexer.getType()).isEqualTo(User.class);
|
||||
@@ -64,58 +77,54 @@ class UserIndexerTest {
|
||||
assertThat(indexer.getVersion()).isEqualTo(UserIndexer.VERSION);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class UpdaterTests {
|
||||
|
||||
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
|
||||
private Index<User> index;
|
||||
@Test
|
||||
void shouldReturnReIndexAllClass() {
|
||||
assertThat(indexer.getReIndexAllTask()).isEqualTo(UserIndexer.ReIndexAll.class);
|
||||
}
|
||||
|
||||
private final User user = UserTestData.createTrillian();
|
||||
@Test
|
||||
void shouldCreateUser() {
|
||||
User trillian = UserTestData.createTrillian();
|
||||
|
||||
@BeforeEach
|
||||
void open() {
|
||||
when(searchEngine.forType(User.class).getOrCreate()).thenReturn(index);
|
||||
}
|
||||
indexer.createStoreTask(trillian).update(index);
|
||||
|
||||
@Test
|
||||
void shouldStore() {
|
||||
indexer.open().store(user);
|
||||
verify(index).store(Id.of(trillian), UserPermissions.read(trillian).asShiroString(), trillian);
|
||||
}
|
||||
|
||||
verify(index).store(Id.of(user), "user:read:trillian", user);
|
||||
}
|
||||
@Test
|
||||
void shouldDeleteUser() {
|
||||
User trillian = UserTestData.createTrillian();
|
||||
|
||||
@Test
|
||||
void shouldDeleteById() {
|
||||
indexer.open().delete(user);
|
||||
indexer.createDeleteTask(trillian).update(index);
|
||||
|
||||
verify(index.delete().byType()).byId(Id.of(user));
|
||||
}
|
||||
verify(index.delete()).byId(Id.of(trillian));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReIndexAll() {
|
||||
when(userManager.getAll()).thenReturn(singletonList(user));
|
||||
@Test
|
||||
void shouldReIndexAll() {
|
||||
User trillian = UserTestData.createTrillian();
|
||||
User slarti = UserTestData.createSlarti();
|
||||
when(userManager.getAll()).thenReturn(Arrays.asList(trillian, slarti));
|
||||
|
||||
indexer.open().reIndexAll();
|
||||
UserIndexer.ReIndexAll reIndexAll = new UserIndexer.ReIndexAll(indexLogStore, userManager);
|
||||
reIndexAll.update(index);
|
||||
|
||||
verify(index.delete().byType()).all();
|
||||
verify(index).store(Id.of(user), "user:read:trillian", user);
|
||||
}
|
||||
verify(index.delete()).all();
|
||||
verify(index).store(Id.of(trillian), UserPermissions.read(trillian).asShiroString(), trillian);
|
||||
verify(index).store(Id.of(slarti), UserPermissions.read(slarti).asShiroString(), slarti);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleEvent() {
|
||||
UserEvent event = new UserEvent(HandlerEventType.DELETE, user);
|
||||
@Test
|
||||
void shouldHandleEvents() {
|
||||
User trillian = UserTestData.createTrillian();
|
||||
UserEvent event = new UserEvent(HandlerEventType.DELETE, trillian);
|
||||
|
||||
indexer.handleEvent(event);
|
||||
indexer.handleEvent(event);
|
||||
|
||||
verify(index.delete().byType()).byId(Id.of(user));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCloseIndex() {
|
||||
indexer.open().close();
|
||||
|
||||
verify(index).close();
|
||||
}
|
||||
verify(searchEngine.forType(User.class)).update(captor.capture());
|
||||
captor.getValue().update(index);
|
||||
verify(index.delete()).byId(Id.of(trillian));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.security.Authentications;
|
||||
import sonia.scm.security.Impersonator;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -54,7 +55,7 @@ class DefaultAdministrationContextTest {
|
||||
Injector injector = Guice.createInjector();
|
||||
SecurityManager securityManager = new DefaultSecurityManager();
|
||||
|
||||
context = new DefaultAdministrationContext(injector, securityManager);
|
||||
context = new DefaultAdministrationContext(injector, new Impersonator(securityManager));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.work;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Inject;
|
||||
import io.micrometer.core.instrument.Meter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.Timer;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.apache.shiro.mgt.SecurityManager;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||
import org.github.sdorra.jse.ShiroExtension;
|
||||
import org.github.sdorra.jse.SubjectAware;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.security.Authentications;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SubjectAware("trillian")
|
||||
@ExtendWith({MockitoExtension.class, ShiroExtension.class})
|
||||
class DefaultCentralWorkQueueTest {
|
||||
|
||||
private final PrincipalCollection principal = new SimplePrincipalCollection("trillian", "test");
|
||||
|
||||
private static final int ITERATIONS = 50;
|
||||
private static final int TIMEOUT = 1; // seconds
|
||||
|
||||
@Mock
|
||||
private Persistence persistence;
|
||||
|
||||
@Nested
|
||||
class WithDefaultInjector {
|
||||
|
||||
private MeterRegistry meterRegistry;
|
||||
private DefaultCentralWorkQueue queue;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
meterRegistry = new SimpleMeterRegistry();
|
||||
queue = new DefaultCentralWorkQueue(Guice.createInjector(new SecurityModule()), persistence, meterRegistry);
|
||||
}
|
||||
|
||||
private final AtomicInteger runs = new AtomicInteger();
|
||||
private int counter = 0;
|
||||
private int copy = -1;
|
||||
|
||||
@Test
|
||||
void shouldRunInSequenceWithBlock() {
|
||||
for (int i = 0; i < ITERATIONS; i++) {
|
||||
queue.append().locks("counter").enqueue(new Increase());
|
||||
}
|
||||
waitForTasks();
|
||||
|
||||
assertThat(counter).isEqualTo(ITERATIONS);
|
||||
}
|
||||
|
||||
private void waitForTasks() {
|
||||
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> queue.getSize() == 0);
|
||||
assertThat(runs.get()).isEqualTo(ITERATIONS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRunInParallel() {
|
||||
for (int i = 0; i < ITERATIONS; i++) {
|
||||
queue.append().enqueue(new Increase());
|
||||
}
|
||||
waitForTasks();
|
||||
|
||||
// we test if the resulting counter is less than the iteration,
|
||||
// because it is extremely likely that we miss a counter update
|
||||
// when we run in parallel
|
||||
assertThat(counter)
|
||||
.isPositive()
|
||||
.isLessThan(ITERATIONS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotBlocked() {
|
||||
for (int i = 0; i < ITERATIONS; i++) {
|
||||
queue.append().locks("counter").enqueue(new Increase());
|
||||
}
|
||||
queue.append().enqueue(() -> copy = counter);
|
||||
waitForTasks();
|
||||
|
||||
assertThat(counter).isEqualTo(ITERATIONS);
|
||||
assertThat(copy).isNotNegative().isLessThan(ITERATIONS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotBlockedByDifferentResource() {
|
||||
for (int i = 0; i < ITERATIONS; i++) {
|
||||
queue.append().locks("counter").enqueue(new Increase());
|
||||
}
|
||||
queue.append().locks("copy").enqueue(() -> copy = counter);
|
||||
waitForTasks();
|
||||
|
||||
assertThat(counter).isEqualTo(ITERATIONS);
|
||||
assertThat(copy).isNotNegative().isLessThan(ITERATIONS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBeBlockedByParentResource() {
|
||||
for (int i = 0; i < ITERATIONS; i++) {
|
||||
queue.append().locks("counter").enqueue(new Increase());
|
||||
}
|
||||
queue.append().locks("counter", "one").enqueue(() -> copy = counter);
|
||||
waitForTasks();
|
||||
|
||||
assertThat(counter).isEqualTo(ITERATIONS);
|
||||
assertThat(copy).isEqualTo(ITERATIONS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBeBlockedByParentAndExactResource() {
|
||||
for (int i = 0; i < ITERATIONS; i++) {
|
||||
if (i % 2 == 0) {
|
||||
queue.append().locks("counter", "c").enqueue(new Increase());
|
||||
} else {
|
||||
queue.append().locks("counter").enqueue(new Increase());
|
||||
}
|
||||
}
|
||||
waitForTasks();
|
||||
assertThat(counter).isEqualTo(ITERATIONS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBeBlockedByParentResourceWithModelObject() {
|
||||
Repository one = repository("one");
|
||||
for (int i = 0; i < ITERATIONS; i++) {
|
||||
queue.append().locks("counter").enqueue(new Increase());
|
||||
}
|
||||
queue.append().locks("counter", one).enqueue(() -> copy = counter);
|
||||
waitForTasks();
|
||||
|
||||
assertThat(counter).isEqualTo(ITERATIONS);
|
||||
assertThat(copy).isEqualTo(ITERATIONS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFinalizeOnError() {
|
||||
queue.append().enqueue(() -> {
|
||||
throw new IllegalStateException("failed");
|
||||
});
|
||||
|
||||
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> queue.getSize() == 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSetThreadName() {
|
||||
AtomicReference<String> threadName = new AtomicReference<>();
|
||||
queue.append().enqueue(() -> threadName.set(Thread.currentThread().getName()));
|
||||
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> threadName.get() != null);
|
||||
|
||||
assertThat(threadName.get()).startsWith("CentralWorkQueue");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCaptureExecutorMetrics() {
|
||||
for (int i = 0; i < ITERATIONS; i++) {
|
||||
queue.append().enqueue(new Increase());
|
||||
}
|
||||
waitForTasks();
|
||||
|
||||
double count = meterRegistry.get("executor.completed").functionCounter().count();
|
||||
assertThat(count).isEqualTo(ITERATIONS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCaptureExecutionDuration() {
|
||||
queue.append().enqueue(new Increase());
|
||||
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> queue.getSize() == 0);
|
||||
|
||||
Timer timer = meterRegistry.get(UnitOfWork.METRIC_EXECUTION).timer();
|
||||
assertThat(timer.count()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCaptureWaitDuration() {
|
||||
queue.append().enqueue(new Increase());
|
||||
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> queue.getSize() == 0);
|
||||
|
||||
Timer timer = meterRegistry.get(UnitOfWork.METRIC_WAIT).timer();
|
||||
assertThat(timer.count()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIncreaseBlockCount() {
|
||||
for (int i = 0; i < ITERATIONS; i++) {
|
||||
queue.append().locks("counter").enqueue(new Increase());
|
||||
}
|
||||
waitForTasks();
|
||||
|
||||
int blockCount = 0;
|
||||
for (Meter meter : meterRegistry.getMeters()) {
|
||||
Meter.Id id = meter.getId();
|
||||
if ("cwq.task.wait.duration".equals(id.getName())) {
|
||||
String blocked = id.getTag("blocked");
|
||||
if (blocked != null) {
|
||||
blockCount += Integer.parseInt(blocked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertThat(blockCount).isPositive();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Repository repository(String id) {
|
||||
Repository one = new Repository();
|
||||
one.setId(id);
|
||||
return one;
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
queue.close();
|
||||
}
|
||||
|
||||
private class Increase implements Task {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("java:S2925")
|
||||
public void run() {
|
||||
int currentCounter = counter;
|
||||
runs.incrementAndGet();
|
||||
try {
|
||||
Thread.sleep(5);
|
||||
counter = currentCounter + 1;
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInjectDependencies() {
|
||||
Context ctx = new Context();
|
||||
DefaultCentralWorkQueue queue = new DefaultCentralWorkQueue(
|
||||
Guice.createInjector(new SecurityModule(), binder -> binder.bind(Context.class).toInstance(ctx)),
|
||||
persistence,
|
||||
new SimpleMeterRegistry(),
|
||||
() -> 2
|
||||
);
|
||||
|
||||
queue.append().enqueue(InjectingTask.class);
|
||||
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> ctx.value != null);
|
||||
assertThat(ctx.value).isEqualTo("Hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLoadFromPersistence() {
|
||||
Context context = new Context();
|
||||
SimpleUnitOfWork one = new SimpleUnitOfWork(
|
||||
21L, principal, Collections.singleton(new Resource("a")), new InjectingTask(context, "one")
|
||||
);
|
||||
SimpleUnitOfWork two = new SimpleUnitOfWork(
|
||||
42L, principal, Collections.singleton(new Resource("a")), new InjectingTask(context, "two")
|
||||
);
|
||||
two.restore(42L);
|
||||
when(persistence.loadAll()).thenReturn(Arrays.asList(one, two));
|
||||
|
||||
new DefaultCentralWorkQueue(Guice.createInjector(new SecurityModule()), persistence, new SimpleMeterRegistry());
|
||||
|
||||
await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> context.value != null);
|
||||
assertThat(context.value).isEqualTo("two");
|
||||
assertThat(one.getOrder()).isEqualTo(1L);
|
||||
assertThat(one.getRestoreCount()).isEqualTo(1);
|
||||
assertThat(two.getOrder()).isEqualTo(2L);
|
||||
assertThat(two.getRestoreCount()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRunAsUser() {
|
||||
DefaultCentralWorkQueue workQueue = new DefaultCentralWorkQueue(
|
||||
Guice.createInjector(new SecurityModule()), persistence, new SimpleMeterRegistry()
|
||||
);
|
||||
|
||||
AtomicReference<Object> ref = new AtomicReference<>();
|
||||
workQueue.append().enqueue(() -> ref.set(SecurityUtils.getSubject().getPrincipal()));
|
||||
await().atMost(1, TimeUnit.SECONDS).until(() -> "trillian".equals(ref.get()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRunAsAdminUser() {
|
||||
DefaultCentralWorkQueue workQueue = new DefaultCentralWorkQueue(
|
||||
Guice.createInjector(new SecurityModule()), persistence, new SimpleMeterRegistry()
|
||||
);
|
||||
|
||||
AtomicReference<Object> ref = new AtomicReference<>();
|
||||
workQueue.append().runAsAdmin().enqueue(() -> ref.set(SecurityUtils.getSubject().getPrincipal()));
|
||||
await().atMost(1, TimeUnit.SECONDS).until(() -> Authentications.PRINCIPAL_SYSTEM.equals(ref.get()));
|
||||
}
|
||||
|
||||
public static class Context {
|
||||
|
||||
private String value;
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
public static class InjectingTask implements Task {
|
||||
|
||||
private final Context context;
|
||||
private final String value;
|
||||
|
||||
@Inject
|
||||
public InjectingTask(Context context) {
|
||||
this(context, "Hello");
|
||||
}
|
||||
|
||||
public InjectingTask(Context context, String value) {
|
||||
this.context = context;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
context.setValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SecurityModule extends AbstractModule {
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(SecurityManager.class).toInstance(SecurityUtils.getSecurityManager());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
178
scm-webapp/src/test/java/sonia/scm/work/PersistenceTest.java
Normal file
178
scm-webapp/src/test/java/sonia/scm/work/PersistenceTest.java
Normal file
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.work;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.store.Blob;
|
||||
import sonia.scm.store.InMemoryBlobStore;
|
||||
import sonia.scm.store.InMemoryBlobStoreFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PersistenceTest {
|
||||
|
||||
private final PrincipalCollection principal = new SimplePrincipalCollection("trillian", "test");
|
||||
|
||||
@Nested
|
||||
class Default {
|
||||
|
||||
@Mock
|
||||
private PluginLoader pluginLoader;
|
||||
|
||||
private Persistence persistence;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(pluginLoader.getUberClassLoader()).thenReturn(PersistenceTest.class.getClassLoader());
|
||||
persistence = new Persistence(pluginLoader, new InMemoryBlobStoreFactory());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStoreSimpleChunkOfWork() {
|
||||
UnitOfWork work = new SimpleUnitOfWork(
|
||||
1L, principal, Collections.singleton(new Resource("a")), new MyTask()
|
||||
);
|
||||
persistence.store(work);
|
||||
|
||||
UnitOfWork loaded = persistence.loadAll().iterator().next();
|
||||
assertThat(loaded).isEqualTo(work);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStoreInjectingChunkOfWork() {
|
||||
UnitOfWork work = new InjectingUnitOfWork(
|
||||
1L, principal, Collections.singleton(new Resource("a")), MyTask.class
|
||||
);
|
||||
persistence.store(work);
|
||||
|
||||
UnitOfWork loaded = persistence.loadAll().iterator().next();
|
||||
assertThat(loaded).isEqualTo(work);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLoadInOrder() {
|
||||
store(5, 3, 1, 4, 2);
|
||||
|
||||
long[] orderIds = persistence.loadAll()
|
||||
.stream()
|
||||
.mapToLong(UnitOfWork::getOrder)
|
||||
.toArray();
|
||||
|
||||
assertThat(orderIds).containsExactly(1, 2, 3, 4, 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRemoveAfterLoad() {
|
||||
store(1, 2);
|
||||
|
||||
assertThat(persistence.loadAll()).hasSize(2);
|
||||
assertThat(persistence.loadAll()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailIfNotSerializable() {
|
||||
store(1);
|
||||
|
||||
SimpleUnitOfWork unitOfWork = new SimpleUnitOfWork(
|
||||
2L, principal, Collections.emptySet(), new NotSerializable()
|
||||
);
|
||||
|
||||
assertThrows(NonPersistableTaskException.class, () -> persistence.store(unitOfWork));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRemoveStored() {
|
||||
store(1);
|
||||
SimpleUnitOfWork chunkOfWork = new SimpleUnitOfWork(
|
||||
2L, principal, Collections.emptySet(), new MyTask()
|
||||
);
|
||||
persistence.store(chunkOfWork);
|
||||
persistence.remove(chunkOfWork);
|
||||
|
||||
assertThat(persistence.loadAll()).hasSize(1);
|
||||
}
|
||||
|
||||
private void store(long... orderIds) {
|
||||
for (long order : orderIds) {
|
||||
persistence.store(new SimpleUnitOfWork(
|
||||
order, principal, Collections.emptySet(), new MyTask()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotFailForNonChunkOfWorkItems() throws IOException {
|
||||
InMemoryBlobStore blobStore = new InMemoryBlobStore();
|
||||
|
||||
Persistence persistence = new Persistence(PersistenceTest.class.getClassLoader(), blobStore);
|
||||
persistence.store(new SimpleUnitOfWork(
|
||||
1L, principal, Collections.emptySet(), new MyTask())
|
||||
);
|
||||
|
||||
Blob blob = blobStore.create();
|
||||
try (ObjectOutputStream stream = new ObjectOutputStream(blob.getOutputStream())) {
|
||||
stream.writeObject(new MyTask());
|
||||
blob.commit();
|
||||
}
|
||||
|
||||
assertThat(persistence.loadAll()).hasSize(1);
|
||||
}
|
||||
|
||||
@EqualsAndHashCode
|
||||
public static class MyTask implements Task {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// non static inner classes are not serializable
|
||||
private class NotSerializable implements Task {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
72
scm-webapp/src/test/java/sonia/scm/work/ResourceTest.java
Normal file
72
scm-webapp/src/test/java/sonia/scm/work/ResourceTest.java
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.work;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ResourceTest {
|
||||
|
||||
@Test
|
||||
void shouldReturnResourceName() {
|
||||
assertThat(res("a")).hasToString("a");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnResourceNameAndId() {
|
||||
assertThat(res("a", "b")).hasToString("a:b");
|
||||
}
|
||||
|
||||
@Nested
|
||||
class IsBlockedByTests {
|
||||
|
||||
@Test
|
||||
void shouldReturnTrue() {
|
||||
assertThat(res("a").isBlockedBy(res("a"))).isTrue();
|
||||
assertThat(res("a", "b").isBlockedBy(res("a", "b"))).isTrue();
|
||||
assertThat(res("a").isBlockedBy(res("a", "b"))).isTrue();
|
||||
assertThat(res("a", "b").isBlockedBy(res("a"))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnFalse() {
|
||||
assertThat(res("a").isBlockedBy(res("b"))).isFalse();
|
||||
assertThat(res("a", "b").isBlockedBy(res("a", "c"))).isFalse();
|
||||
assertThat(res("a", "b").isBlockedBy(res("c", "b"))).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Resource res(String name) {
|
||||
return new Resource(name);
|
||||
}
|
||||
|
||||
private Resource res(String name, String id) {
|
||||
return new Resource(name, id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.work;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Injector;
|
||||
import lombok.Value;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class SimpleUnitOfWorkTest {
|
||||
|
||||
private PrincipalCollection principal = new SimplePrincipalCollection("trillian", "test");
|
||||
|
||||
@Test
|
||||
void shouldInjectMember() {
|
||||
Context context = new Context("awesome");
|
||||
Injector injector = Guice.createInjector(new AbstractModule() {
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(Context.class).toInstance(context);
|
||||
}
|
||||
});
|
||||
|
||||
SimpleTask simpleTask = new SimpleTask();
|
||||
SimpleUnitOfWork unitOfWork = new SimpleUnitOfWork(1L, principal, Collections.emptySet(), simpleTask);
|
||||
unitOfWork.task(injector);
|
||||
|
||||
simpleTask.run();
|
||||
|
||||
assertThat(simpleTask.value).isEqualTo("awesome");
|
||||
}
|
||||
|
||||
@Value
|
||||
public class Context {
|
||||
String value;
|
||||
}
|
||||
|
||||
public class SimpleTask implements Task {
|
||||
|
||||
private Context context;
|
||||
|
||||
private String value = "no value set";
|
||||
|
||||
@Inject
|
||||
public void setContext(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (context != null) {
|
||||
value = context.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.work;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ThreadCountProviderTest {
|
||||
|
||||
@Test
|
||||
void shouldUseTwoWorkersForOneCPU() {
|
||||
ThreadCountProvider provider = new ThreadCountProvider(() -> 1);
|
||||
|
||||
assertThat(provider.getAsInt()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "shouldUseFourWorkersFor{argumentsWithNames}CPU")
|
||||
@ValueSource(ints = {2, 4, 8, 16})
|
||||
void shouldUseFourWorkersForMoreThanOneCPU(int cpus) {
|
||||
ThreadCountProvider provider = new ThreadCountProvider(() -> cpus);
|
||||
|
||||
assertThat(provider.getAsInt()).isEqualTo(4);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class SystemPropertyTests {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
System.clearProperty(ThreadCountProvider.PROPERTY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseCountFromSystemProperty() {
|
||||
ThreadCountProvider provider = new ThreadCountProvider();
|
||||
System.setProperty(ThreadCountProvider.PROPERTY, "6");
|
||||
assertThat(provider.getAsInt()).isEqualTo(6);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"-1", "0", "100", "a", ""})
|
||||
void shouldUseDefaultForInvalidValue(String value) {
|
||||
ThreadCountProvider provider = new ThreadCountProvider(() -> 1);
|
||||
System.setProperty(ThreadCountProvider.PROPERTY, value);
|
||||
assertThat(provider.getAsInt()).isEqualTo(2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user