/*
* 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.config.materials.MaterialConfigs;
import com.thoughtworks.go.domain.*;
import com.thoughtworks.go.domain.activity.JobStatusCache;
import com.thoughtworks.go.helper.JobInstanceMother;
import com.thoughtworks.go.helper.PartialConfigMother;
import com.thoughtworks.go.helper.PipelineConfigMother;
import com.thoughtworks.go.plugin.infra.PluginManager;
import com.thoughtworks.go.server.dao.JobInstanceDao;
import com.thoughtworks.go.server.dao.StageDao;
import com.thoughtworks.go.server.domain.JobStatusListener;
import com.thoughtworks.go.server.domain.Username;
import com.thoughtworks.go.server.messaging.JobResultMessage;
import com.thoughtworks.go.server.messaging.JobResultTopic;
import com.thoughtworks.go.server.service.result.HttpOperationResult;
import com.thoughtworks.go.server.transaction.TestTransactionSynchronizationManager;
import com.thoughtworks.go.server.transaction.TestTransactionTemplate;
import com.thoughtworks.go.server.transaction.TransactionTemplate;
import com.thoughtworks.go.server.ui.JobInstancesModel;
import com.thoughtworks.go.server.ui.SortOrder;
import com.thoughtworks.go.server.util.Pagination;
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 org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import java.sql.SQLException;
import java.util.ArrayList;
import static com.thoughtworks.go.helper.JobInstanceMother.completed;
import static com.thoughtworks.go.helper.JobInstanceMother.scheduled;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.*;
import static org.mockito.MockitoAnnotations.initMocks;
public class JobInstanceServiceTest {
@Mock private JobInstanceDao jobInstanceDao;
@Mock private JobResultTopic topic;
@Mock private PropertiesService buildPropertiesService;
@Mock private StageDao stageDao;
@Mock private GoConfigService goConfigService;
@Mock private CruiseConfig cruiseConfig;
@Mock private SecurityService securityService;
@Mock private PluginManager pluginManager;
@Mock private ServerHealthService serverHealthService;
private JobInstance job;
private TestTransactionSynchronizationManager transactionSynchronizationManager;
private TransactionTemplate transactionTemplate;
private JobStatusCache jobStatusCache;
@Before
public void setUp() throws Exception {
initMocks(this);
job = JobInstanceMother.building("dev");
transactionSynchronizationManager = new TestTransactionSynchronizationManager();
transactionTemplate = new TestTransactionTemplate(transactionSynchronizationManager);
jobStatusCache = new JobStatusCache(stageDao);
}
@After
public void after() {
Mockito.verifyNoMoreInteractions(jobInstanceDao, stageDao, buildPropertiesService);
}
@Test
public void shouldNotifyListenerWhenJobStatusChanged() throws Exception {
final JobStatusListener listener = Mockito.mock(JobStatusListener.class);
JobInstanceService jobService = new JobInstanceService(jobInstanceDao, null, null, jobStatusCache, transactionTemplate, transactionSynchronizationManager, null, null, goConfigService,
null, pluginManager, serverHealthService, listener);
jobService.updateStateAndResult(job);
verify(jobInstanceDao).updateStateAndResult(job);
verify(listener).jobStatusChanged(job);
}
@Test
public void shouldNotifyAllListenersWhenUpdateJobStatus() throws Exception {
final JobStatusListener listener1 = Mockito.mock(JobStatusListener.class, "listener1");
final JobStatusListener listener2 = Mockito.mock(JobStatusListener.class, "listener2");
JobInstanceService jobService = new JobInstanceService(jobInstanceDao, null, null, jobStatusCache, transactionTemplate, transactionSynchronizationManager,
null, null, goConfigService, null, pluginManager, serverHealthService, listener1, listener2);
jobService.updateStateAndResult(job);
verify(jobInstanceDao).updateStateAndResult(job);
verify(listener1).jobStatusChanged(job);
verify(listener2).jobStatusChanged(job);
}
@Test
public void shouldNotifyListenerWhenUpdateAssignedInfo() throws Exception {
final JobStatusListener listener1 = Mockito.mock(JobStatusListener.class, "listener1");
final JobStatusListener listener2 = Mockito.mock(JobStatusListener.class, "listener2");
final JobInstanceService jobService = new JobInstanceService(jobInstanceDao, null, null, jobStatusCache, transactionTemplate, transactionSynchronizationManager,
null, null, goConfigService, null, pluginManager, serverHealthService, listener1, listener2);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
jobService.updateAssignedInfo(job);
}
});
verify(listener1).jobStatusChanged(job);
verify(listener2).jobStatusChanged(job);
verify(jobInstanceDao).updateAssignedInfo(job);
}
@Test
public void shouldNotifyAllListenersWhenSaveJob() throws Exception {
final JobStatusListener listener1 = Mockito.mock(JobStatusListener.class, "listener1");
final JobStatusListener listener2 = Mockito.mock(JobStatusListener.class, "listener2");
final Pipeline pipeline = new NullPipeline();
final Stage stage = new Stage();
stage.setId(1);
final JobInstanceService jobService = new JobInstanceService(jobInstanceDao, null, null, jobStatusCache, transactionTemplate, transactionSynchronizationManager,
null, null, goConfigService, null, pluginManager, serverHealthService, listener1, listener2);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
public void doInTransactionWithoutResult(TransactionStatus status) {
jobService.save(new StageIdentifier(pipeline.getName(), null, pipeline.getLabel(), stage.getName(), String.valueOf(stage.getCounter())), stage.getId(), job);
}
});
verify(jobInstanceDao).save(1l, job);
verify(listener1).jobStatusChanged(job);
verify(listener2).jobStatusChanged(job);
}
@Test
public void shouldIgnoreErrorsWhenNotifyingListenersDuringSave() throws Exception {
final JobStatusListener failingListener = Mockito.mock(JobStatusListener.class, "listener1");
final JobStatusListener passingListener = Mockito.mock(JobStatusListener.class, "listener2");
doThrow(new RuntimeException("Should not be rethrown by save")).when(failingListener).jobStatusChanged(job);
final Pipeline pipeline = new NullPipeline();
final Stage stage = new Stage();
stage.setId(1);
final JobInstanceService jobService = new JobInstanceService(jobInstanceDao, null, null, jobStatusCache, transactionTemplate, transactionSynchronizationManager,
null, null, goConfigService, null, pluginManager, serverHealthService, failingListener, passingListener);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
public void doInTransactionWithoutResult(TransactionStatus status) {
jobService.save(new StageIdentifier(pipeline.getName(), null, pipeline.getLabel(), stage.getName(), String.valueOf(stage.getCounter())), stage.getId(), job);
}
});
verify(jobInstanceDao).save(1l, job);
verify(passingListener).jobStatusChanged(job);
}
@Test
public void shouldNotifyListenersWhenAssignedJobIsCancelled() {
final JobInstanceService jobService = new JobInstanceService(jobInstanceDao, buildPropertiesService, topic, jobStatusCache, transactionTemplate, transactionSynchronizationManager,
null, null, goConfigService, null, pluginManager, serverHealthService);
job.setAgentUuid("dummy agent");
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
jobService.cancelJob(job);
}
});
verify(jobInstanceDao).updateStateAndResult(job);
verify(buildPropertiesService).saveCruiseProperties(job);
verify(topic).post(new JobResultMessage(job.getIdentifier(), JobResult.Cancelled, job.getAgentUuid()));
}
@Test
public void shouldNotNotifyListenersWhenScheduledJobIsCancelled() {
final JobInstance scheduledJob = scheduled("dev");
final JobInstanceService jobService = new JobInstanceService(jobInstanceDao, buildPropertiesService, topic, jobStatusCache, transactionTemplate, transactionSynchronizationManager, null,
null, goConfigService, null, pluginManager, serverHealthService);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
jobService.cancelJob(scheduledJob);
}
});
jobService.cancelJob(scheduledJob);
verify(jobInstanceDao).updateStateAndResult(scheduledJob);
verify(buildPropertiesService).saveCruiseProperties(scheduledJob);
verify(topic, never()).post(Matchers.<JobResultMessage>any());
}
@Test
public void shouldNotNotifyListenersWhenACompletedJobIsCancelled() {
final JobInstance completedJob = completed("dev");
final JobInstanceService jobService = new JobInstanceService(jobInstanceDao, buildPropertiesService, topic, jobStatusCache, transactionTemplate, transactionSynchronizationManager, null,
null, goConfigService, null, pluginManager, serverHealthService);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
jobService.cancelJob(completedJob);
}
});
jobService.cancelJob(completedJob);
verify(jobInstanceDao, never()).updateStateAndResult(completedJob);
verify(buildPropertiesService, never()).saveCruiseProperties(completedJob);
verify(topic, never()).post(Matchers.<JobResultMessage>any());
}
@Test
public void shouldNotNotifyListenersWhenAssignedJobCancellationTransactionRollsback() {
final JobInstanceService jobService = new JobInstanceService(jobInstanceDao, buildPropertiesService, topic, jobStatusCache, transactionTemplate, transactionSynchronizationManager,
null, null, goConfigService, null, pluginManager, serverHealthService);
job.setAgentUuid("dummy agent");
try {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
jobService.cancelJob(job);
throw new RuntimeException("to rollback txn");
}
});
} catch (RuntimeException e) {
//ignore
}
verify(jobInstanceDao).updateStateAndResult(job);
verify(buildPropertiesService).saveCruiseProperties(job);
verify(topic, never()).post(any(JobResultMessage.class));
}
@Test
public void shouldNotNotifyListenersWhenScheduledJobIsFailed() {
final JobInstance scheduledJob = scheduled("dev");
scheduledJob.setId(10);
final JobInstanceService jobService = new JobInstanceService(jobInstanceDao, buildPropertiesService, topic, jobStatusCache, transactionTemplate, transactionSynchronizationManager, null,
null, goConfigService, null, pluginManager, serverHealthService);
when(jobInstanceDao.buildByIdWithTransitions(scheduledJob.getId())).thenReturn(scheduledJob);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
JobInstance jobInstance = jobService.buildByIdWithTransitions(scheduledJob.getId());
jobService.failJob(jobInstance);
}
});
verify(jobInstanceDao).buildByIdWithTransitions(scheduledJob.getId());
verify(jobInstanceDao).updateStateAndResult(scheduledJob);
verify(buildPropertiesService).saveCruiseProperties(scheduledJob);
assertThat(scheduledJob.isFailed(), is(true));
}
@Test
public void shouldDelegateToDAO_getJobHistoryCount() {
final JobInstanceService jobService = new JobInstanceService(jobInstanceDao, buildPropertiesService, topic, jobStatusCache,
transactionTemplate, transactionSynchronizationManager, null, null, goConfigService, null, pluginManager, serverHealthService);
jobService.getJobHistoryCount("pipeline", "stage", "job");
verify(jobInstanceDao).getJobHistoryCount("pipeline", "stage", "job");
}
@Test
public void shouldDelegateToDAO_findJobHistoryPage() {
when(cruiseConfig.hasPipelineNamed(new CaseInsensitiveString("pipeline"))).thenReturn(true);
when(goConfigService.currentCruiseConfig()).thenReturn(cruiseConfig);
when(securityService.hasViewPermissionForPipeline(Username.valueOf("looser"), "pipeline")).thenReturn(true);
final JobInstanceService jobService = new JobInstanceService(jobInstanceDao, buildPropertiesService, topic, jobStatusCache,
transactionTemplate, transactionSynchronizationManager, null, null, goConfigService, securityService, pluginManager, serverHealthService);
Pagination pagination = Pagination.pageStartingAt(1, 1, 1);
jobService.findJobHistoryPage("pipeline", "stage", "job", pagination, "looser", new HttpOperationResult());
verify(jobInstanceDao).findJobHistoryPage("pipeline", "stage", "job", pagination.getPageSize(), pagination.getOffset());
}
@Test
public void shouldPopulateErrorWhenPipelineNotFound_findJobHistoryPage() {
when(cruiseConfig.hasPipelineNamed(new CaseInsensitiveString("pipeline"))).thenReturn(false);
when(goConfigService.currentCruiseConfig()).thenReturn(cruiseConfig);
when(securityService.hasViewPermissionForPipeline(Username.valueOf("looser"), "pipeline")).thenReturn(true);
final JobInstanceService jobService = new JobInstanceService(jobInstanceDao, buildPropertiesService, topic, jobStatusCache,
transactionTemplate, transactionSynchronizationManager, null, null, goConfigService, securityService, pluginManager, serverHealthService);
Pagination pagination = Pagination.pageStartingAt(1, 1, 1);
HttpOperationResult result = new HttpOperationResult();
JobInstances jobHistoryPage = jobService.findJobHistoryPage("pipeline", "stage", "job", pagination, "looser", result);
assertThat(jobHistoryPage, is(nullValue()));
assertThat(result.httpCode(), is(404));
}
@Test
public void shouldPopulateErrorWhenUnauthorized_findJobHistoryPage() {
when(cruiseConfig.hasPipelineNamed(new CaseInsensitiveString("pipeline"))).thenReturn(true);
when(goConfigService.currentCruiseConfig()).thenReturn(cruiseConfig);
when(securityService.hasViewPermissionForPipeline(Username.valueOf("looser"), "pipeline")).thenReturn(false);
final JobInstanceService jobService = new JobInstanceService(jobInstanceDao, buildPropertiesService, topic, jobStatusCache,
transactionTemplate, transactionSynchronizationManager, null, null, goConfigService, securityService, pluginManager, serverHealthService);
Pagination pagination = Pagination.pageStartingAt(1, 1, 1);
HttpOperationResult result = new HttpOperationResult();
JobInstances jobHistoryPage = jobService.findJobHistoryPage("pipeline", "stage", "job", pagination, "looser", result);
assertThat(jobHistoryPage, is(nullValue()));
assertThat(result.canContinue(), is(false));
}
@Test
public void shouldLoadOriginalJobPlan() {
JobResolverService resolver = mock(JobResolverService.class);
final JobInstanceService jobService = new JobInstanceService(jobInstanceDao, buildPropertiesService, topic, jobStatusCache, transactionTemplate, transactionSynchronizationManager,
resolver,
null, goConfigService, null, pluginManager, serverHealthService);
DefaultJobPlan expectedPlan = new DefaultJobPlan(new Resources(), new ArtifactPlans(), new ArtifactPropertiesGenerators(), 7, new JobIdentifier(), null, new EnvironmentVariablesConfig(), new EnvironmentVariablesConfig(), null);
when(jobInstanceDao.loadPlan(7l)).thenReturn(expectedPlan);
JobIdentifier givenId = new JobIdentifier("pipeline-name", 9, "label-9", "stage-name", "2", "job-name", 10l);
when(resolver.actualJobIdentifier(givenId)).thenReturn(new JobIdentifier("pipeline-name", 8, "label-8", "stage-name", "1", "job-name", 7l));
assertThat(jobService.loadOriginalJobPlan(givenId), sameInstance(expectedPlan));
verify(jobInstanceDao).loadPlan(7l);
}
@Test
public void shouldRegisterANewListener() throws SQLException {
JobInstanceService jobService = new JobInstanceService(jobInstanceDao, null, null, jobStatusCache, transactionTemplate, transactionSynchronizationManager, null, null, goConfigService,
null, pluginManager, serverHealthService);
JobStatusListener listener = mock(JobStatusListener.class);
jobService.registerJobStateChangeListener(listener);
jobService.updateStateAndResult(job);
verify(jobInstanceDao).updateStateAndResult(job);
verify(listener).jobStatusChanged(job);
}
@Test
public void shouldGetCompletedJobsOnAgentOnTheGivenPage() {
JobInstanceService jobService = new JobInstanceService(jobInstanceDao, null, null, jobStatusCache, transactionTemplate, transactionSynchronizationManager, null, null, goConfigService,
null, pluginManager, serverHealthService);
ArrayList<JobInstance> expected = new ArrayList<>();
when(jobInstanceDao.totalCompletedJobsOnAgent("uuid")).thenReturn(500);
when(jobInstanceDao.completedJobsOnAgent("uuid", JobInstanceService.JobHistoryColumns.pipeline, SortOrder.ASC, 50, 50)).thenReturn(expected);
JobInstancesModel actualModel = jobService.completedJobsOnAgent("uuid", JobInstanceService.JobHistoryColumns.pipeline, SortOrder.ASC, 2, 50);
assertThat(actualModel, is(new JobInstancesModel(new JobInstances(expected), Pagination.pageByNumber(2, 500, 50))));
verify(jobInstanceDao).totalCompletedJobsOnAgent("uuid");
verify(jobInstanceDao).completedJobsOnAgent("uuid", JobInstanceService.JobHistoryColumns.pipeline, SortOrder.ASC, 50, 50);
}
@Test
public void shouldRemoveJobRelatedServerHealthMessagesOnConfigChange(){
ServerHealthService serverHealthService = new ServerHealthService();
serverHealthService.update(ServerHealthState.error("message", "description", HealthStateType.general(HealthStateScope.forJob("p1", "s1", "j1"))));
serverHealthService.update(ServerHealthState.error("message", "description", HealthStateType.general(HealthStateScope.forJob("p2", "s2", "j2"))));
assertThat(serverHealthService.getAllLogs().errorCount(), is(2));
JobInstanceService jobService = new JobInstanceService(jobInstanceDao, null, null, jobStatusCache, transactionTemplate, transactionSynchronizationManager, null, null, goConfigService,
null, pluginManager, serverHealthService);
jobService.onConfigChange(new BasicCruiseConfig());
assertThat(serverHealthService.getAllLogs().errorCount(), is(0));
}
@Test
public void shouldRemoveJobRelatedServerHealthMessagesOnPipelineConfigChange(){
ServerHealthService serverHealthService = new ServerHealthService();
serverHealthService.update(ServerHealthState.error("message", "description", HealthStateType.general(HealthStateScope.forJob("p1", "s1", "j1"))));
serverHealthService.update(ServerHealthState.error("message", "description", HealthStateType.general(HealthStateScope.forJob("p2", "s2", "j2"))));
assertThat(serverHealthService.getAllLogs().errorCount(), is(2));
JobInstanceService jobService = new JobInstanceService(jobInstanceDao, null, null, jobStatusCache, transactionTemplate, transactionSynchronizationManager, null, null, goConfigService,
null, pluginManager, serverHealthService);
JobInstanceService.PipelineConfigChangedListener pipelineConfigChangedListener = jobService.new PipelineConfigChangedListener();
pipelineConfigChangedListener.onEntityConfigChange(PipelineConfigMother.pipelineConfig("p1", "s_new", new MaterialConfigs(), "j1"));
assertThat(serverHealthService.getAllLogs().errorCount(), is(1));
assertThat(serverHealthService.getAllLogs().get(0).getType().getScope().getScope(), is("p2/s2/j2"));
}
}