/* * This file is part of LibrePlan * * Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e * Desenvolvemento Tecnolóxico de Galicia * Copyright (C) 2010-2011 Igalia, S.L. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.libreplan.business.test.planner.entities; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.expectLastCall; import static org.easymock.EasyMock.getCurrentArguments; import static org.easymock.EasyMock.isA; import static org.easymock.EasyMock.createNiceMock; import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.resetToNice; import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.libreplan.business.BusinessGlobalNames.BUSINESS_SPRING_CONFIG_FILE; import static org.libreplan.business.test.BusinessGlobalNames.BUSINESS_SPRING_CONFIG_TEST_FILE; import static org.libreplan.business.test.planner.entities.DayAssignmentMatchers.haveHours; import static org.libreplan.business.workingday.EffortDuration.hours; import java.math.BigDecimal; import java.math.MathContext; import java.util.Collections; import java.util.Date; import javax.annotation.Resource; import org.easymock.IAnswer; import org.joda.time.LocalDate; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.libreplan.business.IDataBootstrap; import org.libreplan.business.calendars.entities.AvailabilityTimeLine; import org.libreplan.business.calendars.entities.BaseCalendar; import org.libreplan.business.calendars.entities.ResourceCalendar; import org.libreplan.business.orders.entities.HoursGroup; import org.libreplan.business.orders.entities.Order; import org.libreplan.business.orders.entities.OrderLine; import org.libreplan.business.orders.entities.SchedulingDataForVersion; import org.libreplan.business.orders.entities.SumChargedEffort; import org.libreplan.business.orders.entities.TaskSource; import org.libreplan.business.planner.entities.AggregateOfDayAssignments; import org.libreplan.business.planner.entities.DayAssignment.FilterType; import org.libreplan.business.planner.entities.Dependency; import org.libreplan.business.planner.entities.Dependency.Type; import org.libreplan.business.planner.entities.SpecificResourceAllocation; import org.libreplan.business.planner.entities.Task; import org.libreplan.business.planner.entities.TaskDeadlineViolationStatusEnum; import org.libreplan.business.planner.entities.TaskElement; import org.libreplan.business.planner.entities.TaskGroup; import org.libreplan.business.planner.entities.TaskStatusEnum; import org.libreplan.business.planner.limiting.entities.LimitingResourceQueueElement; import org.libreplan.business.resources.entities.Worker; import org.libreplan.business.scenarios.entities.OrderVersion; import org.libreplan.business.workingday.EffortDuration; import org.libreplan.business.workingday.IntraDayDate; import org.libreplan.business.workingday.IntraDayDate.PartialDay; import org.libreplan.business.workingday.ResourcesPerDay; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; /** * @author Óscar González Fernández <ogonzalez@igalia.com> */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { BUSINESS_SPRING_CONFIG_FILE, BUSINESS_SPRING_CONFIG_TEST_FILE }) public class TaskTest { private static final OrderVersion mockedOrderVersion = mockOrderVersion(); public static OrderVersion mockOrderVersion() { OrderVersion result = createNiceMock(OrderVersion.class); replay(result); return result; } private BaseCalendar taskCalendar; private Task task; private HoursGroup hoursGroup; private ResourceCalendar workerCalendar; private Worker worker; @Resource private IDataBootstrap defaultAdvanceTypesBootstrapListener; @Before public void loadRequiredData() { // Load data defaultAdvanceTypesBootstrapListener.loadRequiredData(); // Create basic data hoursGroup = new HoursGroup(); hoursGroup.setWorkingHours(3); Order order = new Order(); order.useSchedulingDataFor(mockedOrderVersion); order.setInitDate(new Date()); OrderLine orderLine = OrderLine.create(); order.add(orderLine); order.setCalendar(stubCalendar()); SchedulingDataForVersion version = TaskElementTest.mockSchedulingDataForVersion(orderLine); TaskSource taskSource = TaskSource.create(version, Collections.singletonList(hoursGroup)); task = Task.createTask(taskSource); } private BaseCalendar stubCalendar() { taskCalendar = createNiceMock(BaseCalendar.class); expect(taskCalendar.getCapacityOn(isA(PartialDay.class))).andReturn(hours(8)).anyTimes(); expect(taskCalendar.getAvailability()).andReturn(AvailabilityTimeLine.allValid()).anyTimes(); replay(taskCalendar); return taskCalendar; } @Test @Transactional public void taskIsASubclassOfTaskElement() { assertTrue(task instanceof TaskElement); } @Test @Transactional public void taskHasHoursSpecifiedAtOrderComingFromItsHoursGroup() { assertThat(task.getHoursSpecifiedAtOrder(), equalTo(hoursGroup.getWorkingHours())); } @Test @Transactional public void taskMustHaveOneHoursGroup() { HoursGroup hoursGroup = task.getHoursGroup(); assertNotNull(hoursGroup); } public static Task createValidTask() { HoursGroup hoursGroup = new HoursGroup(); hoursGroup.setWorkingHours(3); OrderLine orderLine = OrderLine.create(); Order order = new Order(); order.useSchedulingDataFor(mockedOrderVersion); order.setInitDate(new Date()); order.add(orderLine); SchedulingDataForVersion version = TaskElementTest.mockSchedulingDataForVersion(orderLine); TaskSource taskSource = TaskSource.create(version, Collections.singletonList(hoursGroup)); return Task.createTask(taskSource); } public static Task createValidTaskWithFullProgress(){ Task task = createValidTask(); task.setAdvancePercentage(BigDecimal.ONE); return task; } @Test @Transactional public void getResourceAllocationsDoesNotRetrieveUnsatisfiedAllocations() { assertThat(task.getSatisfiedResourceAllocations().size(), equalTo(0)); SpecificResourceAllocation unsatisfied = SpecificResourceAllocation.create(task); assertTrue("in order to be meaningful this test needs an unsatisfied " + "allocation", unsatisfied.isUnsatisfied()); task.addResourceAllocation(unsatisfied); assertThat(task.getSatisfiedResourceAllocations().size(), equalTo(0)); assertThat(task.getAllResourceAllocations().size(), equalTo(1)); } @Test @Transactional public void addingNoEmptyResourceAllocationAddsIt() { assertThat(task.getSatisfiedResourceAllocations().size(), equalTo(0)); SpecificResourceAllocation resourceAllocation = stubResourceAllocationWithAssignedHours(task, 500); task.addResourceAllocation(resourceAllocation); assertThat(task.getSatisfiedResourceAllocations().size(), equalTo(1)); } @Test @Transactional public void taskRemoveResourceAllocation() { assertThat(task.getSatisfiedResourceAllocations().size(), equalTo(0)); SpecificResourceAllocation resourceAllocation = stubResourceAllocationWithAssignedHours(task, 500); task.addResourceAllocation(resourceAllocation); assertThat(task.getSatisfiedResourceAllocations().size(), equalTo(1)); task.removeResourceAllocation(resourceAllocation); assertThat(task.getSatisfiedResourceAllocations().size(), equalTo(0)); } @Test @Transactional public void aTaskWithoutAllocationsHasZeroAssignedHours() { assertThat(task.getAssignedHours(), equalTo(0)); } @Test @Transactional public void aTaskWithAllocationsReturnsTheSumOfItsAllocations() { task.addResourceAllocation(stubResourceAllocationWithAssignedHours(task, 5)); task.addResourceAllocation(stubResourceAllocationWithAssignedHours(task, 3)); assertThat(task.getAssignedHours(), equalTo(8)); } @Test @Transactional public void theWorkableDaysAreCalculatedBasedOnlyOnDatesNotHours() { task.setIntraDayStartDate(IntraDayDate.create(new LocalDate(2010, 1, 13), EffortDuration.hours(3))); task.setIntraDayEndDate(IntraDayDate.startOfDay(new LocalDate(2010, 1, 14))); assertThat(task.getWorkableDays(), equalTo(1)); } @Test @Transactional public void atLeastOneWorkableDayEvenIfStartAndEndDatesAreAtTheSameDay() { LocalDate day = new LocalDate(2010, 1, 13); task.setIntraDayStartDate(IntraDayDate.create(day, EffortDuration.hours(3))); task.setIntraDayEndDate(IntraDayDate.create(day, EffortDuration.hours(4))); assertThat(task.getWorkableDays(), equalTo(1)); } @Test @Transactional public void ifTheEndIsInTheMiddleOfADayTheWholeDayIsCounted() { LocalDate start = new LocalDate(2010, 1, 13); task.setIntraDayStartDate(IntraDayDate.create(start, EffortDuration.hours(3))); task.setIntraDayEndDate(IntraDayDate.create(start.plusDays(1), EffortDuration.minutes(1))); assertThat(task.getWorkableDays(), equalTo(2)); } @Test @Transactional public void ifSomeDayIsNotWorkableIsNotCounted() { final LocalDate start = new LocalDate(2010, 1, 13); resetToNice(taskCalendar); expect(taskCalendar.getCapacityOn(isA(PartialDay.class))) .andAnswer(new IAnswer<EffortDuration>() { @Override public EffortDuration answer() throws Throwable { Object[] args = getCurrentArguments(); PartialDay day = (PartialDay) args[0]; return day.getDate().equals(start.plusDays(1)) ? hours(0) : hours(8); } }) .anyTimes(); replay(taskCalendar); task.setIntraDayStartDate(IntraDayDate.create(start, EffortDuration.hours(3))); task.setIntraDayEndDate(IntraDayDate.create(start.plusDays(1), EffortDuration.minutes(1))); assertThat(task.getWorkableDays(), equalTo(1)); } /** * @param task * @param hours * @return {@link SpecificResourceAllocation} */ private SpecificResourceAllocation stubResourceAllocationWithAssignedHours(Task task, int hours) { SpecificResourceAllocation resourceAllocation = createNiceMock(SpecificResourceAllocation.class); expect(resourceAllocation.getAssignedHours()).andReturn(hours).anyTimes(); expect(resourceAllocation.getTask()).andReturn(task).anyTimes(); expect(resourceAllocation.hasAssignments()).andReturn(true).anyTimes(); expect(resourceAllocation.isSatisfied()).andReturn(true).anyTimes(); resourceAllocation.detach(); expectLastCall().anyTimes(); replay(resourceAllocation); return resourceAllocation; } @Test @Transactional public void testIsLimiting() { LimitingResourceQueueElement element = LimitingResourceQueueElement.create(); Task task = createValidTask(); SpecificResourceAllocation resourceAllocation = SpecificResourceAllocation.create(task); resourceAllocation.setLimitingResourceQueueElement(element); task.addResourceAllocation(resourceAllocation); assertTrue(task.getLimitingResourceAllocations().size() == 1); } @Test @Transactional public void testIsNonLimiting() { Task task = createValidTask(); SpecificResourceAllocation resourceAllocation = SpecificResourceAllocation.create(task); task.addResourceAllocation(resourceAllocation); assertTrue(task.getNonLimitingResourceAllocations().size() == 1); } @Test @Transactional public void theoreticalHoursIsZeroIfNoResourcesAreAllocated() { assertThat(task.getTheoreticalCompletedTimeUntilDate(new Date()), equalTo(EffortDuration.zero())); } @Test @Transactional public void theoreticalHoursIsTotalIfDateIsLaterThanEndDate() { prepareTaskForTheoreticalAdvanceTesting(); EffortDuration totalAllocatedTime = AggregateOfDayAssignments.create(task.getDayAssignments(FilterType.KEEP_ALL)).getTotalTime(); assertThat(task.getTheoreticalCompletedTimeUntilDate(task.getEndDate()), equalTo(totalAllocatedTime)); } @Test @Transactional public void theoreticalHoursIsZeroIfDateIsEarlierThanStartDate() { prepareTaskForTheoreticalAdvanceTesting(); assertThat(task.getTheoreticalCompletedTimeUntilDate(task.getStartDate()), equalTo(EffortDuration.zero())); } @Test @Transactional public void theoreticalHoursWithADateWithinStartAndEndDateHead() { prepareTaskForTheoreticalAdvanceTesting(); LocalDate limit = task.getStartAsLocalDate().plusDays(1); EffortDuration expected = EffortDuration.hours(8); assertThat(task.getTheoreticalCompletedTimeUntilDate(limit.toDateTimeAtStartOfDay().toDate()), equalTo(expected)); } @Test @Transactional public void theoreticalHoursWithADateWithinStartAndEndDateTail() { prepareTaskForTheoreticalAdvanceTesting(); LocalDate limit = task.getEndAsLocalDate().minusDays(1); EffortDuration expected = EffortDuration.hours(32); assertThat(task.getTheoreticalCompletedTimeUntilDate(limit.toDateTimeAtStartOfDay().toDate()), equalTo(expected)); } @Test @Transactional public void theoreticalAdvancePercentageIsZeroIfNoResourcesAreAllocated() { assertThat(task.getTheoreticalAdvancePercentageUntilDate(new Date()), equalTo(new BigDecimal(0))); } @Test @Transactional public void theoreticalPercentageIsOneIfDateIsLaterThanEndDate() { prepareTaskForTheoreticalAdvanceTesting(); assertThat(task.getTheoreticalAdvancePercentageUntilDate(task.getEndDate()), equalTo(new BigDecimal("1.00000000"))); } @Test @Transactional public void theoreticalPercentageWithADateWithinStartAndEndDateHead() { prepareTaskForTheoreticalAdvanceTesting(); LocalDate limit = task.getStartAsLocalDate().plusDays(1); assertThat(task.getTheoreticalAdvancePercentageUntilDate(limit.toDateTimeAtStartOfDay().toDate()), equalTo(new BigDecimal("0.20000000"))); } @Test @Transactional public void taskIsFinishedIfAdvancePercentageIsOne() { task.setAdvancePercentage(BigDecimal.ONE); assertTrue(task.isFinished()); assertFalse(task.isInProgress()); assertTrue(task.getTaskStatus() == TaskStatusEnum.FINISHED); } @Test @Transactional public void taskIsProgressIfAdvancePercentageIsLessThanOne() { task.setAdvancePercentage(new BigDecimal("0.9999", new MathContext(4))); assertFalse(task.isFinished()); assertTrue(task.isInProgress()); assertTrue(task.getTaskStatus() == TaskStatusEnum.IN_PROGRESS); } @Test @Transactional public void taskIsProgressIfAdvancePercentageIsGreaterThanZero() { task.setAdvancePercentage(new BigDecimal("0.0001", new MathContext(4))); assertFalse(task.isFinished()); assertTrue(task.isInProgress()); assertTrue(task.getTaskStatus() == TaskStatusEnum.IN_PROGRESS); } @Test @Transactional public void taskIsNotInProgressIfAdvancePercentageIsZeroAndNoWorkReportsAttached() { task.setAdvancePercentage(BigDecimal.ZERO); SumChargedEffort sumChargedEffort = task.getOrderElement().getSumChargedEffort(); assertTrue(sumChargedEffort == null || sumChargedEffort.isZero()); assertFalse(task.isFinished()); assertFalse(task.isInProgress()); } @Test @Transactional public void taskIsInProgressIfAdvancePercentageIsZeroButWorkReportsAttached() { SumChargedEffort sumChargedEffort = SumChargedEffort.create(task.getOrderElement()); sumChargedEffort.addDirectChargedEffort(EffortDuration.hours(1)); task.getOrderElement().setSumChargedEffort(sumChargedEffort); assertFalse(task.isFinished()); assertTrue(task.isInProgress()); assertTrue(task.getTaskStatus() == TaskStatusEnum.IN_PROGRESS); } @Test @Transactional public void taskIsReadyToStartIfAllEndStartDepsAreFinished() { Dependency dependency = mockDependency(Type.END_START); dependency.getOrigin().setAdvancePercentage(BigDecimal.ONE); assertFalse(task.isFinished()); assertFalse(task.isInProgress()); assertTrue(task.getTaskStatus() == TaskStatusEnum.READY_TO_START); } @Test @Transactional public void taskIsReadyToStartIfAllStartStartDepsAreInProgressOrFinished() { Dependency dependency1 = mockDependency(Type.START_START); dependency1.getOrigin().setAdvancePercentage(BigDecimal.ONE); Dependency dependency2 = mockDependency(Type.START_START); dependency2.getOrigin().setAdvancePercentage(new BigDecimal("0.0001", new MathContext(4))); assertFalse(task.isFinished()); assertFalse(task.isInProgress()); assertTrue(task.getTaskStatus() == TaskStatusEnum.READY_TO_START); } @Test @Transactional public void taskIsBlockedIfHasAnUnfinishedEndStartDependency() { Dependency dependency = mockDependency(Type.END_START); dependency.getOrigin().setAdvancePercentage(new BigDecimal("0.0001", new MathContext(4))); assertFalse(task.isFinished()); assertFalse(task.isInProgress()); assertTrue(task.getTaskStatus() == TaskStatusEnum.BLOCKED); } @Test @Transactional public void taskIsBlockedIfHasANotStartedStartStartDependency() { Dependency dependency = mockDependency(Type.START_START); dependency.getOrigin().setAdvancePercentage(BigDecimal.ZERO); assertFalse(task.isFinished()); assertFalse(task.isInProgress()); assertTrue(task.getTaskStatus() == TaskStatusEnum.BLOCKED); } @Test @Transactional public void taskStatusCalculationTakesIntoAccountDifferentDepType() { Dependency dependency1 = mockDependency(Type.END_START); dependency1.getOrigin().setAdvancePercentage(BigDecimal.ONE); Dependency dependency2 = mockDependency(Type.START_START); dependency2.getOrigin().setAdvancePercentage(new BigDecimal("0.0001", new MathContext(4))); assertFalse(task.isFinished()); assertFalse(task.isInProgress()); assertTrue(task.getTaskStatus() == TaskStatusEnum.READY_TO_START); dependency2.getOrigin().setAdvancePercentage(BigDecimal.ZERO); assertTrue(task.getTaskStatus() == TaskStatusEnum.BLOCKED); } @Test @Transactional public void taskIsBlockedIfHasAnUnfinishedEndStartDependencyUsingGroup() { Task task1 = createValidTaskWithFullProgress(); Task task2 = createValidTask(); task2.setAdvancePercentage(new BigDecimal("0.0001", new MathContext(4))); TaskGroup taskGroup = new TaskGroup(); taskGroup.addTaskElement(task1); taskGroup.addTaskElement(task2); mockDependency(taskGroup, this.task, Type.END_START); assertFalse(task.isFinished()); assertFalse(task.isInProgress()); assertTrue(task.getTaskStatus() == TaskStatusEnum.BLOCKED); } @Test @Transactional public void taskDependenciesDontMatterIfProgressIsNotZero() { Task task1 = createValidTaskWithFullProgress(); Task task2 = createValidTask(); task2.setAdvancePercentage(new BigDecimal("0.0001", new MathContext(4))); TaskGroup taskGroup = new TaskGroup(); taskGroup.addTaskElement(task1); taskGroup.addTaskElement(task2); mockDependency(taskGroup, this.task, Type.END_START); task.setAdvancePercentage(new BigDecimal("0.0001", new MathContext(4))); assertFalse(task.isFinished()); assertTrue(task.getTaskStatus() == TaskStatusEnum.IN_PROGRESS); task.setAdvancePercentage(BigDecimal.ONE); assertTrue(task.getTaskStatus() == TaskStatusEnum.FINISHED); } @Test @Transactional public void taskStatusNotAffectedByEndEndDeps() { Dependency dependency = mockDependency(Type.END_END); dependency.getOrigin().setAdvancePercentage(BigDecimal.ZERO); assertTrue(task.getTaskStatus() == TaskStatusEnum.READY_TO_START); } @Test @Transactional public void taskWithNoDeadlineHasCorrectDeadlineViolationStatus() { task.setDeadline(null); assertTrue(task.getDeadlineViolationStatus() == TaskDeadlineViolationStatusEnum.NO_DEADLINE); } @Test @Transactional public void taskWithViolatedDeadlineHasCorrectDeadlineViolationStatus() { task.setDeadline(new LocalDate()); LocalDate tomorrow = new LocalDate().plusDays(1); task.setEndDate(tomorrow.toDateTimeAtStartOfDay().toDate()); assertTrue(task.getDeadlineViolationStatus() == TaskDeadlineViolationStatusEnum.DEADLINE_VIOLATED); } @Test @Transactional public void taskWithUnviolatedDeadlineHasCorrectDeadlineViolationStatusJustInTime() { LocalDate now = new LocalDate(); task.setDeadline(now); task.setEndDate(now.toDateTimeAtStartOfDay().toDate()); assertTrue(task.getDeadlineViolationStatus() == TaskDeadlineViolationStatusEnum.ON_SCHEDULE); } @Test @Transactional public void taskWithUnviolatedDeadlineHasCorrectDeadlineViolationStatusMargin() { LocalDate now = new LocalDate(); task.setDeadline(now); task.setEndDate(now.minusDays(1).toDateTimeAtStartOfDay().toDate()); assertTrue(task.getDeadlineViolationStatus() == TaskDeadlineViolationStatusEnum.ON_SCHEDULE); } private void prepareTaskForTheoreticalAdvanceTesting() { task.getHoursGroup().setWorkingHours(40); assertThat(task.getTotalHours(), equalTo(40)); task.setEndDate(task.getStartAsLocalDate().plusDays(5).toDateTimeAtStartOfDay().toDate()); SpecificResourceAllocation resourceAllocation = SpecificResourceAllocation.create(task); givenWorker(8); resourceAllocation.setResource(this.worker); assertTrue(resourceAllocation.getResource() != null); resourceAllocation.allocate(ResourcesPerDay.amount(1)); assertThat(resourceAllocation.getAssignments().size(), equalTo(5)); assertThat(resourceAllocation.getAssignments(), haveHours(8, 8, 8, 8, 8)); assertThat(task.getAssignedHours(), equalTo(0)); task.addResourceAllocation(resourceAllocation); assertTrue(task.getNonLimitingResourceAllocations().size() == 1); assertThat(task.getAssignedHours(), equalTo(40)); assertTrue(task.getDayAssignments(FilterType.KEEP_ALL).size() == 5); } private void givenWorker(int hoursPerDay) { this.worker = createNiceMock(Worker.class); givenResourceCalendarAlwaysReturning(hoursPerDay); expect(this.worker.getCalendar()).andReturn(this.workerCalendar).anyTimes(); replay(this.worker); } private void givenResourceCalendarAlwaysReturning(final int hours) { this.workerCalendar = SpecificResourceAllocationTest.createResourceCalendarAlwaysReturning(hours); } private Dependency mockDependency(Type type){ return mockDependency(createValidTask(), this.task, type); } private Dependency mockDependency(TaskElement origin, TaskElement destination, Type type) { Dependency dependency = createNiceMock(Dependency.class); expect(dependency.getOrigin()).andReturn(origin).anyTimes(); expect(dependency.getDestination()).andReturn(destination).anyTimes(); expect(dependency.getType()).andReturn(type).anyTimes(); replay(dependency); origin.add(dependency); destination.add(dependency); return dependency; } }