/*
* 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.service;
import com.thoughtworks.go.config.*;
import com.thoughtworks.go.domain.*;
import com.thoughtworks.go.domain.activity.AgentAssignment;
import com.thoughtworks.go.domain.buildcause.BuildCause;
import com.thoughtworks.go.domain.materials.MaterialConfig;
import com.thoughtworks.go.helper.*;
import com.thoughtworks.go.i18n.Localizer;
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.Username;
import com.thoughtworks.go.server.perf.SchedulingPerformanceLogger;
import com.thoughtworks.go.server.scheduling.PipelineScheduledMessage;
import com.thoughtworks.go.server.scheduling.PipelineScheduledTopic;
import com.thoughtworks.go.server.service.result.HttpLocalizedOperationResult;
import com.thoughtworks.go.server.transaction.TestTransactionSynchronizationManager;
import com.thoughtworks.go.server.transaction.TestTransactionTemplate;
import com.thoughtworks.go.serverhealth.HealthStateScope;
import com.thoughtworks.go.serverhealth.HealthStateType;
import com.thoughtworks.go.serverhealth.ServerHealthService;
import com.thoughtworks.go.serverhealth.ServerHealthState;
import com.thoughtworks.go.util.GoConstants;
import com.thoughtworks.go.util.TimeProvider;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
import java.util.HashMap;
import static com.thoughtworks.go.helper.ModificationsMother.modifySomeFiles;
import static com.thoughtworks.go.util.DataStructureUtils.m;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNull.nullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.*;
public class ScheduleServiceTest {
private ScheduleService service;
private JobInstanceService jobInstanceService;
private GoConfigService goConfigService;
private EnvironmentConfigService environmentConfigService;
private ServerHealthService serverHealthService;
private SchedulingCheckerService schedulingChecker;
private PipelineScheduleQueue pipelineScheduleQueue;
private ConsoleActivityMonitor consoleActivityMonitor;
public PipelinePauseService pipelinePauseService;
private StageService stageService;
private SecurityService securityService;
private PipelineService pipelineService;
private Localizer localizer;
private TimeProvider timeProvider;
private InstanceFactory instanceFactory = null;
private SchedulingPerformanceLogger schedulingPerformanceLogger;
private ElasticProfileService elasticProfileService;
@Before
public void setup() {
jobInstanceService = mock(JobInstanceService.class);
goConfigService = mock(GoConfigService.class);
environmentConfigService = mock(EnvironmentConfigService.class);
serverHealthService = mock(ServerHealthService.class);
final TestTransactionSynchronizationManager synchronizationManager = new TestTransactionSynchronizationManager();
schedulingChecker = mock(SchedulingCheckerService.class);
pipelineScheduleQueue = mock(PipelineScheduleQueue.class);
consoleActivityMonitor = mock(ConsoleActivityMonitor.class);
pipelinePauseService = mock(PipelinePauseService.class);
stageService = mock(StageService.class);
securityService = mock(SecurityService.class);
pipelineService = mock(PipelineService.class);
localizer = mock(Localizer.class);
timeProvider = new TimeProvider();
schedulingPerformanceLogger = mock(SchedulingPerformanceLogger.class);
elasticProfileService = mock(ElasticProfileService.class);
service = new ScheduleService(goConfigService, pipelineService, stageService, schedulingChecker, mock(
PipelineScheduledTopic.class), mock(PipelineDao.class), mock(StageDao.class), mock(StageOrderService.class), securityService, pipelineScheduleQueue,
jobInstanceService, mock(JobInstanceDao.class), mock(AgentAssignment.class), environmentConfigService, mock(PipelineLockService.class), serverHealthService,
new TestTransactionTemplate(synchronizationManager),
mock(AgentService.class), synchronizationManager, timeProvider, consoleActivityMonitor, pipelinePauseService, instanceFactory, schedulingPerformanceLogger, elasticProfileService);
}
@Test
public void shouldCancelStage() throws Exception {
HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
Pipeline pipeline = PipelineMother.pipeline("pipeline-name", StageMother.passedStageInstance("mingle", "job-bar", "pipeline-name"));
Stage spiedStage = spy(pipeline.getFirstStage());
long stageId = spiedStage.getId();
Username admin = new Username(new CaseInsensitiveString("admin"));
doReturn(true).when(spiedStage).isActive();
when(stageService.stageById(stageId)).thenReturn(spiedStage);
when(securityService.hasOperatePermissionForStage(pipeline.getName(), spiedStage.getName(), admin.getUsername().toString())).thenReturn(true);
ScheduleService spyedService = spy(service);
doNothing().when(spyedService).automaticallyTriggerRelevantStagesFollowingCompletionOf(spiedStage);
Stage resultStage = spyedService.cancelAndTriggerRelevantStages(stageId, admin, result);
assertThat(resultStage, is(spiedStage));
assertThat(result.httpCode(), is(SC_OK));
assertThat(result.isSuccessful(), is(true));
when(localizer.localize("STAGE_CANCELLED_SUCCESSFULLY", new Object[] {})).thenReturn("Stage cancelled successfully.");
assertThat(result.message(localizer), is("Stage cancelled successfully."));
verify(localizer).localize("STAGE_CANCELLED_SUCCESSFULLY", new Object[] {});
verify(securityService).hasOperatePermissionForStage(pipeline.getName(), spiedStage.getName(), admin.getUsername().toString());
verify(stageService).cancelStage(spiedStage);
verify(spiedStage).isActive();
}
@Test
public void shouldNotCancelStageIfItsNotActive() throws Exception {
HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
Pipeline pipeline = PipelineMother.pipeline("pipeline-name", StageMother.passedStageInstance("mingle", "job-bar", "pipeline-name"));
Stage firstStage = pipeline.getFirstStage();
long stageId = firstStage.getId();
Username admin = new Username(new CaseInsensitiveString("admin"));
when(stageService.stageById(stageId)).thenReturn(firstStage);
Stage resultStage = service.cancelAndTriggerRelevantStages(stageId, admin, result);
assertThat(resultStage, is(firstStage));
assertThat(result.httpCode(), is(SC_OK));
assertThat(result.isSuccessful(), is(true));
assertThat(result.hasMessage(), is(true));
Localizer localizer = mock(Localizer.class);
String respMsg = "Stage is not active. Cancellation Ignored";
String stageNotActiveKey = "STAGE_IS_NOT_ACTIVE_FOR_CANCELLATION";
when(localizer.localize(eq(stageNotActiveKey),anyVararg())).thenReturn(respMsg);
assertThat(result.message(localizer),is(respMsg));
verify(stageService).stageById(stageId);
verify(localizer).localize(eq(stageNotActiveKey),anyVararg());
}
@Test
public void shouldCancelStageUsingPipelineNameAndStageName() throws Exception {
HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
String pipelineName = "pipeline-name";
Username admin = new Username(new CaseInsensitiveString("admin"));
String stageName = "mingle";
Pipeline pipeline = PipelineMother.pipeline(pipelineName, StageMother.passedStageInstance(stageName, "job-bar", pipelineName));
Stage firstStage = pipeline.getFirstStage();
long stageId = firstStage.getId();
when(stageService.findLatestStage(pipelineName, stageName)).thenReturn(firstStage);
ScheduleService spyedService = spy(service);
doReturn(firstStage).when(spyedService).cancelAndTriggerRelevantStages(stageId, admin, result);
Stage resultStage = spyedService.cancelAndTriggerRelevantStages(pipelineName, stageName, admin, result);
assertThat(resultStage, is(firstStage));
assertThat(result.httpCode(), is(SC_OK));
assertThat(result.isSuccessful(), is(true));
verify(stageService).findLatestStage(pipelineName, stageName);
}
@Test
public void shouldNotCancelStageWhenTheUserDoesNotHaveOperatePermission() throws Exception {
HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
Pipeline pipeline = PipelineMother.pipeline("pipeline-name", StageMother.passedStageInstance("mingle", "job-bar", "pipeline-name"));
Stage spiedStage = spy(pipeline.getFirstStage());
long stageId = spiedStage.getId();
Username admin = new Username(new CaseInsensitiveString("admin"));
doReturn(true).when(spiedStage).isActive();
when(stageService.stageById(stageId)).thenReturn(spiedStage);
when(securityService.hasOperatePermissionForStage(pipeline.getName(), spiedStage.getName(), admin.getUsername().toString())).thenReturn(false);
Stage resultStage = service.cancelAndTriggerRelevantStages(stageId, admin, result);
assertThat(resultStage, is(nullValue()));
assertThat(result.httpCode(), is(SC_UNAUTHORIZED));
assertThat(result.isSuccessful(), is(false));
verify(securityService).hasOperatePermissionForStage(pipeline.getName(), spiedStage.getName(), admin.getUsername().toString());
verify(stageService, never()).cancelStage(spiedStage);
verify(spiedStage).isActive();
}
@Test
public void shouldNotCompleteBuildThatIsCancelled() {
String agentUuid = "uuid";
JobIdentifier jobIdendifier = new JobIdentifier("pipeline", 1, "label", "stage", "LATEST", "build", 1L);
final JobInstance job = new JobInstance("test");
//MF this is!
job.setResult(JobResult.Cancelled);
when(jobInstanceService.buildByIdWithTransitions(jobIdendifier.getBuildId())).thenReturn(job);
service.jobCompleting(jobIdendifier, JobResult.Passed, agentUuid);
verify(jobInstanceService).buildByIdWithTransitions(jobIdendifier.getBuildId());
verifyNoMoreInteractions(jobInstanceService);
}
@Test
public void shouldSetServerHealthMessageWhenStageScheduleFailsWithCannotScheduleException() {
final PipelineConfig pipelineConfig = PipelineConfigMother.pipelineConfig("pipeline-quux");
when(goConfigService.pipelineConfigNamed(new CaseInsensitiveString("pipeline-quux"))).thenReturn(pipelineConfig);
when(environmentConfigService.agentsForPipeline(new CaseInsensitiveString("pipeline-quux"))).thenReturn(new Agents());
when(goConfigService.scheduleStage("pipeline-quux", "mingle", new DefaultSchedulingContext("loser", new Agents()))).thenThrow(new CannotScheduleException("foo", "stage-baz"));
try {
Pipeline pipeline = PipelineMother.pipeline("pipeline-quux", StageMother.passedStageInstance("mingle", "job-bar", "pipeline-name"));
service.scheduleStage(pipeline, "mingle", "loser",
new ScheduleService.NewStageInstanceCreator(goConfigService), new ScheduleService.ExceptioningErrorHandler());
fail("should have failed as stage could not be scheduled");
} catch (CannotScheduleException e) {
assertThat(e.getMessage(), is("foo"));
}
verify(serverHealthService).update(ServerHealthState.failedToScheduleStage(HealthStateType.general(HealthStateScope.forStage("pipeline-quux", "stage-baz")),
"pipeline-quux","stage-baz" , "foo"));
}
@Test
public void shouldClearServerHealthMessageWhenStageScheduleCompletesSuccessfully() {
final PipelineConfig pipelineConfig = PipelineConfigMother.pipelineConfig("pipeline-quux");
when(goConfigService.pipelineConfigNamed(new CaseInsensitiveString("pipeline-quux"))).thenReturn(pipelineConfig);
when(environmentConfigService.agentsForPipeline(new CaseInsensitiveString("pipeline-quux"))).thenReturn(new Agents());
when(goConfigService.scheduleStage("pipeline-quux", "mingle", new DefaultSchedulingContext("loser", new Agents()))).thenReturn(new Stage());
Stage stageConfig = StageMother.passedStageInstance("mingle", "job-bar", "pipeline-name");
service.scheduleStage(PipelineMother.pipeline("pipeline-quux", stageConfig), "mingle", "loser",
new ScheduleService.NewStageInstanceCreator(goConfigService), new ScheduleService.ExceptioningErrorHandler());
verify(serverHealthService).update(ServerHealthState.success(HealthStateType.general(HealthStateScope.forStage("pipeline-quux", "mingle"))));
}
@Test
public void shouldSetServerHealthMessageWhenPipelineCreationFailsWithCannotScheduleException() {
final PipelineConfig pipelineConfig = PipelineConfigMother.pipelineConfig("pipeline-quux");
when(goConfigService.pipelineConfigNamed(new CaseInsensitiveString("pipeline-quux"))).thenReturn(pipelineConfig);
CruiseConfig cruiseConfig = mock(BasicCruiseConfig.class);
when(cruiseConfig.getMd5()).thenReturn("md5-test");
when(goConfigService.getCurrentConfig()).thenReturn(cruiseConfig);
when(schedulingChecker.canAutoTriggerConsumer(pipelineConfig)).thenReturn(true);
when(pipelineScheduleQueue.createPipeline(any(BuildCause.class), eq(pipelineConfig), any(SchedulingContext.class), eq("md5-test"), eq(timeProvider))).thenThrow(
new CannotScheduleException("foo", "stage-baz"));
final HashMap<String, BuildCause> map = new HashMap<>();
map.put("pipeline-quux", BuildCause.createManualForced());
when(pipelineScheduleQueue.toBeScheduled()).thenReturn(map);
service.autoSchedulePipelinesFromRequestBuffer();
verify(serverHealthService).update(ServerHealthState.failedToScheduleStage(HealthStateType.general(HealthStateScope.forStage("pipeline-quux", "stage-baz")),
"pipeline-quux","stage-baz" , "foo"));
}
@Test
public void shouldClearServerHealthMessageWhenPipelineGetsCreatedSuccessfully() {
final PipelineConfig pipelineConfig = PipelineConfigMother.pipelineConfig("pipeline-quux");
final MaterialConfig materialConfig = pipelineConfig.materialConfigs().get(0);
when(goConfigService.pipelineConfigNamed(new CaseInsensitiveString("pipeline-quux"))).thenReturn(pipelineConfig);
CruiseConfig cruiseConfig = mock(BasicCruiseConfig.class);
when(cruiseConfig.getMd5()).thenReturn("md5-test");
when(goConfigService.getCurrentConfig()).thenReturn(cruiseConfig);
when(schedulingChecker.canAutoTriggerConsumer(pipelineConfig)).thenReturn(true);
when(pipelineScheduleQueue.createPipeline(any(BuildCause.class), eq(pipelineConfig), any(SchedulingContext.class), eq("md5-test"), eq(timeProvider))).thenReturn(PipelineMother.schedule(pipelineConfig,
BuildCause.createManualForced(new MaterialRevisions(new MaterialRevision(new MaterialConfigConverter().toMaterial(materialConfig), ModificationsMother.aCheckIn("123", "foo.c"))), new Username(new CaseInsensitiveString("loser")))));
final HashMap<String, BuildCause> map = new HashMap<>();
map.put("pipeline-quux", BuildCause.createManualForced());
when(pipelineScheduleQueue.toBeScheduled()).thenReturn(map);
service.autoSchedulePipelinesFromRequestBuffer();
verify(serverHealthService).update(ServerHealthState.success(HealthStateType.general(HealthStateScope.forStage("pipeline-quux", "mingle"))));
}
@Test
public void shouldNotifyForEveryPipelineCreated() throws Exception {
CruiseConfig cruiseConfig = mock(BasicCruiseConfig.class);
when(cruiseConfig.getMd5()).thenReturn("md5-test");
when(goConfigService.getCurrentConfig()).thenReturn(cruiseConfig);
StubPipelineScheduleTopic stubTopic = new StubPipelineScheduleTopic();
service = new ScheduleService(goConfigService, null, null, schedulingChecker, stubTopic, null, null, null, null, pipelineScheduleQueue,
jobInstanceService, null, null, environmentConfigService, null, serverHealthService, null, null, null, timeProvider, null, null, null, schedulingPerformanceLogger, elasticProfileService);
PipelineConfig mingleConfig = PipelineConfigMother.createPipelineConfig("mingle", "build", "unit", "functional");
PipelineConfig evolveConfig = PipelineConfigMother.createPipelineConfig("evolve", "build", "unit");
BuildCause mingleBuildCause = modifySomeFiles(mingleConfig);
BuildCause evolveBuildCause = modifySomeFiles(evolveConfig);
when(pipelineScheduleQueue.toBeScheduled()).thenReturn(m("mingle", mingleBuildCause, "evolve", evolveBuildCause));
when(goConfigService.pipelineConfigNamed(new CaseInsensitiveString("mingle"))).thenReturn(mingleConfig);
when(goConfigService.pipelineConfigNamed(new CaseInsensitiveString("evolve"))).thenReturn(evolveConfig);
when(schedulingChecker.canAutoTriggerConsumer(mingleConfig)).thenReturn(true);
when(schedulingChecker.canAutoTriggerConsumer(evolveConfig)).thenReturn(true);
when(pipelineScheduleQueue.createPipeline(mingleBuildCause, mingleConfig, new DefaultSchedulingContext(GoConstants.DEFAULT_APPROVED_BY, new Agents()), "md5-test", timeProvider)).thenReturn(
PipelineMother.schedule(mingleConfig, mingleBuildCause));
when(pipelineScheduleQueue.createPipeline(evolveBuildCause, evolveConfig, new DefaultSchedulingContext(GoConstants.DEFAULT_APPROVED_BY, new Agents()), "md5-test", timeProvider)).thenReturn(
PipelineMother.schedule(evolveConfig, evolveBuildCause));
service.autoSchedulePipelinesFromRequestBuffer();
assertThat(stubTopic.callCount, Matchers.is(2));
}
@Test
public void shouldCancelUnresponsiveJobs() {
service.cancelHungJobs();
verify(consoleActivityMonitor).cancelUnresponsiveJobs(service);
}
private static class StubPipelineScheduleTopic extends PipelineScheduledTopic {
private int callCount;
public StubPipelineScheduleTopic() {
super(null);
}
public void post(PipelineScheduledMessage message) {
callCount++;
}
}
}