/*
* Copyright 2017 ThoughtWorks, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.thoughtworks.go.server.domain;
import java.util.*;
import com.thoughtworks.go.config.CaseInsensitiveString;
import com.thoughtworks.go.domain.PipelineTimelineEntry;
import com.thoughtworks.go.listener.TimelineUpdateListener;
import com.thoughtworks.go.server.persistence.PipelineRepository;
import com.thoughtworks.go.helper.PipelineMaterialModificationMother;
import com.thoughtworks.go.server.transaction.TransactionSynchronizationManager;
import com.thoughtworks.go.server.transaction.TransactionTemplate;
import org.joda.time.DateTime;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionSynchronization;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNull.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.junit.matchers.JUnitMatchers.hasItems;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyListOf;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
public class PipelineTimelineTest {
private DateTime now;
private List<String> materials;
private PipelineTimelineEntry first;
private PipelineTimelineEntry second;
private PipelineTimelineEntry third;
private PipelineTimelineEntry fourth;
private PipelineRepository pipelineRepository;
private String pipelineName;
private TransactionTemplate transactionTemplate;
private TransactionSynchronizationManager transactionSynchronizationManager;
private TransactionSynchronization transactionSynchronization;
private PipelineTimelineEntry[] repositoryEntries;
private int txnStatus;
@Before public void setUp() throws Exception {
now = new DateTime();
pipelineRepository = mock(PipelineRepository.class);
materials = Arrays.asList("first", "second", "third", "fourth");
first = PipelineMaterialModificationMother.modification(1, materials, Arrays.asList(now, now.plusMinutes(1), now.plusMinutes(2), now.plusMinutes(3)), 1, "111", "pipeline");
second = PipelineMaterialModificationMother.modification(2, materials, Arrays.asList(now, now.plusMinutes(2), now.plusMinutes(1), now.plusMinutes(2)), 2, "222", "pipeline");
third = PipelineMaterialModificationMother.modification(3, materials, Arrays.asList(now, now.plusMinutes(2), now.plusMinutes(1), now.plusMinutes(3)), 3, "333", "pipeline");
fourth = PipelineMaterialModificationMother.modification(4, materials, Arrays.asList(now, now.plusMinutes(2), now.plusMinutes(3), now.plusMinutes(2)), 4, "444", "pipeline");
pipelineName = "pipeline";
transactionTemplate = mock(TransactionTemplate.class);
transactionSynchronizationManager = mock(TransactionSynchronizationManager.class);
}
@Test
public void shouldReturnTheNextAndPreviousOfAGivenPipeline() throws Exception {
PipelineTimeline mods = new PipelineTimeline(pipelineRepository, transactionTemplate, transactionSynchronizationManager);
mods.add(first);
mods.add(third);
mods.add(second);
mods.add(fourth);
assertBeforeAfter(mods, first, null, null);
assertBeforeAfter(mods, third, first, null);
assertBeforeAfter(mods, second, first, third);
assertBeforeAfter(mods, fourth, third, null);
}
private void assertBeforeAfter(PipelineTimeline mods, PipelineTimelineEntry actual, PipelineTimelineEntry before, PipelineTimelineEntry after) {
PipelineTimelineEntry actualBefore = mods.runBefore(actual.getId(), new CaseInsensitiveString(pipelineName));
PipelineTimelineEntry actualAfter = mods.runAfter(actual.getId(), new CaseInsensitiveString(pipelineName));
assertEquals("Expected " + before + " to be before " + actual + ". Got " + actualBefore, actualBefore, before);
assertEquals("Expected " + after + " to be after " + actual + ". Got " + actualAfter, actualAfter, after);
}
@Test public void shouldPopulateTheBeforeAndAfterNodesForAGivenPMMDuringAddition() throws Exception {
PipelineTimeline mods = new PipelineTimeline(pipelineRepository, transactionTemplate, transactionSynchronizationManager);
mods.add(first);
mods.add(fourth);
mods.add(third);
mods.add(second);
assertThat(third.insertedBefore(), is(fourth));
assertThat(third.insertedAfter(), is(first));
assertThat(second.insertedBefore(), is(third));
assertThat(second.insertedAfter(), is(first));
}
@Test public void shouldReturnThePipelineBeforeAGivenPipelineId() throws Exception {
PipelineTimeline mods = new PipelineTimeline(pipelineRepository, transactionTemplate, transactionSynchronizationManager);
mods.add(first);
mods.add(fourth);
mods.add(third);
mods.add(second);
assertThat(mods.pipelineBefore(first.getId()), is(-1L));
assertThat(mods.pipelineBefore(second.getId()), is(first.getId()));
assertThat(mods.pipelineBefore(third.getId()), is(second.getId()));
assertThat(mods.pipelineBefore(fourth.getId()), is(third.getId()));
}
@Test public void shouldReturnThePipelineAfterAGivenPipelineId() throws Exception {
PipelineTimeline mods = new PipelineTimeline(pipelineRepository, transactionTemplate, transactionSynchronizationManager);
mods.add(first);
mods.add(fourth);
mods.add(third);
mods.add(second);
assertThat(mods.pipelineAfter(first.getId()), is(second.getId()));
assertThat(mods.pipelineAfter(second.getId()), is(third.getId()));
assertThat(mods.pipelineAfter(third.getId()), is(fourth.getId()));
assertThat(mods.pipelineAfter(fourth.getId()), is(-1L));
}
@Test public void shouldBeAbleToFindThePreviousPipelineForAGivenPipeline() throws Exception {
PipelineTimeline mods = new PipelineTimeline(pipelineRepository, transactionTemplate, transactionSynchronizationManager);
mods.add(first);
mods.add(fourth);
assertThat(mods.naturalOrderBefore(fourth), is(first));
//bisect
mods.add(third);
assertThat(mods.naturalOrderBefore(fourth), is(third));
assertThat(mods.naturalOrderBefore(third), is(first));
//bisect
mods.add(second);
assertThat(mods.naturalOrderBefore(fourth), is(third));
assertThat(mods.naturalOrderBefore(third), is(second));
assertThat(mods.naturalOrderBefore(second), is(first));
}
@Test public void shouldPopuplateTheBeforeAndAfterNodesForAGivenPipelineDuringAddition() throws Exception {
PipelineTimelineEntry anotherPipeline1 = PipelineMaterialModificationMother.modification("another", 4, materials, Arrays.asList(now, now.plusMinutes(1), now.plusMinutes(2), now.plusMinutes(3)), 1, "123");
PipelineTimelineEntry anotherPipeline2 = PipelineMaterialModificationMother.modification("another", 5, materials, Arrays.asList(now, now.plusMinutes(2), now.plusMinutes(1), now.plusMinutes(3)), 2, "123");
PipelineTimelineEntry anotherPipeline3 = PipelineMaterialModificationMother.modification("another", 6, materials, Arrays.asList(now, now.plusMinutes(2), now.plusMinutes(3), now.plusMinutes(2)), 3, "123");
PipelineTimeline mods = new PipelineTimeline(pipelineRepository, transactionTemplate, transactionSynchronizationManager);
mods.add(first);
mods.add(fourth);
mods.add(anotherPipeline1);
mods.add(third);
mods.add(anotherPipeline3);
mods.add(second);
mods.add(anotherPipeline2);
assertThat(third.insertedBefore(), is(fourth));
assertThat(third.insertedAfter(), is(first));
assertThat(second.insertedBefore(), is(third));
assertThat(second.insertedAfter(), is(first));
assertThat(anotherPipeline2.insertedBefore(), is(anotherPipeline3));
assertThat(anotherPipeline2.insertedAfter(), is(anotherPipeline1));
assertThat(mods.runAfter(anotherPipeline2.getId(), new CaseInsensitiveString("another")), is(anotherPipeline3));
assertThat(mods.runBefore(anotherPipeline2.getId(), new CaseInsensitiveString("another")), is(anotherPipeline1));
assertThat(mods.runAfter(first.getId(), new CaseInsensitiveString(first.getPipelineName())), is(nullValue()));
assertThat(mods.runAfter(second.getId(), new CaseInsensitiveString(second.getPipelineName())), is(third));
}
@Test public void updateShouldNotifyListenersOnAddition() throws Exception {
stubTransactionSynchronization();
setupTransactionTemplateStub(TransactionSynchronization.STATUS_COMMITTED, true);
final List<PipelineTimelineEntry>[] entries = new List[1];
entries[0] = new ArrayList<>();
final PipelineTimeline timeline = new PipelineTimeline(pipelineRepository, transactionTemplate, transactionSynchronizationManager, new TimelineUpdateListener() {
public void added(PipelineTimelineEntry newlyAddedEntry, TreeSet<PipelineTimelineEntry> timeline) {
assertThat(timeline.contains(newlyAddedEntry), is(true));
assertThat(timeline.containsAll(entries[0]), is(true));
entries[0].add(newlyAddedEntry);
}
});
stubPipelineRepository(timeline, true, new PipelineTimelineEntry[]{first, second});
timeline.update();
assertThat(entries[0].size(), is(1));
assertThat(entries[0].contains(first), is(true));
}
@Test public void updateShouldIgnoreExceptionThrownByListenersDuringNotifications() throws Exception {
stubTransactionSynchronization();
setupTransactionTemplateStub(TransactionSynchronization.STATUS_COMMITTED, true);
TimelineUpdateListener anotherListener = mock(TimelineUpdateListener.class);
final PipelineTimeline timeline = new PipelineTimeline(pipelineRepository, transactionTemplate, transactionSynchronizationManager, new TimelineUpdateListener() {
public void added(PipelineTimelineEntry newlyAddedEntry, TreeSet<PipelineTimelineEntry> timeline) {
throw new RuntimeException();
}
}, anotherListener);
stubPipelineRepository(timeline, true, new PipelineTimelineEntry[]{first, second});
try {
timeline.update();
} catch (Exception e) {
fail("should not have failed because of exception thrown by listener");
}
verify(anotherListener).added(eq(first), any(TreeSet.class));
}
@Test public void updateOnInitShouldBeDoneOutsideTransaction() throws Exception {
PipelineTimeline timeline = new PipelineTimeline(pipelineRepository, transactionTemplate, transactionSynchronizationManager);
PipelineTimelineEntry[] entries = {first, second};
stubPipelineRepository(timeline, true, entries);
timeline.updateTimelineOnInit();
verify(pipelineRepository).updatePipelineTimeline(timeline, Arrays.asList(entries));
verifyNoMoreInteractions(transactionSynchronizationManager);
verifyNoMoreInteractions(transactionTemplate);
assertThat(timeline.maximumId(), is(2L));
assertThat(timeline.pipelineAfter(1L), is(2L));
}
@Test public void updateShouldLoadNewInstancesFromTheDatabase() throws Exception {
stubTransactionSynchronization();
setupTransactionTemplateStub(TransactionSynchronization.STATUS_COMMITTED, true);
final PipelineTimeline timeline = new PipelineTimeline(pipelineRepository, transactionTemplate, transactionSynchronizationManager);
PipelineTimelineEntry[] entries = {first, second};
stubPipelineRepository(timeline, true, entries);
timeline.update();
verify(pipelineRepository).updatePipelineTimeline(timeline, Arrays.asList(entries));
assertThat(timeline.maximumId(), is(2L));
assertThat(timeline.pipelineAfter(1L), is(2L));
}
@Test public void updateShouldRemoveTheTimelinesReturnedOnRollback() throws Exception {
stubTransactionSynchronization();
setupTransactionTemplateStub(TransactionSynchronization.STATUS_ROLLED_BACK, true);
final PipelineTimeline timeline = new PipelineTimeline(pipelineRepository, transactionTemplate, transactionSynchronizationManager);
PipelineTimelineEntry[] entries = {first, second};
stubPipelineRepository(timeline, true, new PipelineTimelineEntry[]{first, second});
timeline.update();
verify(pipelineRepository).updatePipelineTimeline(timeline, Arrays.asList(entries));
assertThat(timeline.maximumId(), is(-1L));
}
@Test
public void shouldRemove_NewlyAddedTimelineEntries_fromAllCollections_UponRollback() throws Exception {
Collection<PipelineTimelineEntry> allEntries;
stubTransactionSynchronization();
setupTransactionTemplateStub(TransactionSynchronization.STATUS_COMMITTED, true);
final PipelineTimeline timeline = new PipelineTimeline(pipelineRepository, transactionTemplate, transactionSynchronizationManager);
stubPipelineRepository(timeline, true, first, second);
timeline.update();
allEntries = timeline.getEntriesFor("pipeline");
setupTransactionTemplateStub(TransactionSynchronization.STATUS_ROLLED_BACK, false);
stubPipelineRepository(timeline, false, third, fourth);
timeline.update();
allEntries = timeline.getEntriesFor("pipeline");
assertThat(timeline.maximumId(), is(2L));
assertThat(timeline.getEntriesFor("pipeline").size(), is(2));
assertThat(allEntries, hasItems(first, second));
assertThat(timeline.instanceCount(new CaseInsensitiveString("pipeline")), is(2));
assertThat(timeline.instanceFor(new CaseInsensitiveString("pipeline"), 0), is(first));
assertThat(timeline.instanceFor(new CaseInsensitiveString("pipeline"), 1), is(second));
}
private void stubPipelineRepository(final PipelineTimeline timeline, boolean restub, final PipelineTimelineEntry... entries) {
repositoryEntries = entries;
if (restub) {
doAnswer(new Answer<Object>() {
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
for (PipelineTimelineEntry entry : repositoryEntries) {
timeline.add(entry);
}
((List<PipelineTimelineEntry>) invocationOnMock.getArguments()[1]).addAll(Arrays.asList(repositoryEntries));
return Arrays.asList(repositoryEntries);
}
}).when(pipelineRepository).updatePipelineTimeline(eq(timeline), anyListOf(PipelineTimelineEntry.class));
}
}
private void stubTransactionSynchronization() {
doAnswer(new Answer() {
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
transactionSynchronization = (TransactionSynchronization) invocationOnMock.getArguments()[0];
return null;
}
}).when(transactionSynchronizationManager).registerSynchronization(any(TransactionSynchronization.class));
}
private void setupTransactionTemplateStub(final int status, final boolean restub) throws Exception {
this.txnStatus = status;
if (restub) {
when(transactionTemplate.execute(Mockito.any(TransactionCallback.class))).thenAnswer(new Answer<Object>() {
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
TransactionCallback callback = (TransactionCallback) invocationOnMock.getArguments()[0];
callback.doInTransaction(null);
if (txnStatus == TransactionSynchronization.STATUS_COMMITTED) {
transactionSynchronization.afterCommit();
}
transactionSynchronization.afterCompletion(txnStatus);
return null;
}
});
}
}
@Test public void shouldReturnNullForPipelineBeforeAndAfterIfPipelineDoesNotExist() throws Exception {
PipelineTimeline timeline = new PipelineTimeline(pipelineRepository, transactionTemplate, transactionSynchronizationManager);
timeline.add(first);
assertThat(timeline.runBefore(2, new CaseInsensitiveString("not-present")), is(nullValue()));
assertThat(timeline.runAfter(2, new CaseInsensitiveString("not-present")), is(nullValue()));
}
@Test
public void shouldCreateANaturalOrderingHalfWayBetweenEachPipeline() throws Exception {
PipelineTimeline mods = new PipelineTimeline(pipelineRepository, transactionTemplate, transactionSynchronizationManager);
mods.add(first);
assertThat(first.naturalOrder(), is(1.0));
mods.add(fourth);
assertThat(fourth.naturalOrder(), is(2.0));
double thirdOrder = (2.0 + 1.0) / 2.0;
mods.add(third);
assertThat(third.naturalOrder(), is(thirdOrder));
mods.add(second);
assertThat(second.naturalOrder(), is((thirdOrder + 1.0) / 2.0));
}
@Test
public void shouldCreateANaturalOrderingHalfWayBetweenEachPipelineWhenInsertedInReverseOrder() throws Exception {
PipelineTimeline mods = new PipelineTimeline(pipelineRepository, transactionTemplate, transactionSynchronizationManager);
mods.add(fourth);
assertThat(fourth.naturalOrder(), is(1.0));
mods.add(first);
assertThat(first.naturalOrder(), is(0.5));
double thirdOrder = (1.0 + 0.5) / 2.0;
mods.add(third);
assertThat(third.naturalOrder(), is(thirdOrder));
mods.add(second);
assertThat(second.naturalOrder(), is((thirdOrder + 0.5) / 2.0));
}
@Test
public void shouldNotAllowResetingOfNaturalOrder() {
PipelineTimeline mods = new PipelineTimeline(pipelineRepository, transactionTemplate, transactionSynchronizationManager);
mods.add(fourth);
mods.add(first);
try {
mods.add(fourth);
} catch (Exception e) {
assertThat(e.getMessage(), is("Calculated natural ordering 1.5 is not the same as the existing naturalOrder 1.0, for pipeline pipeline, with id 4"));
}
}
}