Increase FetchOne coverage with unit tests
Achieved 88% line and 86% function coverage,
what is not covered is some getters and log statements.
Bug: Issue 16806
Change-Id: I34349e5d8fa8af2ff32b61d330c122b0afcc3630
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java
new file mode 100644
index 0000000..3980f93
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java
@@ -0,0 +1,848 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replication.pull;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.metrics.DisabledMetricMaker;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.PerThreadRequestScope;
+import com.google.gerrit.server.util.IdGenerator;
+import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiRequestMetrics;
+import com.googlesource.gerrit.plugins.replication.pull.fetch.Fetch;
+import com.googlesource.gerrit.plugins.replication.pull.fetch.FetchFactory;
+import com.googlesource.gerrit.plugins.replication.pull.fetch.InexistentRefTransportException;
+import com.googlesource.gerrit.plugins.replication.pull.fetch.RefUpdateState;
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.Callable;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import org.eclipse.jgit.errors.PackProtocolException;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.RemoteConfig;
+import org.eclipse.jgit.transport.URIish;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+
+@RunWith(MockitoJUnitRunner.class)
+public class FetchOneTest {
+ private final String TEST_PROJECT_NAME = "FetchOneTest";
+ private final Project.NameKey PROJECT_NAME = Project.NameKey.parse(TEST_PROJECT_NAME);
+ private final String TEST_REF = "refs/heads/refForReplicationTask";
+ private final String URI_PATTERN = "http://test.com/" + TEST_PROJECT_NAME + ".git";
+
+ @Mock private GitRepositoryManager grm;
+ @Mock private Repository repository;
+ @Mock private Source source;
+ @Mock private SourceConfiguration sourceConfiguration;
+ @Mock private PerThreadRequestScope.Scoper scoper;
+ @Mock private IdGenerator idGenerator;
+ @Mock private ReplicationStateListeners replicationStateListeners;
+ @Mock private FetchFactory fetchFactory;
+ @Mock private PullReplicationApiRequestMetrics pullReplicationApiRequestMetrics;
+ @Mock private RemoteConfig remoteConfig;
+ @Mock private DynamicItem<ReplicationFetchFilter> replicationFilter;
+
+ private URIish urIish;
+ private FetchOne objectUnderTest;
+
+ @Before
+ public void setup() throws Exception {
+ FetchReplicationMetrics fetchReplicationMetrics =
+ new FetchReplicationMetrics("pull-replication", new DisabledMetricMaker());
+ urIish = new URIish(URI_PATTERN);
+
+ grm = mock(GitRepositoryManager.class);
+ source = mock(Source.class);
+ sourceConfiguration = mock(SourceConfiguration.class);
+ scoper = mock(PerThreadRequestScope.Scoper.class);
+ idGenerator = mock(IdGenerator.class);
+ replicationStateListeners = mock(ReplicationStateListeners.class);
+ fetchFactory = mock(FetchFactory.class);
+ pullReplicationApiRequestMetrics = mock(PullReplicationApiRequestMetrics.class);
+ remoteConfig = mock(RemoteConfig.class);
+ replicationFilter = mock(DynamicItem.class);
+
+ when(sourceConfiguration.getRemoteConfig()).thenReturn(remoteConfig);
+ when(idGenerator.next()).thenReturn(1);
+ int maxLockRetries = 1;
+ when(source.getLockErrorMaxRetries()).thenReturn(maxLockRetries);
+
+ objectUnderTest =
+ new FetchOne(
+ grm,
+ source,
+ sourceConfiguration,
+ scoper,
+ idGenerator,
+ replicationStateListeners,
+ fetchReplicationMetrics,
+ fetchFactory,
+ PROJECT_NAME,
+ urIish,
+ Optional.of(pullReplicationApiRequestMetrics));
+ }
+
+ @Test
+ public void shouldIncludeTheTaskIndexInItsStringRepresentation() {
+ String expected = "[" + objectUnderTest.getTaskIdHex() + "] fetch " + URI_PATTERN;
+
+ assertThat(objectUnderTest.toString()).isEqualTo(expected);
+ }
+
+ @Test
+ public void shouldIncludeTheRetryCountInItsStringRepresentationWhenATaskIsRetried() {
+ objectUnderTest.setToRetry();
+ String expected = "(retry 1) [" + objectUnderTest.getTaskIdHex() + "] fetch " + URI_PATTERN;
+
+ assertThat(objectUnderTest.toString()).isEqualTo(expected);
+ }
+
+ @Test
+ public void shouldAddARefToTheDeltaIfItsNotTheAllRefs() {
+ Set<String> refs = Set.of(TEST_REF);
+ objectUnderTest.addRefs(refs);
+
+ assertThat(refs).isEqualTo(objectUnderTest.getRefs());
+ }
+
+ @Test
+ public void shouldIgnoreEveryRefButTheAllRefsWhenAddingARef() {
+ objectUnderTest.addRefs(Set.of(TEST_REF, FetchOne.ALL_REFS));
+
+ assertThat(Set.of(FetchOne.ALL_REFS)).isEqualTo(objectUnderTest.getRefs());
+ }
+
+ @Test
+ public void shouldReturnExistingStates() {
+ assertThat(createTestStates(TEST_REF, 1)).isEqualTo(objectUnderTest.getStates().get(TEST_REF));
+ }
+
+ @Test
+ public void shouldKeepMultipleStatesInInsertionOrderForARef() {
+ List<ReplicationState> states = createTestStates(TEST_REF, 2);
+
+ List<ReplicationState> actualStates = objectUnderTest.getStates().get(TEST_REF);
+
+ assertThat(actualStates).containsExactlyElementsIn(states).inOrder();
+ }
+
+ @Test
+ public void shouldReturnStatesInAnArray() {
+ List<ReplicationState> states = createTestStates(TEST_REF, 2);
+
+ ReplicationState[] actualStates = objectUnderTest.getStatesAsArray();
+
+ assertThat(actualStates).asList().containsExactly(states.toArray());
+ }
+
+ @Test
+ public void shouldClearTheStates() {
+ createTestStates(TEST_REF, 2);
+
+ objectUnderTest.removeStates();
+
+ assertThat(objectUnderTest.getStates().isEmpty()).isTrue();
+ }
+
+ @Test
+ public void shouldNotifyTheSourceWhenTaskIsCancelled() {
+ objectUnderTest.cancel();
+
+ verify(source).fetchWasCanceled(objectUnderTest);
+ assertThat(objectUnderTest.wasCanceled()).isTrue();
+ }
+
+ @Test
+ public void shouldRunAReplicationTaskForAllRefsIfDeltaIsEmpty() throws Exception {
+ setupMocks(true);
+ List<ReplicationState> states = createTestStates(FetchOne.ALL_REFS, 1);
+ setupFetchFactoryMock(Collections.emptyList());
+
+ objectUnderTest.run();
+
+ assertFinishedWithEmptyStateAndNoFailures();
+ verify(states.get(0))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME,
+ FetchOne.ALL_REFS,
+ urIish,
+ ReplicationState.RefFetchResult.SUCCEEDED,
+ RefUpdate.Result.NO_CHANGE);
+ }
+
+ @Test
+ public void shouldRescheduleReplicationTaskAndExitIfTheQueueLockCantBeObtained()
+ throws Exception {
+ setupMocks(false);
+
+ objectUnderTest.run();
+
+ verify(source, never()).notifyFinished(objectUnderTest);
+ verify(source).reschedule(objectUnderTest, Source.RetryReason.COLLISION);
+ }
+
+ @Test
+ public void shouldNotRescheduleAnAlreadyCancelledReplicationTaskIfTheQueueLockCantBeObtained()
+ throws Exception {
+ setupMocks(false);
+ objectUnderTest.canceledByReplication();
+
+ objectUnderTest.run();
+
+ verify(source, never()).notifyFinished(objectUnderTest);
+ verify(source, never()).reschedule(objectUnderTest, Source.RetryReason.COLLISION);
+ }
+
+ @Test
+ public void shouldRunTheFetchOperationEvenWhenStateIsEmpty() throws Exception {
+ setupMocks(true);
+ Fetch mockFetch =
+ setupFetchFactoryMock(
+ List.of(
+ new FetchFactoryEntry.Builder()
+ .withRefSpecName(TEST_REF)
+ .withRemoteName("testRemote")
+ .withResult(RefUpdate.Result.NEW)
+ .build()));
+ objectUnderTest.addRefs(Set.of(TEST_REF));
+
+ objectUnderTest.run();
+
+ verify(mockFetch).fetch(List.of(new RefSpec(TEST_REF)));
+ }
+
+ @Test
+ public void
+ shouldSetTheReplicationFetchResultStatusToNotAttemptedAndThenFailedForARefForWhichThereIsNoState()
+ throws Exception {
+ setupMocks(true);
+ String someRef = "refs/heads/someRef";
+ List<ReplicationState> states = createTestStates(someRef, 1);
+ setupFetchFactoryMock(
+ List.of(new FetchFactoryEntry.Builder().refSpecNameWithDefaults(TEST_REF).build()));
+ objectUnderTest.addRefs(Set.of(TEST_REF));
+
+ objectUnderTest.run();
+
+ assertFinishedWithNonEmptyStateAndNoFailures();
+ verify(states.get(0))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME,
+ someRef,
+ urIish,
+ ReplicationState.RefFetchResult.NOT_ATTEMPTED,
+ null);
+ verify(states.get(0))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME, someRef, urIish, ReplicationState.RefFetchResult.FAILED, null);
+ }
+
+ @Test(expected = InternalError.class)
+ public void shouldThrowAnExceptionForUnrecoverableErrors() {
+ setupFailingScopeMock();
+
+ objectUnderTest.run();
+ }
+
+ @Test
+ public void shouldFilterOutRefsFromFetchReplicationDelta() throws Exception {
+ setupMocks(true);
+ String filteredRef = "refs/heads/filteredRef";
+ Set<String> refSpecs = Set.of(TEST_REF, filteredRef);
+ List<ReplicationState> states =
+ Stream.concat(
+ createTestStates(TEST_REF, 1).stream(), createTestStates(filteredRef, 1).stream())
+ .collect(Collectors.toList());
+ setupFetchFactoryMock(
+ List.of(new FetchFactoryEntry.Builder().refSpecNameWithDefaults(TEST_REF).build()),
+ Optional.of(List.of(TEST_REF)));
+ objectUnderTest.addRefs(refSpecs);
+ objectUnderTest.setReplicationFetchFilter(replicationFilter);
+ ReplicationFetchFilter mockFilter = mock(ReplicationFetchFilter.class);
+ when(replicationFilter.get()).thenReturn(mockFilter);
+ when(mockFilter.filter(TEST_PROJECT_NAME, refSpecs)).thenReturn(Set.of(TEST_REF));
+
+ objectUnderTest.run();
+
+ assertFinishedWithNonEmptyStateAndNoFailures();
+ verify(states.get(0))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME,
+ TEST_REF,
+ urIish,
+ ReplicationState.RefFetchResult.SUCCEEDED,
+ RefUpdate.Result.NEW);
+ verify(states.get(1))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME, filteredRef, urIish, ReplicationState.RefFetchResult.FAILED, null);
+ }
+
+ @Test
+ public void shouldMarkTheReplicationStatusAsSucceededOnSuccessfulReplicationOfARef()
+ throws Exception {
+ setupMocks(true);
+ List<ReplicationState> states = createTestStates(TEST_REF, 1);
+ setupFetchFactoryMock(
+ List.of(new FetchFactoryEntry.Builder().refSpecNameWithDefaults(TEST_REF).build()));
+ objectUnderTest.addRefs(Set.of(TEST_REF));
+
+ objectUnderTest.run();
+ assertFinishedWithEmptyStateAndNoFailures();
+ verify(states.get(0))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME,
+ TEST_REF,
+ urIish,
+ ReplicationState.RefFetchResult.SUCCEEDED,
+ RefUpdate.Result.NEW);
+ }
+
+ @Test
+ public void shouldMarkAllTheStatesOfARefAsReplicatedSuccessfullyOnASuccessfulReplication()
+ throws Exception {
+ setupMocks(true);
+ List<ReplicationState> states = createTestStates(TEST_REF, 2);
+ setupFetchFactoryMock(
+ List.of(new FetchFactoryEntry.Builder().refSpecNameWithDefaults(TEST_REF).build()));
+ objectUnderTest.addRefs(Set.of(TEST_REF));
+
+ objectUnderTest.run();
+
+ assertFinishedWithEmptyStateAndNoFailures();
+ verify(states.get(0))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME,
+ TEST_REF,
+ urIish,
+ ReplicationState.RefFetchResult.SUCCEEDED,
+ RefUpdate.Result.NEW);
+ verify(states.get(1))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME,
+ TEST_REF,
+ urIish,
+ ReplicationState.RefFetchResult.SUCCEEDED,
+ RefUpdate.Result.NEW);
+ }
+
+ @Test
+ public void shouldUpdateTheStateOfAllRefsOnSuccessfulReplication() throws Exception {
+ setupMocks(true);
+ List<ReplicationState> states =
+ Stream.concat(
+ createTestStates(TEST_REF, 1).stream(),
+ createTestStates(FetchOne.ALL_REFS, 1).stream())
+ .collect(Collectors.toList());
+ setupFetchFactoryMock(
+ List.of(new FetchFactoryEntry.Builder().refSpecNameWithDefaults(TEST_REF).build()));
+ objectUnderTest.addRefs(Set.of(TEST_REF));
+
+ objectUnderTest.run();
+
+ assertFinishedWithEmptyStateAndNoFailures();
+ verify(states.get(0))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME,
+ TEST_REF,
+ urIish,
+ ReplicationState.RefFetchResult.SUCCEEDED,
+ RefUpdate.Result.NEW);
+ verify(states.get(1))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME,
+ FetchOne.ALL_REFS,
+ urIish,
+ ReplicationState.RefFetchResult.SUCCEEDED,
+ RefUpdate.Result.NEW);
+ }
+
+ @Test
+ public void shouldMarkReplicationStateAsRejectedWhenTheObjectIsNotInRepository()
+ throws Exception {
+ setupMocks(true);
+ List<ReplicationState> states = createTestStates(TEST_REF, 2);
+ setupFetchFactoryMock(
+ List.of(
+ new FetchFactoryEntry.Builder()
+ .withRefNames(TEST_REF)
+ .withResult(RefUpdate.Result.REJECTED_MISSING_OBJECT)
+ .build()));
+ objectUnderTest.addRefs(Set.of(TEST_REF));
+
+ objectUnderTest.run();
+
+ assertFinishedWithEmptyStateAndNoFailures();
+ verify(states.get(0))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME,
+ TEST_REF,
+ urIish,
+ ReplicationState.RefFetchResult.FAILED,
+ RefUpdate.Result.REJECTED_MISSING_OBJECT);
+ }
+
+ @Test
+ public void shouldMarkReplicationStateAsRejectedWhenFailedForUnknownReason() throws Exception {
+ setupMocks(true);
+ List<ReplicationState> states = createTestStates(TEST_REF, 1);
+ setupFetchFactoryMock(
+ List.of(
+ new FetchFactoryEntry.Builder()
+ .withRefNames(TEST_REF)
+ .withResult(RefUpdate.Result.REJECTED_OTHER_REASON)
+ .build()));
+ objectUnderTest.addRefs(Set.of(TEST_REF));
+
+ objectUnderTest.run();
+
+ assertFinishedWithEmptyStateAndNoFailures();
+ verify(states.get(0))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME,
+ TEST_REF,
+ urIish,
+ ReplicationState.RefFetchResult.FAILED,
+ RefUpdate.Result.REJECTED_OTHER_REASON);
+ }
+
+ @Test
+ public void shouldMarkReplicationStateOfAllRefsAsRejectedForAnyFailedTask() throws Exception {
+ setupMocks(true);
+ String failingRef = "refs/heads/failingRef";
+ String forcedRef = "refs/heads/forcedRef";
+ List<ReplicationState> states =
+ Stream.of(
+ createTestStates(TEST_REF, 1),
+ createTestStates(failingRef, 1),
+ createTestStates(forcedRef, 1),
+ createTestStates(FetchOne.ALL_REFS, 1))
+ .flatMap(Collection::stream)
+ .collect(Collectors.toList());
+ setupFetchFactoryMock(
+ List.of(
+ new FetchFactoryEntry.Builder()
+ .withRefNames(TEST_REF)
+ .withResult(RefUpdate.Result.NEW)
+ .build(),
+ new FetchFactoryEntry.Builder()
+ .withRefNames(failingRef)
+ .withResult(RefUpdate.Result.REJECTED_MISSING_OBJECT)
+ .build(),
+ new FetchFactoryEntry.Builder()
+ .withRefNames(forcedRef)
+ .withResult(RefUpdate.Result.FORCED)
+ .build()));
+ objectUnderTest.addRefs(Set.of(TEST_REF, failingRef, forcedRef));
+
+ objectUnderTest.run();
+
+ assertFinishedWithEmptyStateAndNoFailures();
+ verify(states.get(0))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME,
+ TEST_REF,
+ urIish,
+ ReplicationState.RefFetchResult.SUCCEEDED,
+ RefUpdate.Result.NEW);
+ verify(states.get(1))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME,
+ failingRef,
+ urIish,
+ ReplicationState.RefFetchResult.FAILED,
+ RefUpdate.Result.REJECTED_MISSING_OBJECT);
+ verify(states.get(2))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME,
+ forcedRef,
+ urIish,
+ ReplicationState.RefFetchResult.SUCCEEDED,
+ RefUpdate.Result.FORCED);
+ verify(states.get(3))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME,
+ FetchOne.ALL_REFS,
+ urIish,
+ ReplicationState.RefFetchResult.FAILED,
+ RefUpdate.Result.FORCED);
+ }
+
+ @Test
+ public void shouldRetryOnLockingFailures() throws Exception {
+ setupMocks(true);
+ List<ReplicationState> states = createTestStates(TEST_REF, 1);
+ setupFetchFactoryMock(
+ List.of(
+ new FetchFactoryEntry.Builder()
+ .withRefNames(TEST_REF)
+ .withResult(RefUpdate.Result.LOCK_FAILURE)
+ .build()));
+ objectUnderTest.addRefs(Set.of(TEST_REF));
+
+ objectUnderTest.run();
+
+ assertFinishedWithNonEmptyStateAndNoFailures();
+ verify(states.get(0))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME, TEST_REF, urIish, ReplicationState.RefFetchResult.FAILED, null);
+ verify(source).reschedule(objectUnderTest, Source.RetryReason.TRANSPORT_ERROR);
+ }
+
+ @Test
+ public void shouldNotRetryWhenMaxLockRetriesLimitIsReached() throws Exception {
+ setupMocks(true);
+ List<ReplicationState> states = createTestStates(TEST_REF, 1);
+ setupFetchFactoryMock(
+ List.of(
+ new FetchFactoryEntry.Builder()
+ .withRefNames(TEST_REF)
+ .withResult(RefUpdate.Result.LOCK_FAILURE)
+ .build()));
+ objectUnderTest.addRefs(Set.of(TEST_REF));
+
+ Stream.of(1, 1).forEach(e -> objectUnderTest.run());
+
+ verify(source, times(2)).notifyFinished(objectUnderTest);
+ verify(states.get(0), times(2))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME, TEST_REF, urIish, ReplicationState.RefFetchResult.FAILED, null);
+ verify(source).reschedule(objectUnderTest, Source.RetryReason.TRANSPORT_ERROR);
+ }
+
+ @Test
+ public void shouldNotRetryOnLockingFailuresIfTheTaskWasCancelledWhileRunning() throws Exception {
+ setupMocks(true);
+ List<ReplicationState> states = createTestStates(TEST_REF, 1);
+ setupFetchFactoryMock(
+ List.of(
+ new FetchFactoryEntry.Builder()
+ .withRefNames(TEST_REF)
+ .withResult(RefUpdate.Result.LOCK_FAILURE)
+ .build()));
+ objectUnderTest.addRefs(Set.of(TEST_REF));
+ objectUnderTest.setCanceledWhileRunning();
+
+ objectUnderTest.run();
+
+ assertFinishedWithNonEmptyStateAndNoFailures();
+ verify(states.get(0))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME, TEST_REF, urIish, ReplicationState.RefFetchResult.FAILED, null);
+ verify(source, never()).reschedule(any(), any());
+ }
+
+ @Test
+ public void shouldNotRetryForUnexpectedIOErrors() throws Exception {
+ setupMocks(true);
+ List<ReplicationState> states = createTestStates(TEST_REF, 1);
+ setupFetchFactoryMock(
+ List.of(
+ new FetchFactoryEntry.Builder()
+ .withRefNames(TEST_REF)
+ .withResult(RefUpdate.Result.IO_FAILURE)
+ .build()));
+ objectUnderTest.addRefs(Set.of(TEST_REF));
+
+ objectUnderTest.run();
+
+ assertFinishedWithNonEmptyStateAndNoFailures();
+ verify(states.get(0))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME, TEST_REF, urIish, ReplicationState.RefFetchResult.FAILED, null);
+ verify(source, never()).reschedule(any(), any());
+ }
+
+ @Test
+ public void shouldTreatInexistentRefsAsFailures() throws Exception {
+ setupMocks(true);
+ List<ReplicationState> states = createTestStates(TEST_REF, 1);
+ Fetch fetch =
+ setupFetchFactoryMock(
+ List.of(
+ new FetchFactoryEntry.Builder()
+ .withRefNames(TEST_REF)
+ .withResult(RefUpdate.Result.NEW)
+ .build()));
+ when(fetch.fetch(anyList()))
+ .thenThrow(new InexistentRefTransportException(TEST_REF, new Throwable("boom")));
+ objectUnderTest.addRefs(Set.of(TEST_REF));
+
+ objectUnderTest.run();
+
+ assertFinishedWithNonEmptyStateAndFailures();
+ verify(states.get(0))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME, TEST_REF, urIish, ReplicationState.RefFetchResult.FAILED, null);
+ }
+
+ @Test
+ public void shouldRemoveAnInexistentRefFromTheDeltaAndCarryOn() throws Exception {
+ setupMocks(true);
+ String inexistentRef = "refs/heads/inexistentRef";
+ List<ReplicationState> states =
+ Stream.of(createTestStates(inexistentRef, 1), createTestStates(TEST_REF, 1))
+ .flatMap(Collection::stream)
+ .collect(Collectors.toList());
+ Fetch fetch =
+ setupFetchFactoryMock(
+ List.of(
+ new FetchFactoryEntry.Builder()
+ .withRefNames(inexistentRef)
+ .withResult(RefUpdate.Result.NEW)
+ .build(),
+ new FetchFactoryEntry.Builder()
+ .withRefNames(TEST_REF)
+ .withResult(RefUpdate.Result.NEW)
+ .build()));
+ when(fetch.fetch(anyList()))
+ .thenThrow(new InexistentRefTransportException(TEST_REF, new Throwable("boom")))
+ .thenReturn(List.of(new RefUpdateState(TEST_REF, RefUpdate.Result.NEW)));
+ objectUnderTest.addRefs(Set.of(inexistentRef, TEST_REF));
+
+ objectUnderTest.run();
+
+ assertFinishedWithNonEmptyStateAndFailures();
+ verify(states.get(0))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME,
+ inexistentRef,
+ urIish,
+ ReplicationState.RefFetchResult.NOT_ATTEMPTED,
+ null);
+ verify(states.get(0))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME, inexistentRef, urIish, ReplicationState.RefFetchResult.FAILED, null);
+ verify(states.get(1))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME,
+ TEST_REF,
+ urIish,
+ ReplicationState.RefFetchResult.SUCCEEDED,
+ RefUpdate.Result.NEW);
+ }
+
+ @Test
+ public void shouldRescheduleCertainTypesOfTransportException() throws Exception {
+ setupMocks(true);
+ List<ReplicationState> states = createTestStates(TEST_REF, 1);
+ Fetch fetch =
+ setupFetchFactoryMock(
+ List.of(
+ new FetchFactoryEntry.Builder()
+ .withRefNames(TEST_REF)
+ .withResult(RefUpdate.Result.NEW)
+ .build()));
+ when(fetch.fetch(anyList())).thenThrow(new PackProtocolException(urIish, "boom"));
+ objectUnderTest.addRefs(Set.of(TEST_REF));
+
+ objectUnderTest.run();
+
+ assertFinishedWithNonEmptyStateAndNoFailures();
+ verify(states.get(0))
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME, TEST_REF, urIish, ReplicationState.RefFetchResult.FAILED, null);
+ verify(source).reschedule(objectUnderTest, Source.RetryReason.TRANSPORT_ERROR);
+ }
+
+ @Test
+ public void shouldNotMarkReplicationTaskAsFailedIfItIsBeingRetried() throws Exception {
+ setupMocks(true);
+ List<ReplicationState> states = createTestStates(TEST_REF, 1);
+ Fetch fetch =
+ setupFetchFactoryMock(
+ List.of(
+ new FetchFactoryEntry.Builder()
+ .withRefNames(TEST_REF)
+ .withResult(RefUpdate.Result.NEW)
+ .build()));
+ when(fetch.fetch(anyList())).thenThrow(new PackProtocolException(urIish, "boom"));
+ objectUnderTest.addRefs(Set.of(TEST_REF));
+ objectUnderTest.setToRetry();
+
+ objectUnderTest.run();
+
+ assertFinishedWithNonEmptyStateAndNoFailures();
+ verify(states.get(0), never())
+ .notifyRefReplicated(
+ TEST_PROJECT_NAME, TEST_REF, urIish, ReplicationState.RefFetchResult.FAILED, null);
+ }
+
+ private void setupRequestScopeMock() {
+ when(scoper.scope(any()))
+ .thenAnswer(
+ (Answer<Callable<Object>>)
+ invocation -> {
+ Callable<Object> originalCall = (Callable<Object>) invocation.getArguments()[0];
+ return originalCall;
+ });
+ }
+
+ private void setupFailingScopeMock() {
+ when(scoper.scope(any())).thenThrow(new InternalError());
+ }
+
+ private void setupMocks(boolean runawayAllowed) throws Exception {
+ setupRequestScopeMock();
+ setupSourceMock(runawayAllowed);
+ setupGitRepoManagerMock();
+ }
+
+ private void setupSourceMock(boolean allowed) {
+ when(source.requestRunway(any())).thenReturn(allowed);
+ }
+
+ private void setupGitRepoManagerMock() throws IOException {
+ when(grm.openRepository(PROJECT_NAME)).thenReturn(repository);
+ }
+
+ private List<ReplicationState> createTestStates(String ref, int numberOfStates) {
+ List<ReplicationState> states =
+ IntStream.rangeClosed(1, numberOfStates)
+ .mapToObj(i -> Mockito.mock(ReplicationState.class))
+ .collect(Collectors.toList());
+ states.forEach(rs -> objectUnderTest.addState(ref, rs));
+
+ return states;
+ }
+
+ private void setupRemoteConfigMock(List<RefSpec> refSpecs) {
+ when(remoteConfig.getFetchRefSpecs()).thenReturn(refSpecs);
+ when(remoteConfig.getName()).thenReturn(PROJECT_NAME.get());
+ }
+
+ private Fetch setupFetchFactoryMock(List<FetchFactoryEntry> fetchFactoryEntries)
+ throws Exception {
+ return setupFetchFactoryMock(fetchFactoryEntries, Optional.empty());
+ }
+
+ private Fetch setupFetchFactoryMock(
+ List<FetchFactoryEntry> fetchFactoryEntries, Optional<List<String>> filteredRefs)
+ throws Exception {
+ List<RefSpec> refSpecs =
+ fetchFactoryEntries.stream()
+ .map(ffe -> new RefSpec(ffe.getRefSpecName()))
+ .collect(Collectors.toList());
+ List<RefUpdateState> refUpdateStates =
+ fetchFactoryEntries.stream()
+ .map(ffe -> new RefUpdateState(ffe.getRemoteName(), ffe.getResult()))
+ .collect(Collectors.toList());
+ List<RefSpec> filteredRefSpecs =
+ filteredRefs
+ .map(refList -> refList.stream().map(RefSpec::new).collect(Collectors.toList()))
+ .orElse(refSpecs);
+
+ setupRemoteConfigMock(refSpecs);
+ Fetch mockFetch = mock(Fetch.class);
+ when(fetchFactory.create(objectUnderTest.getTaskIdHex(), urIish, repository))
+ .thenReturn(mockFetch);
+ when(mockFetch.fetch(argThat(rs -> rs.containsAll(filteredRefSpecs))))
+ .thenReturn(refUpdateStates);
+ return mockFetch;
+ }
+
+ private void assertFinishedWithEmptyStateAndNoFailures() {
+ assertFinishedWithStateAndFailures(true, true);
+ }
+
+ private void assertFinishedWithNonEmptyStateAndNoFailures() {
+ assertFinishedWithStateAndFailures(false, true);
+ }
+
+ private void assertFinishedWithNonEmptyStateAndFailures() {
+ assertFinishedWithStateAndFailures(false, false);
+ }
+
+ private void assertFinishedWithStateAndFailures(boolean emptyState, boolean noFailures) {
+ assertThat(objectUnderTest.getStates().isEmpty()).isEqualTo(emptyState);
+ verify(source).notifyFinished(objectUnderTest);
+ assertThat(objectUnderTest.getFetchFailures().isEmpty()).isEqualTo(noFailures);
+ }
+}
+
+class FetchFactoryEntry {
+ private String refSpecName;
+ private String remoteName;
+ private RefUpdate.Result result;
+
+ public String getRefSpecName() {
+ return refSpecName;
+ }
+
+ public String getRemoteName() {
+ return remoteName;
+ }
+
+ public RefUpdate.Result getResult() {
+ return result;
+ }
+
+ private FetchFactoryEntry(Builder builder) {
+ this.refSpecName = builder.refSpecName;
+ this.remoteName = builder.remoteName;
+ this.result = builder.result;
+ }
+
+ public static class Builder {
+ private String refSpecName;
+ private String remoteName;
+ private RefUpdate.Result result;
+
+ public Builder withRefSpecName(String refSpecName) {
+ this.refSpecName = refSpecName;
+ return this;
+ }
+
+ public Builder withRemoteName(String remoteName) {
+ this.remoteName = remoteName;
+ return this;
+ }
+
+ public Builder withResult(RefUpdate.Result result) {
+ this.result = result;
+ return this;
+ }
+
+ public Builder refSpecNameWithDefaults(String refSpecName) {
+ this.refSpecName = refSpecName;
+ this.remoteName = refSpecName;
+ this.result = RefUpdate.Result.NEW;
+ return this;
+ }
+
+ public Builder withRefNames(String refSpecName) {
+ this.refSpecName = refSpecName;
+ this.remoteName = refSpecName;
+ return this;
+ }
+
+ public FetchFactoryEntry build() {
+ return new FetchFactoryEntry(this);
+ }
+ }
+}