/*
* Copyright 2016 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.service;
import com.thoughtworks.go.config.GoConfigDao;
import com.thoughtworks.go.domain.CannotScheduleException;
import com.thoughtworks.go.domain.JobState;
import com.thoughtworks.go.domain.Pipeline;
import com.thoughtworks.go.domain.Stage;
import com.thoughtworks.go.domain.StageIdentifier;
import com.thoughtworks.go.domain.StageState;
import com.thoughtworks.go.domain.activity.AgentAssignment;
import com.thoughtworks.go.domain.activity.JobStatusCache;
import com.thoughtworks.go.domain.activity.StageStatusCache;
import com.thoughtworks.go.fixture.PipelineWithTwoStages;
import com.thoughtworks.go.fixture.SchedulerFixture;
import com.thoughtworks.go.i18n.Localizer;
import com.thoughtworks.go.server.GoUnauthorizedException;
import com.thoughtworks.go.server.cache.GoCache;
import com.thoughtworks.go.server.dao.DatabaseAccessHelper;
import com.thoughtworks.go.server.dao.JobInstanceDao;
import com.thoughtworks.go.server.dao.PipelineDao;
import com.thoughtworks.go.server.dao.StageDao;
import com.thoughtworks.go.server.domain.StageStatusListener;
import com.thoughtworks.go.server.messaging.JobResultMessage;
import com.thoughtworks.go.server.messaging.JobResultTopic;
import com.thoughtworks.go.server.messaging.StageStatusMessage;
import com.thoughtworks.go.server.messaging.StageStatusTopic;
import com.thoughtworks.go.server.perf.SchedulingPerformanceLogger;
import com.thoughtworks.go.server.persistence.MaterialRepository;
import com.thoughtworks.go.server.scheduling.PipelineScheduledTopic;
import com.thoughtworks.go.server.service.result.HttpLocalizedOperationResult;
import com.thoughtworks.go.server.transaction.TransactionCallback;
import com.thoughtworks.go.server.transaction.TransactionSynchronizationManager;
import com.thoughtworks.go.server.transaction.TransactionTemplate;
import com.thoughtworks.go.serverhealth.ServerHealthService;
import com.thoughtworks.go.serverhealth.ServerHealthStates;
import com.thoughtworks.go.util.GoConfigFileHelper;
import com.thoughtworks.go.plugin.infra.PluginManager;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.TransactionStatus;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
"classpath:WEB-INF/applicationContext-global.xml",
"classpath:WEB-INF/applicationContext-dataLocalAccess.xml",
"classpath:WEB-INF/applicationContext-acegi-security.xml"
})
public class ScheduleServiceStageTriggerTest {
@Autowired private GoConfigDao goConfigDao;
@Autowired private StageService stageService;
@Autowired private PipelineService pipelineService;
@Autowired private ScheduleService scheduleService;
@Autowired private DatabaseAccessHelper dbHelper;
@Autowired private MaterialRepository materialRepository;
@Autowired private GoConfigService goConfigService;
@Autowired private CachedCurrentActivityService cachedCurrentActivityService;
@Autowired private SchedulingCheckerService schedulingCheckerService;
@Autowired private PipelineScheduledTopic pipelineScheduledTopic;
@Autowired private PipelineDao pipelineDao;
@Autowired private StageDao stageDao;
@Autowired private StageOrderService stageOrderService;
@Autowired private SecurityService securityService;
@Autowired private PipelineScheduleQueue pipelineScheduleQueue;
@Autowired private JobInstanceService jobInstanceService;
@Autowired private JobInstanceDao jobInstanceDao;
@Autowired private AgentAssignment agentAssignment;
@Autowired private EnvironmentConfigService environmentConfigService;
@Autowired private PipelineLockService pipelineLockService;
@Autowired private ServerHealthService serverHealthService;
@Autowired private TransactionTemplate transactionTemplate;
@Autowired private StageStatusCache stageStatusCache;
@Autowired private JobStatusCache jobStatusCache;
@Autowired private PropertiesService propertiesService;
@Autowired private ChangesetService changesetService;
@Autowired private TransactionSynchronizationManager transactionSynchronizationManager;
@Autowired private GoCache goCache;
@Autowired private Localizer localizer;
@Autowired private PluginManager pluginManager;
private PipelineWithTwoStages preCondition;
private SchedulerFixture schedulerFixture;
private static GoConfigFileHelper configHelper = new GoConfigFileHelper();
@Before
public void setUp() throws Exception {
preCondition = new PipelineWithTwoStages(materialRepository, transactionTemplate);
configHelper.onSetUp();
configHelper.usingCruiseConfigDao(goConfigDao);
dbHelper.onSetUp();
preCondition.usingConfigHelper(configHelper).usingDbHelper(dbHelper).onSetUp();
schedulerFixture = new SchedulerFixture(dbHelper, stageDao, scheduleService);
}
@After
public void teardown() throws Exception {
dbHelper.onTearDown();
preCondition.onTearDown();
}
@Test
public void shouldTriggerNextStageByHistoricalOrder() throws Exception {
// having a pipeline with two stages both are completed
Pipeline pipeline = preCondition.createdPipelineWithAllStagesPassed();
// now we reorder the two stages via config from dev -> ft to ft -> dev
reOrderTwoStages();
// and then rerun the devstage
scheduleService.rerunStage(pipeline, preCondition.devStage(), "anyone");
pipeline = pipelineService.mostRecentFullPipelineByName(preCondition.pipelineName);
dbHelper.passStage(pipeline.getFirstStage());
Stage devStage = stageDao.mostRecentWithBuilds(preCondition.pipelineName, preCondition.devStage());
Stage oldFtStage = stageDao.mostRecentWithBuilds(preCondition.pipelineName, preCondition.ftStage());
// after devStage passes, it should automatically trigger the NEXT stage according to historical order
// (ftStage), but NOT according to what is currently defined in the config file (none)
scheduleService.automaticallyTriggerRelevantStagesFollowingCompletionOf(devStage);
// verifying that ftStage is rerun
Stage ftStage = stageDao.mostRecentWithBuilds(preCondition.pipelineName, preCondition.ftStage());
assertThat(String.format("Should schedule new ft stage: old id: %s, new id: %s",
oldFtStage.getId(), ftStage.getId()), ftStage.getId() > oldFtStage.getId(), is(true));
assertThat(ftStage.getJobInstances().first().getState(), is(JobState.Scheduled));
}
@Test
public void shouldNotTriggerNextStageFromConfigIfItIsScheduled() throws Exception {
// having a pipeline with two stages both are completed
Pipeline pipeline = preCondition.createdPipelineWithAllStagesPassed();
Stage oldDevStage = pipeline.getStages().byName(preCondition.devStage);
// now we reorder the two stages via config from dev -> ft to ft -> dev
reOrderTwoStages();
// and then rerun the ftstage
schedulerFixture.rerunAndPassStage(pipeline, preCondition.ftStage());
// after ftStage passes, it should NOT trigger dev stage again otherwise this will
// ends up in a deadlock
Stage ftStage = stageDao.mostRecentWithBuilds(preCondition.pipelineName, preCondition.ftStage());
scheduleService.automaticallyTriggerRelevantStagesFollowingCompletionOf(ftStage);
// verifying that devStage is NOT rerun
Stage devStage = stageDao.mostRecentWithBuilds(preCondition.pipelineName, preCondition.devStage());
assertThat("Should not schedule dev stage again", devStage.getId(), is(oldDevStage.getId()));
}
@Test
public void shouldNotRerunCurrentStageInNewerPipeline() throws Exception {
Pipeline olderPipeline = preCondition.createdPipelineWithAllStagesPassed();
Pipeline newerPipeline = preCondition.createdPipelineWithAllStagesPassed();
Stage oldFtStage = newerPipeline.getStages().byName(preCondition.ftStage);
schedulerFixture.rerunAndPassStage(olderPipeline, preCondition.ftStage());
Stage passedFtStage = pipelineService.fullPipelineById(olderPipeline.getId()).getStages().byName(
preCondition.ftStage);
scheduleService.automaticallyTriggerRelevantStagesFollowingCompletionOf(passedFtStage);
Stage ftStage = pipelineService.mostRecentFullPipelineByName(preCondition.pipelineName).getStages().byName(
preCondition.ftStage);
assertThat("Should not rerun ft in newer pipeline", ftStage.getId(), is(oldFtStage.getId()));
}
@Test
public void cancelCurrentStageShouldTriggerSameStageInMostRecentPipeline() throws Exception {
Pipeline oldest = preCondition.createPipelineWithFirstStagePassedAndSecondStageRunning();
preCondition.createPipelineWithFirstStagePassedAndSecondStageHasNotStarted();
preCondition.createPipelineWithFirstStagePassedAndSecondStageHasNotStarted();
long cancelledStageId = oldest.getStages().byName(preCondition.ftStage).getId();
scheduleService.cancelAndTriggerRelevantStages(cancelledStageId, null, null);
Pipeline mostRecent = pipelineService.mostRecentFullPipelineByName(preCondition.pipelineName);
Stage cancelledStage = stageService.stageById(cancelledStageId);
assertThat(cancelledStage.stageState(), is(StageState.Cancelled));
assertThat(mostRecent.getStages().byName(preCondition.ftStage).stageState(), is(StageState.Building));
}
@Test
public void errorInSchedulingSubsequentStageShouldNotRollbackCancelAction() throws Exception {
Pipeline oldest = preCondition.createPipelineWithFirstStagePassedAndSecondStageRunning();
preCondition.createPipelineWithFirstStagePassedAndSecondStageHasNotStarted();
long cancelledStageId = oldest.getStages().byName(preCondition.ftStage).getId();
preCondition.setRunOnAllAgentsForSecondStage();
try {
scheduleService.cancelAndTriggerRelevantStages(cancelledStageId, null, null);
fail("Must have failed scheduling the next stage as it has run on all agents");
} catch (CannotScheduleException expected) {
}
Pipeline mostRecent = pipelineService.mostRecentFullPipelineByName(preCondition.pipelineName);
Stage cancelledStage = stageService.stageById(cancelledStageId);
assertThat(cancelledStage.stageState(), is(StageState.Cancelled));
assertThat(mostRecent.getStages().size(), is(1));
}
@Test
public void cancelCurrentStageShouldNotTriggerSameStageInMostRecentPipelineWhenItIsScheduledAlready()
throws Exception {
Pipeline oldest = preCondition.createPipelineWithFirstStagePassedAndSecondStageRunning();
preCondition.createPipelineWithFirstStagePassedAndSecondStageHasNotStarted();
preCondition.createdPipelineWithAllStagesPassed();
long cancelledStageId = oldest.getStages().byName(preCondition.ftStage).getId();
scheduleService.cancelAndTriggerRelevantStages(cancelledStageId, null, null);
Pipeline mostRecent = pipelineService.mostRecentFullPipelineByName(preCondition.pipelineName);
Stage cancelledStage = stageService.stageById(cancelledStageId);
assertThat(cancelledStage.stageState(), is(StageState.Cancelled));
assertThat(mostRecent.getStages().byName(preCondition.ftStage).stageState(), is(StageState.Passed));
}
@Test
public void shouldDoCancellationInTransaction() throws Exception {
Pipeline oldest = preCondition.createPipelineWithFirstStagePassedAndSecondStageRunning();
preCondition.createPipelineWithFirstStagePassedAndSecondStageHasNotStarted();
preCondition.createPipelineWithFirstStagePassedAndSecondStageHasNotStarted();
Stage stage = oldest.getStages().byName(preCondition.ftStage);
StageStatusTopic stageStatusTopic = mock(StageStatusTopic.class);
JobResultTopic jobResultTopic = mock(JobResultTopic.class);
StageStatusListener stageStatusListener = mock(StageStatusListener.class);
StageService stageService = mock(StageService.class);
when(stageService.stageById(stage.getId())).thenReturn(stage);
Mockito.doAnswer(new Answer() {
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
throw new RuntimeException();
}
}).when(stageService).cancelStage(stage);
StageOrderService stageOrderService = mock(StageOrderService.class);
SchedulingPerformanceLogger schedulingPerformanceLogger = mock(SchedulingPerformanceLogger.class);
scheduleService = new ScheduleService(goConfigService, pipelineService, stageService, schedulingCheckerService, pipelineScheduledTopic, pipelineDao, stageDao,
stageOrderService, securityService, pipelineScheduleQueue, this.jobInstanceService, jobInstanceDao, agentAssignment, environmentConfigService, pipelineLockService, serverHealthService,
transactionTemplate, null, transactionSynchronizationManager, null, null, null, null, schedulingPerformanceLogger, null);
try {
scheduleService.cancelAndTriggerRelevantStages(stage.getId(), null, null);
} catch (RuntimeException e) {
//ignore
}
verify(stageStatusTopic, never()).post(any(StageStatusMessage.class));
verify(jobResultTopic, never()).post(any(JobResultMessage.class));
verify(stageStatusListener, never()).stageStatusChanged(any(Stage.class));
}
private JobInstanceService jobInstanceService(JobResultTopic jobResultTopic) {
ServerHealthService serverHealthService = mock(ServerHealthService.class);
when(serverHealthService.getAllLogs()).thenReturn(new ServerHealthStates());
return new JobInstanceService(jobInstanceDao, propertiesService, jobResultTopic, jobStatusCache, transactionTemplate,
transactionSynchronizationManager, null, null, goConfigService, null, pluginManager, serverHealthService);
}
@Test
public void shouldNotNotifyListenersForWhenCancelStageTransactionRollsback() throws Exception {
Pipeline oldest = preCondition.createPipelineWithFirstStagePassedAndSecondStageRunning();
preCondition.createPipelineWithFirstStagePassedAndSecondStageHasNotStarted();
preCondition.createPipelineWithFirstStagePassedAndSecondStageHasNotStarted();
final Stage stage = oldest.getStages().byName(preCondition.ftStage);
final StageIdentifier identifier = stage.getIdentifier();
StageStatusTopic stageStatusTopic = mock(StageStatusTopic.class);
JobResultTopic jobResultTopic = mock(JobResultTopic.class);
StageStatusListener stageStatusListener = mock(StageStatusListener.class);
JobInstanceService jobInstanceService = jobInstanceService(jobResultTopic);
StageService stageService = new StageService(stageDao, jobInstanceService, stageStatusTopic, stageStatusCache, securityService, pipelineDao, changesetService, goConfigService,
transactionTemplate,
transactionSynchronizationManager, goCache, stageStatusListener);
SchedulingPerformanceLogger schedulingPerformanceLogger = mock(SchedulingPerformanceLogger.class);
scheduleService = new ScheduleService(goConfigService, pipelineService, stageService, schedulingCheckerService, pipelineScheduledTopic, pipelineDao, stageDao,
stageOrderService, securityService, pipelineScheduleQueue, this.jobInstanceService, jobInstanceDao, agentAssignment, environmentConfigService, pipelineLockService, serverHealthService,
transactionTemplate, null, transactionSynchronizationManager, null, null, null, null, schedulingPerformanceLogger, null);
try {
transactionTemplate.executeWithExceptionHandling(new TransactionCallback() {
@Override public Object doInTransaction(TransactionStatus status) throws Exception {
scheduleService.cancelAndTriggerRelevantStages(stage.getId(), null, null);
throw new GoUnauthorizedException();
}
});
} catch (Exception e) {
//ignore
}
verify(stageStatusTopic, never()).post(any(StageStatusMessage.class));
verify(jobResultTopic, never()).post(any(JobResultMessage.class));
verify(stageStatusListener, never()).stageStatusChanged(any(Stage.class));
}
@Test
public void shouldBeAbletoCancelStageByName() throws Exception {
Pipeline oldest = preCondition.createPipelineWithFirstStagePassedAndSecondStageRunning();
preCondition.createPipelineWithFirstStagePassedAndSecondStageHasNotStarted();
preCondition.createPipelineWithFirstStagePassedAndSecondStageHasNotStarted();
Stage stage = oldest.getStages().byName(preCondition.ftStage);
StageIdentifier identifier = stage.getIdentifier();
HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
Stage cancelledStage = scheduleService.cancelAndTriggerRelevantStages(stage.getId(), null, result);
Pipeline mostRecent = pipelineService.mostRecentFullPipelineByName(preCondition.pipelineName);
assertThat(cancelledStage.stageState(), is(StageState.Cancelled));
assertThat(mostRecent.getStages().byName(preCondition.ftStage).stageState(), is(StageState.Building));
assertThat(result.message(localizer), is("Stage cancelled successfully."));
}
private void reOrderTwoStages() throws Exception {
configHelper.removeStage(preCondition.pipelineName, preCondition.devStage);
configHelper.addStageToPipeline(preCondition.pipelineName, preCondition.devStage);
}
}