flight-crew-scheduling-java / src /test /java /org /acme /flighcrewscheduling /solver /FlightCrewSchedulingConstraintProviderTest.java
blackopsrepl's picture
.
f568829
package org.acme.flighcrewscheduling.solver;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import jakarta.inject.Inject;
import ai.timefold.solver.test.api.score.stream.ConstraintVerifier;
import org.acme.flighcrewscheduling.domain.Airport;
import org.acme.flighcrewscheduling.domain.Employee;
import org.acme.flighcrewscheduling.domain.Flight;
import org.acme.flighcrewscheduling.domain.FlightAssignment;
import org.acme.flighcrewscheduling.domain.FlightCrewSchedule;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
class FlightCrewSchedulingConstraintProviderTest {
private final ConstraintVerifier<FlightCrewSchedulingConstraintProvider, FlightCrewSchedule> constraintVerifier;
@Inject
public FlightCrewSchedulingConstraintProviderTest(
ConstraintVerifier<FlightCrewSchedulingConstraintProvider, FlightCrewSchedule> constraintVerifier) {
this.constraintVerifier = constraintVerifier;
}
@Test
void requiredSkill() {
FlightAssignment assignment = new FlightAssignment("1", null, 0, "1");
Employee employee = new Employee("1");
employee.setSkills(List.of("2"));
assignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::requiredSkill)
.given(assignment)
.penalizesBy(1); // missing requiredSkill
}
@Test
void flightConflict() {
Employee employee = new Employee("1");
Flight flight = new Flight("1", null, LocalDateTime.now(), null, LocalDateTime.now().plusMinutes(10));
FlightAssignment assignment = new FlightAssignment("1", flight);
assignment.setEmployee(employee);
Flight overlappingFlight =
new Flight("1", null, LocalDateTime.now().plusMinutes(1), null, LocalDateTime.now().plusMinutes(11));
FlightAssignment overlappingAssignment = new FlightAssignment("2", overlappingFlight);
overlappingAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::flightConflict)
.given(assignment, overlappingAssignment)
.penalizesBy(1); // one overlapping thirdFlight
}
@Test
void transferBetweenTwoFlights() {
Employee employee = new Employee("1");
Airport firstAirport = new Airport("1");
Airport secondAirport = new Airport("2");
Flight firstFlight =
new Flight("1", firstAirport, LocalDateTime.now(), secondAirport, LocalDateTime.now().plusMinutes(10));
FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight);
firstAssignment.setEmployee(employee);
Flight firstInvalidFlight =
new Flight("2", firstAirport, LocalDateTime.now().plusMinutes(11), secondAirport,
LocalDateTime.now().plusMinutes(12));
FlightAssignment firstInvalidAssignment = new FlightAssignment("2", firstInvalidFlight);
firstInvalidAssignment.setEmployee(employee);
Flight secondInvalidFlight =
new Flight("3", firstAirport, LocalDateTime.now().plusMinutes(13), secondAirport,
LocalDateTime.now().plusMinutes(14));
FlightAssignment secondInvalidAssignment = new FlightAssignment("3", secondInvalidFlight);
secondInvalidAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::transferBetweenTwoFlights)
.given(firstAssignment, firstInvalidAssignment, secondInvalidAssignment)
.penalizesBy(2); // two invalid connections
}
@Test
void employeeUnavailability() {
var date = LocalDate.now();
var employee = new Employee("1");
employee.setUnavailableDays(List.of(date));
var flight =
new Flight("1", null, date.atStartOfDay(), null, date.atStartOfDay().plusMinutes(10));
var assignment = new FlightAssignment("1", flight);
assignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::employeeUnavailability)
.given(assignment)
.penalizesBy(1); // unavailable at departure
flight.setDepartureUTCDateTime(date.minusDays(1).atStartOfDay());
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::employeeUnavailability)
.given(assignment)
.penalizesBy(1); // unavailable during flight
flight.setDepartureUTCDateTime(date.plusDays(1).atStartOfDay());
flight.setArrivalUTCDateTime(date.plusDays(1).atStartOfDay().plusMinutes(10));
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::employeeUnavailability)
.given(assignment)
.penalizesBy(0); // employee available
}
@Test
void firstAssignmentNotDepartingFromHome() {
Employee employee = new Employee("1");
employee.setHomeAirport(new Airport("1"));
employee.setUnavailableDays(List.of(LocalDate.now()));
Flight flight =
new Flight("1", new Airport("2"), LocalDateTime.now(), new Airport("3"),
LocalDateTime.now().plusMinutes(10));
FlightAssignment assignment = new FlightAssignment("1", flight);
assignment.setEmployee(employee);
Flight secondFlight =
new Flight("2", new Airport("2"), LocalDateTime.now().plusMinutes(1), new Airport("3"),
LocalDateTime.now().plusMinutes(10));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
Flight thirdFlight =
new Flight("3", new Airport("2"), LocalDateTime.now().plusMinutes(1), new Airport("3"),
LocalDateTime.now().plusMinutes(10));
FlightAssignment thirdAssignment = new FlightAssignment("3", thirdFlight);
thirdAssignment.setEmployee(employee);
Employee secondEmployee = new Employee("2");
secondEmployee.setHomeAirport(new Airport("3"));
secondEmployee.setUnavailableDays(List.of(LocalDate.now()));
Flight fourthFlight =
new Flight("4", new Airport("3"), LocalDateTime.now(), new Airport("4"),
LocalDateTime.now().plusMinutes(10));
FlightAssignment fourthAssignment = new FlightAssignment("4", fourthFlight);
fourthAssignment.setEmployee(secondEmployee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::firstAssignmentNotDepartingFromHome)
.given(employee, secondEmployee, assignment, secondAssignment, thirdAssignment, fourthAssignment)
.penalizesBy(1); // invalid first airport
}
@Test
void lastAssignmentNotArrivingAtHome() {
Employee employee = new Employee("1");
employee.setHomeAirport(new Airport("1"));
employee.setUnavailableDays(List.of(LocalDate.now()));
Flight firstFlight =
new Flight("1", new Airport("2"), LocalDateTime.now(), new Airport("3"),
LocalDateTime.now().plusMinutes(10));
FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight);
firstAssignment.setEmployee(employee);
Flight secondFlight =
new Flight("2", new Airport("3"), LocalDateTime.now().plusMinutes(11), new Airport("4"),
LocalDateTime.now().plusMinutes(12));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
Employee secondEmployee = new Employee("2");
secondEmployee.setHomeAirport(new Airport("2"));
secondEmployee.setUnavailableDays(List.of(LocalDate.now()));
Flight thirdFlight =
new Flight("3", new Airport("2"), LocalDateTime.now(), new Airport("3"),
LocalDateTime.now().plusMinutes(10));
FlightAssignment thirdFlightAssignment = new FlightAssignment("3", thirdFlight);
thirdFlightAssignment.setEmployee(secondEmployee);
Flight fourthFlight =
new Flight("4", new Airport("3"), LocalDateTime.now().plusMinutes(11), new Airport("2"),
LocalDateTime.now().plusMinutes(12));
FlightAssignment fourthFlightAssignment = new FlightAssignment("4", fourthFlight);
fourthFlightAssignment.setEmployee(secondEmployee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::lastAssignmentNotArrivingAtHome)
.given(employee, secondEmployee, firstAssignment, secondAssignment, thirdFlightAssignment,
fourthFlightAssignment)
.penalizesBy(1); // invalid last airport
}
@Test
void minimumRestAtHomeBase_satisfied() {
Employee employee = new Employee("1");
Airport homeAirport = new Airport("LHR");
employee.setHomeAirport(homeAirport);
LocalDateTime now = LocalDateTime.now();
// First flight: 8 hours duration, FDP = 8h + 65min = ~8.08h
// Required rest at home base = max(8.08, 12) = 12 hours
Flight firstFlight = new Flight("1", new Airport("LHR"), now,
new Airport("JFK"), now.plusHours(8));
FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight);
firstAssignment.setEmployee(employee);
// Second flight departs from home base 13 hours after first flight duty end
// Duty end of first = arrival + 20 min = now + 8h + 20min
// Second flight duty start = departure - 45 min
// Gap = 13 hours (satisfies 12 hour minimum)
LocalDateTime secondDeparture = now.plusHours(8).plusMinutes(20).plusHours(13).plusMinutes(45);
Flight secondFlight = new Flight("2", homeAirport, secondDeparture,
new Airport("BRU"), secondDeparture.plusHours(1));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAtHomeBase)
.given(firstAssignment, secondAssignment)
.penalizesBy(0); // 13 hours rest satisfies 12 hour minimum
}
@Test
void minimumRestAtHomeBase_violated() {
Employee employee = new Employee("1");
Airport homeAirport = new Airport("LHR");
employee.setHomeAirport(homeAirport);
LocalDateTime now = LocalDateTime.now();
// First flight: 8 hours duration
Flight firstFlight = new Flight("1", new Airport("LHR"), now,
new Airport("JFK"), now.plusHours(8));
FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight);
firstAssignment.setEmployee(employee);
// Second flight departs from home base only 10 hours after first flight duty end
// Violates 12 hour minimum by 2 hours = 120 minutes
LocalDateTime secondDeparture = now.plusHours(8).plusMinutes(20).plusHours(10).plusMinutes(45);
Flight secondFlight = new Flight("2", homeAirport, secondDeparture,
new Airport("BRU"), secondDeparture.plusHours(1));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAtHomeBase)
.given(firstAssignment, secondAssignment)
.penalizesBy(120); // 2 hours = 120 minutes violation
}
@Test
void minimumRestAtHomeBase_notApplicableAwayFromHome() {
Employee employee = new Employee("1");
Airport homeAirport = new Airport("LHR");
employee.setHomeAirport(homeAirport);
LocalDateTime now = LocalDateTime.now();
Flight firstFlight = new Flight("1", new Airport("LHR"), now,
new Airport("JFK"), now.plusHours(8));
FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight);
firstAssignment.setEmployee(employee);
// Second flight does NOT depart from home base - constraint should not apply
LocalDateTime secondDeparture = now.plusHours(8).plusMinutes(20).plusHours(5).plusMinutes(45);
Flight secondFlight = new Flight("2", new Airport("JFK"), secondDeparture,
new Airport("BRU"), secondDeparture.plusHours(1));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAtHomeBase)
.given(firstAssignment, secondAssignment)
.penalizesBy(0); // constraint doesn't apply when not at home base
}
@Test
void minimumRestAwayFromHomeBase_satisfied() {
Employee employee = new Employee("1");
Airport homeAirport = new Airport("LHR");
employee.setHomeAirport(homeAirport);
Airport jfk = new Airport("JFK");
Airport atl = new Airport("ATL");
LocalDateTime now = LocalDateTime.now();
// First flight ends at JFK
Flight firstFlight = new Flight("1", new Airport("LHR"), now,
jfk, now.plusHours(8));
FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight);
firstAssignment.setEmployee(employee);
// Second flight departs from ATL (away from home)
// Required rest = max(8.08, 10) = 10 hours (no taxi time adjustment for test simplicity)
// Provide 11 hours rest
LocalDateTime secondDeparture = now.plusHours(8).plusMinutes(20).plusHours(11).plusMinutes(45);
Flight secondFlight = new Flight("2", atl, secondDeparture,
new Airport("BRU"), secondDeparture.plusHours(2));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAwayFromHomeBase)
.given(firstAssignment, secondAssignment)
.penalizesBy(0); // 11 hours satisfies 10 hour minimum
}
@Test
void minimumRestAwayFromHomeBase_violated() {
Employee employee = new Employee("1");
Airport homeAirport = new Airport("LHR");
employee.setHomeAirport(homeAirport);
Airport jfk = new Airport("JFK");
Airport atl = new Airport("ATL");
LocalDateTime now = LocalDateTime.now();
Flight firstFlight = new Flight("1", new Airport("LHR"), now,
jfk, now.plusHours(8));
FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight);
firstAssignment.setEmployee(employee);
// Second flight away from home with only 8 hours rest
// Violates 10 hour minimum by 2 hours = 120 minutes
LocalDateTime secondDeparture = now.plusHours(8).plusMinutes(20).plusHours(8).plusMinutes(45);
Flight secondFlight = new Flight("2", atl, secondDeparture,
new Airport("BRU"), secondDeparture.plusHours(2));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAwayFromHomeBase)
.given(firstAssignment, secondAssignment)
.penalizesBy(120); // 2 hours = 120 minutes violation
}
@Test
void minimumRestAwayFromHomeBase_withTravelTimeAdjustment() {
Employee employee = new Employee("1");
Airport homeAirport = new Airport("LHR");
employee.setHomeAirport(homeAirport);
Airport jfk = new Airport("JFK");
jfk.setTaxiTimeInMinutes(java.util.Map.of("ATL", 180L)); // 3 hours taxi time
Airport atl = new Airport("ATL");
LocalDateTime now = LocalDateTime.now();
Flight firstFlight = new Flight("1", new Airport("LHR"), now,
jfk, now.plusHours(8));
FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight);
firstAssignment.setEmployee(employee);
// Taxi time = 180 minutes, excess = 180 - 30 = 150 minutes
// Travel adjustment = 150 * 2 / 60 = 5 hours
// Required rest = max(8.08, 10) + 5 = 15 hours
// Provide only 12 hours rest - violates by 3 hours = 180 minutes
LocalDateTime secondDeparture = now.plusHours(8).plusMinutes(20).plusHours(12).plusMinutes(45);
Flight secondFlight = new Flight("2", atl, secondDeparture,
new Airport("BRU"), secondDeparture.plusHours(2));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAwayFromHomeBase)
.given(firstAssignment, secondAssignment)
.penalizesBy(180); // 3 hours violation due to travel time adjustment
}
@Test
void extendedRecoveryRestPeriod_satisfied_frequentLongRests() {
Employee employee = new Employee("1");
Airport homeAirport = new Airport("LHR");
employee.setHomeAirport(homeAirport);
LocalDateTime start = LocalDateTime.of(2025, 1, 1, 8, 0);
// Pattern: Work 3 days, 40h rest, repeat (well within compliance)
List<Object> given = new ArrayList<>();
given.add(employee);
LocalDateTime currentTime = start;
for (int cycle = 0; cycle < 3; cycle++) {
// 3 assignments with short rests
for (int i = 0; i < 3; i++) {
Flight flight = new Flight("C" + cycle + "F" + i, homeAirport, currentTime,
new Airport("JFK"), currentTime.plusHours(8));
FlightAssignment assignment = new FlightAssignment("C" + cycle + "A" + i, flight);
assignment.setEmployee(employee);
given.add(assignment);
// 15 hour rest between assignments
currentTime = currentTime.plusHours(8).plusMinutes(20).plusHours(15).plusMinutes(45);
}
// Long rest (40 hours) after every 3 assignments
currentTime = currentTime.minusMinutes(45).plusHours(40).plusMinutes(45);
}
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::extendedRecoveryRestPeriod)
.given(given.toArray())
.penalizesBy(0);
}
@Test
void extendedRecoveryRestPeriod_violated_continuousShortRests() {
Employee employee = new Employee("1");
Airport homeAirport = new Airport("LHR");
employee.setHomeAirport(homeAirport);
LocalDateTime start = LocalDateTime.of(2025, 1, 1, 8, 0);
// Create 10 assignments over 10 days with only 15-hour rests (< 36h)
// This spans ~230 hours without a qualifying rest - should violate
List<Object> given = new ArrayList<>();
given.add(employee);
LocalDateTime currentTime = start;
for (int i = 0; i < 10; i++) {
Flight flight = new Flight("F" + i, homeAirport, currentTime,
new Airport("JFK"), currentTime.plusHours(8));
FlightAssignment assignment = new FlightAssignment("A" + i, flight);
assignment.setEmployee(employee);
given.add(assignment);
// 15 hour rest (duty end to next duty start)
// Duty end = arrival + 20min = currentTime + 8h 20min
// Next duty start = duty end + 15h
// Next departure = duty start + 45min
currentTime = currentTime.plusHours(23).plusMinutes(20);
}
// Expected violations: multiple windows will exceed 168h without 36h rest
// With 10 assignments over ~230 hours with no 36h rest, there are 2 violations
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::extendedRecoveryRestPeriod)
.given(given.toArray())
.penalizesBy(2);
}
@Test
void extendedRecoveryRestPeriod_satisfied_sparseSchedule() {
Employee employee = new Employee("1");
Airport homeAirport = new Airport("LHR");
employee.setHomeAirport(homeAirport);
LocalDateTime start = LocalDateTime.of(2025, 1, 1, 8, 0);
// Two assignments 10 days apart with long rest
Flight flight1 = new Flight("F1", homeAirport, start,
new Airport("JFK"), start.plusHours(8));
FlightAssignment a1 = new FlightAssignment("A1", flight1);
a1.setEmployee(employee);
// 230 hour (9.5 day) rest period - well over 36h
LocalDateTime secondDeparture = start.plusHours(8).plusMinutes(20)
.plusHours(230).plusMinutes(45);
Flight flight2 = new Flight("F2", homeAirport, secondDeparture,
new Airport("JFK"), secondDeparture.plusHours(8));
FlightAssignment a2 = new FlightAssignment("A2", flight2);
a2.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::extendedRecoveryRestPeriod)
.given(employee, a1, a2)
.penalizesBy(0);
}
@Test
void extendedRecoveryRestPeriod_edgeCase_singleAssignment() {
Employee employee = new Employee("1");
Airport homeAirport = new Airport("LHR");
employee.setHomeAirport(homeAirport);
LocalDateTime start = LocalDateTime.of(2025, 1, 1, 8, 0);
Flight flight = new Flight("F1", homeAirport, start,
new Airport("JFK"), start.plusHours(8));
FlightAssignment assignment = new FlightAssignment("A1", flight);
assignment.setEmployee(employee);
// Single assignment - no violation possible
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::extendedRecoveryRestPeriod)
.given(employee, assignment)
.penalizesBy(0);
}
@Test
void extendedRecoveryRestPeriod_satisfied_weeklyPattern() {
Employee employee = new Employee("1");
Airport homeAirport = new Airport("LHR");
employee.setHomeAirport(homeAirport);
LocalDateTime start = LocalDateTime.of(2025, 1, 1, 8, 0);
// Realistic pattern: Work Mon-Fri with 15h rests, then 48h weekend rest
List<Object> given = new ArrayList<>();
given.add(employee);
LocalDateTime currentTime = start;
// Week 1: Mon-Fri
for (int i = 0; i < 5; i++) {
Flight flight = new Flight("W1F" + i, homeAirport, currentTime,
new Airport("JFK"), currentTime.plusHours(8));
FlightAssignment assignment = new FlightAssignment("W1A" + i, flight);
assignment.setEmployee(employee);
given.add(assignment);
currentTime = currentTime.plusHours(23).plusMinutes(20); // 15h rest
}
// Weekend rest: 48 hours
currentTime = currentTime.minusMinutes(45).plusHours(48).plusMinutes(45);
// Week 2: Mon-Fri
for (int i = 0; i < 5; i++) {
Flight flight = new Flight("W2F" + i, homeAirport, currentTime,
new Airport("JFK"), currentTime.plusHours(8));
FlightAssignment assignment = new FlightAssignment("W2A" + i, flight);
assignment.setEmployee(employee);
given.add(assignment);
currentTime = currentTime.plusHours(23).plusMinutes(20);
}
// Should satisfy - 48h rest every week
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::extendedRecoveryRestPeriod)
.given(given.toArray())
.penalizesBy(0);
}
@Test
void extendedRecoveryRestPeriod_multipleEmployees_isolated() {
// Employee 1: Violates ERRP
Employee employee1 = new Employee("1");
employee1.setHomeAirport(new Airport("LHR"));
// Employee 2: Satisfies ERRP
Employee employee2 = new Employee("2");
employee2.setHomeAirport(new Airport("LHR"));
LocalDateTime start = LocalDateTime.of(2025, 1, 1, 8, 0);
List<Object> given = new ArrayList<>();
given.add(employee1);
given.add(employee2);
// Employee 1: Continuous short rests (violation)
LocalDateTime time1 = start;
for (int i = 0; i < 10; i++) {
Flight flight = new Flight("E1F" + i, new Airport("LHR"), time1,
new Airport("JFK"), time1.plusHours(8));
FlightAssignment assignment = new FlightAssignment("E1A" + i, flight);
assignment.setEmployee(employee1);
given.add(assignment);
time1 = time1.plusHours(23).plusMinutes(20);
}
// Employee 2: Long rests (no violation)
LocalDateTime time2 = start;
for (int i = 0; i < 3; i++) {
Flight flight = new Flight("E2F" + i, new Airport("LHR"), time2,
new Airport("JFK"), time2.plusHours(8));
FlightAssignment assignment = new FlightAssignment("E2A" + i, flight);
assignment.setEmployee(employee2);
given.add(assignment);
time2 = time2.plusHours(8).plusMinutes(20).plusHours(50).plusMinutes(45);
}
// Should only penalize employee1's violations (2 violations for employee1)
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::extendedRecoveryRestPeriod)
.given(given.toArray())
.penalizesBy(2);
}
@Test
void transferBetweenTwoFlights_withExcessiveTaxiTime() {
Employee employee = new Employee("1");
Airport lhr = new Airport("LHR");
// Set taxi time to same airport as 400 minutes (> 5 hours limit)
lhr.setTaxiTimeInMinutes(java.util.Map.of("LHR", 400L));
LocalDateTime now = LocalDateTime.now();
Flight firstFlight = new Flight("1", lhr, now, lhr, now.plusHours(2));
FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight);
firstAssignment.setEmployee(employee);
// Second flight also at LHR but taxi time is excessive
Flight secondFlight = new Flight("2", lhr, now.plusHours(3), lhr, now.plusHours(5));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::transferBetweenTwoFlights)
.given(firstAssignment, secondAssignment)
.penalizesBy(1); // same airport but excessive taxi time
}
@Test
void transferBetweenTwoFlights_withAcceptableTaxiTime() {
Employee employee = new Employee("1");
Airport lhr = new Airport("LHR");
// Set acceptable taxi time (< 5 hours)
lhr.setTaxiTimeInMinutes(java.util.Map.of("LHR", 200L));
LocalDateTime now = LocalDateTime.now();
Flight firstFlight = new Flight("1", lhr, now, lhr, now.plusHours(2));
FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight);
firstAssignment.setEmployee(employee);
Flight secondFlight = new Flight("2", lhr, now.plusHours(3), lhr, now.plusHours(5));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::transferBetweenTwoFlights)
.given(firstAssignment, secondAssignment)
.penalizesBy(0); // acceptable taxi time at same airport
}
@Test
void minimumRestAfterLongHaul_satisfied() {
Employee employee = new Employee("1");
Airport homeAirport = new Airport("LHR");
employee.setHomeAirport(homeAirport);
LocalDateTime now = LocalDateTime.now();
// First flight: Long haul (9 hours)
Flight longHaulFlight = new Flight("1", homeAirport, now,
new Airport("JFK"), now.plusHours(9));
FlightAssignment longHaulAssignment = new FlightAssignment("1", longHaulFlight);
longHaulAssignment.setEmployee(employee);
// Second flight: 50 hours rest after long haul (exceeds 48h requirement)
// Duty end of long haul = arrival + 20 min = now + 9h + 20min
// Second duty start = duty end + 50h
LocalDateTime secondDeparture = now.plusHours(9).plusMinutes(20)
.plusHours(50).plusMinutes(45);
Flight secondFlight = new Flight("2", homeAirport, secondDeparture,
new Airport("BRU"), secondDeparture.plusHours(2));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterLongHaul)
.given(longHaulAssignment, secondAssignment)
.penalizesBy(0); // 50 hours rest satisfies 48 hour minimum
}
@Test
void minimumRestAfterLongHaul_violated() {
Employee employee = new Employee("1");
Airport homeAirport = new Airport("LHR");
employee.setHomeAirport(homeAirport);
LocalDateTime now = LocalDateTime.now();
// First flight: Long haul (10 hours)
Flight longHaulFlight = new Flight("1", homeAirport, now,
new Airport("JFK"), now.plusHours(10));
FlightAssignment longHaulAssignment = new FlightAssignment("1", longHaulFlight);
longHaulAssignment.setEmployee(employee);
// Second flight: Only 30 hours rest after long haul
// Violates 48 hour minimum by 18 hours = 1080 minutes
LocalDateTime secondDeparture = now.plusHours(10).plusMinutes(20)
.plusHours(30).plusMinutes(45);
Flight secondFlight = new Flight("2", homeAirport, secondDeparture,
new Airport("ATL"), secondDeparture.plusHours(3));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterLongHaul)
.given(longHaulAssignment, secondAssignment)
.penalizesBy(1080); // 18 hours = 1080 minutes violation
}
@Test
void minimumRestAfterLongHaul_notApplicableToShortHaul() {
Employee employee = new Employee("1");
Airport homeAirport = new Airport("LHR");
employee.setHomeAirport(homeAirport);
LocalDateTime now = LocalDateTime.now();
// First flight: Short haul (3 hours, below 8h threshold)
Flight shortHaulFlight = new Flight("1", homeAirport, now,
new Airport("BRU"), now.plusHours(3));
FlightAssignment shortHaulAssignment = new FlightAssignment("1", shortHaulFlight);
shortHaulAssignment.setEmployee(employee);
// Second flight: Only 15 hours rest
// Constraint should not apply because first flight is not long haul
LocalDateTime secondDeparture = now.plusHours(3).plusMinutes(20)
.plusHours(15).plusMinutes(45);
Flight secondFlight = new Flight("2", homeAirport, secondDeparture,
new Airport("ATL"), secondDeparture.plusHours(8));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterLongHaul)
.given(shortHaulAssignment, secondAssignment)
.penalizesBy(0); // constraint doesn't apply to short haul flights
}
@Test
void minimumRestAfterLongHaul_exactlyTenHours() {
Employee employee = new Employee("1");
Airport homeAirport = new Airport("LHR");
employee.setHomeAirport(homeAirport);
LocalDateTime now = LocalDateTime.now();
// First flight: Exactly 10 hours (boundary case - should be long haul)
Flight boundaryFlight = new Flight("1", homeAirport, now,
new Airport("JFK"), now.plusHours(10));
FlightAssignment boundaryAssignment = new FlightAssignment("1", boundaryFlight);
boundaryAssignment.setEmployee(employee);
// Second flight: Only 24 hours rest
// Should violate because 10h exactly is considered long haul
LocalDateTime secondDeparture = now.plusHours(10).plusMinutes(20)
.plusHours(24).plusMinutes(45);
Flight secondFlight = new Flight("2", homeAirport, secondDeparture,
new Airport("ATL"), secondDeparture.plusHours(3));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterLongHaul)
.given(boundaryAssignment, secondAssignment)
.penalizesBy(1440); // 24 hours = 1440 minutes violation
}
@Test
void minimumRestAfterLongHaul_awayFromHomeBase() {
Employee employee = new Employee("1");
Airport homeAirport = new Airport("LHR");
employee.setHomeAirport(homeAirport);
Airport jfk = new Airport("JFK");
Airport atl = new Airport("ATL");
LocalDateTime now = LocalDateTime.now();
// First flight: Long haul ending at JFK (away from home) - 10 hours
Flight longHaulFlight = new Flight("1", homeAirport, now,
jfk, now.plusHours(10));
FlightAssignment longHaulAssignment = new FlightAssignment("1", longHaulFlight);
longHaulAssignment.setEmployee(employee);
// Second flight: Departing from ATL with only 36 hours rest
// Should still violate 48h requirement even away from home
LocalDateTime secondDeparture = now.plusHours(10).plusMinutes(20)
.plusHours(36).plusMinutes(45);
Flight secondFlight = new Flight("2", atl, secondDeparture,
new Airport("BRU"), secondDeparture.plusHours(7));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterLongHaul)
.given(longHaulAssignment, secondAssignment)
.penalizesBy(720); // 12 hours = 720 minutes violation
}
@Test
void minimumRestAfterLongHaul_multipleLongHauls() {
Employee employee = new Employee("1");
Airport homeAirport = new Airport("LHR");
employee.setHomeAirport(homeAirport);
LocalDateTime now = LocalDateTime.now();
// First long haul
Flight firstLongHaul = new Flight("1", homeAirport, now,
new Airport("JFK"), now.plusHours(9));
FlightAssignment firstAssignment = new FlightAssignment("1", firstLongHaul);
firstAssignment.setEmployee(employee);
// Second long haul after 50h rest (satisfies first constraint)
LocalDateTime secondDeparture = now.plusHours(9).plusMinutes(20)
.plusHours(50).plusMinutes(45);
Flight secondLongHaul = new Flight("2", homeAirport, secondDeparture,
new Airport("ATL"), secondDeparture.plusHours(10));
FlightAssignment secondAssignment = new FlightAssignment("2", secondLongHaul);
secondAssignment.setEmployee(employee);
// Third flight after only 20h rest (violates second constraint)
LocalDateTime thirdDeparture = secondDeparture.plusHours(10).plusMinutes(20)
.plusHours(20).plusMinutes(45);
Flight thirdFlight = new Flight("3", homeAirport, thirdDeparture,
new Airport("BRU"), thirdDeparture.plusHours(2));
FlightAssignment thirdAssignment = new FlightAssignment("3", thirdFlight);
thirdAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterLongHaul)
.given(firstAssignment, secondAssignment, thirdAssignment)
.penalizesBy(1680); // 28 hours violation for second-to-third
}
@Test
void minimumRestAfterLongHaul_multipleEmployeesIsolated() {
// Employee 1: Violates constraint
Employee employee1 = new Employee("1");
employee1.setHomeAirport(new Airport("LHR"));
// Employee 2: Satisfies constraint
Employee employee2 = new Employee("2");
employee2.setHomeAirport(new Airport("LHR"));
LocalDateTime now = LocalDateTime.now();
// Employee 1: Long haul with insufficient rest - changed to 10 hours
Flight e1Flight1 = new Flight("F1", new Airport("LHR"), now,
new Airport("JFK"), now.plusHours(10));
FlightAssignment e1Assignment1 = new FlightAssignment("A1", e1Flight1);
e1Assignment1.setEmployee(employee1);
Flight e1Flight2 = new Flight("F2", new Airport("LHR"),
now.plusHours(10).plusMinutes(20).plusHours(30).plusMinutes(45),
new Airport("ATL"), now.plusHours(43));
FlightAssignment e1Assignment2 = new FlightAssignment("A2", e1Flight2);
e1Assignment2.setEmployee(employee1);
// Employee 2: Long haul with sufficient rest
Flight e2Flight1 = new Flight("F3", new Airport("LHR"), now,
new Airport("BNE"), now.plusHours(20));
FlightAssignment e2Assignment1 = new FlightAssignment("A3", e2Flight1);
e2Assignment1.setEmployee(employee2);
Flight e2Flight2 = new Flight("F4", new Airport("LHR"),
now.plusHours(20).plusMinutes(20).plusHours(60).plusMinutes(45),
new Airport("JFK"), now.plusHours(88));
FlightAssignment e2Assignment2 = new FlightAssignment("A4", e2Flight2);
e2Assignment2.setEmployee(employee2);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterLongHaul)
.given(employee1, employee2, e1Assignment1, e1Assignment2, e2Assignment1, e2Assignment2)
.penalizesBy(1080); // Only employee1's violation (18 hours)
}
@Test
void minimumRestAfterConsecutiveLongHaul_satisfied() {
Employee employee = new Employee("1");
Airport lhr = new Airport("LHR");
Airport jfk = new Airport("JFK");
Airport lax = new Airport("LAX");
employee.setHomeAirport(lhr);
LocalDateTime now = LocalDateTime.now();
// First flight: 5 hours (short haul alone)
Flight firstFlight = new Flight("1", lhr, now,
jfk, now.plusHours(5));
FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight);
firstAssignment.setEmployee(employee);
// Second flight: 6 hours, consecutive with first (total 11 hours = long haul)
// Departs 1 hour after first arrives (within 2 hour window)
LocalDateTime secondDeparture = now.plusHours(5).plusHours(1);
Flight secondFlight = new Flight("2", jfk, secondDeparture,
lax, secondDeparture.plusHours(6));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
// Third flight: 50 hours rest after the consecutive long haul
// Should satisfy 48 hour requirement
LocalDateTime thirdDeparture = secondDeparture.plusHours(6).plusMinutes(20)
.plusHours(50).plusMinutes(45);
Flight thirdFlight = new Flight("3", lhr, thirdDeparture,
lax, thirdDeparture.plusHours(3));
FlightAssignment thirdAssignment = new FlightAssignment("3", thirdFlight);
thirdAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterConsecutiveLongHaul)
.given(firstAssignment, secondAssignment, thirdAssignment)
.penalizesBy(0); // 50 hours rest satisfies 48 hour minimum
}
@Test
void minimumRestAfterConsecutiveLongHaul_violated() {
Employee employee = new Employee("1");
Airport lhr = new Airport("LHR");
Airport jfk = new Airport("JFK");
Airport lax = new Airport("LAX");
employee.setHomeAirport(lhr);
LocalDateTime now = LocalDateTime.now();
// First flight: 5 hours
Flight firstFlight = new Flight("1", lhr, now,
jfk, now.plusHours(5));
FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight);
firstAssignment.setEmployee(employee);
// Second flight: 6 hours, consecutive (total 11 hours = long haul)
LocalDateTime secondDeparture = now.plusHours(5).plusHours(1);
Flight secondFlight = new Flight("2", jfk, secondDeparture,
lax, secondDeparture.plusHours(6));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
// Third flight: Only 30 hours rest after consecutive long haul
// Violates 48 hour minimum by 18 hours = 1080 minutes
LocalDateTime thirdDeparture = secondDeparture.plusHours(6).plusMinutes(20)
.plusHours(30).plusMinutes(45);
Flight thirdFlight = new Flight("3", lhr, thirdDeparture,
lax, thirdDeparture.plusHours(3));
FlightAssignment thirdAssignment = new FlightAssignment("3", thirdFlight);
thirdAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterConsecutiveLongHaul)
.given(firstAssignment, secondAssignment, thirdAssignment)
.penalizesBy(1080); // 18 hours = 1080 minutes violation
}
@Test
void minimumRestAfterConsecutiveLongHaul_notApplicableToNonConsecutive() {
Employee employee = new Employee("1");
Airport lhr = new Airport("LHR");
Airport jfk = new Airport("JFK");
Airport atl = new Airport("ATL");
employee.setHomeAirport(lhr);
LocalDateTime now = LocalDateTime.now();
// First flight: 5 hours
Flight firstFlight = new Flight("1", lhr, now,
jfk, now.plusHours(5));
FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight);
firstAssignment.setEmployee(employee);
// Second flight: 6 hours but NOT consecutive (different airport)
LocalDateTime secondDeparture = now.plusHours(5).plusHours(1);
Flight secondFlight = new Flight("2", atl, secondDeparture, // ATL, not JFK
lhr, secondDeparture.plusHours(6));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
// Third flight: Only 20 hours rest
// Should NOT violate because first + second don't form consecutive long haul
LocalDateTime thirdDeparture = secondDeparture.plusHours(6).plusMinutes(20)
.plusHours(20).plusMinutes(45);
Flight thirdFlight = new Flight("3", lhr, thirdDeparture,
jfk, thirdDeparture.plusHours(5));
FlightAssignment thirdAssignment = new FlightAssignment("3", thirdFlight);
thirdAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterConsecutiveLongHaul)
.given(firstAssignment, secondAssignment, thirdAssignment)
.penalizesBy(0); // constraint doesn't apply to non-consecutive flights
}
@Test
void minimumRestAfterConsecutiveLongHaul_notApplicableToShortTotal() {
Employee employee = new Employee("1");
Airport lhr = new Airport("LHR");
Airport jfk = new Airport("JFK");
Airport lax = new Airport("LAX");
employee.setHomeAirport(lhr);
LocalDateTime now = LocalDateTime.now();
// First flight: 3 hours
Flight firstFlight = new Flight("1", lhr, now,
jfk, now.plusHours(3));
FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight);
firstAssignment.setEmployee(employee);
// Second flight: 4 hours, consecutive (total only 7 hours = NOT long haul)
LocalDateTime secondDeparture = now.plusHours(3).plusHours(1);
Flight secondFlight = new Flight("2", jfk, secondDeparture,
lax, secondDeparture.plusHours(4));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
// Third flight: Only 20 hours rest
// Should NOT violate because combined duration < 10 hours
LocalDateTime thirdDeparture = secondDeparture.plusHours(4).plusMinutes(20)
.plusHours(20).plusMinutes(45);
Flight thirdFlight = new Flight("3", lhr, thirdDeparture,
lax, thirdDeparture.plusHours(3));
FlightAssignment thirdAssignment = new FlightAssignment("3", thirdFlight);
thirdAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterConsecutiveLongHaul)
.given(firstAssignment, secondAssignment, thirdAssignment)
.penalizesBy(0); // constraint doesn't apply when total < 10 hours
}
@Test
void minimumRestAfterConsecutiveLongHaul_tooLongGapBetweenFlights() {
Employee employee = new Employee("1");
Airport lhr = new Airport("LHR");
Airport jfk = new Airport("JFK");
Airport lax = new Airport("LAX");
employee.setHomeAirport(lhr);
LocalDateTime now = LocalDateTime.now();
// First flight: 5 hours
Flight firstFlight = new Flight("1", lhr, now,
jfk, now.plusHours(5));
FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight);
firstAssignment.setEmployee(employee);
// Second flight: 6 hours but 3 hours after first (> 2 hour limit)
// Not considered consecutive despite total being 11 hours
LocalDateTime secondDeparture = now.plusHours(5).plusHours(3);
Flight secondFlight = new Flight("2", jfk, secondDeparture,
lax, secondDeparture.plusHours(6));
FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight);
secondAssignment.setEmployee(employee);
// Third flight: Only 20 hours rest
// Should NOT violate because gap between flights is too long
LocalDateTime thirdDeparture = secondDeparture.plusHours(6).plusMinutes(20)
.plusHours(20).plusMinutes(45);
Flight thirdFlight = new Flight("3", lhr, thirdDeparture,
lax, thirdDeparture.plusHours(3));
FlightAssignment thirdAssignment = new FlightAssignment("3", thirdFlight);
thirdAssignment.setEmployee(employee);
constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterConsecutiveLongHaul)
.given(firstAssignment, secondAssignment, thirdAssignment)
.penalizesBy(0); // constraint doesn't apply when gap > 2 hours
}
}