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); + } + } +}