Add integration tests for PassengerOrientedSelector to reproduce Redmine#7812

This commit is contained in:
Romain Thouvenin 2024-04-17 15:03:04 +02:00
parent f6b27978e9
commit a9f5c36d49
5 changed files with 536 additions and 35 deletions

View File

@ -1,11 +1,17 @@
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { Selector } from '../algorithm.abstract';
import { Waypoint } from '../../../types/waypoint.type';
import { Point } from '../../../types/point.type';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { ScheduleItem } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
import { Point } from '../../../types/point.type';
import { Waypoint } from '../../../types/waypoint.type';
import { Selector } from '../algorithm.abstract';
/**
* This class complements the AdRepository prisma service by turning a match query object into a SQL query,
* with the assumption that the query is passenger-oriented (i.e. it is up to the driver to go out of his way to pick up the passenger)
* TODO: Converting the query object into a SQL query is a job for the prisma service / repository implementation,
* any logic related to being passenger-oriented should be in the domain layer
*/
export class PassengerOrientedSelector extends Selector {
select = async (): Promise<CandidateEntity[]> => {
const queryStringRoles: QueryStringRole[] = [];

View File

@ -1,6 +1,7 @@
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
import { v4 as uuidv4 } from 'uuid';
export const Nice: PointProps = {
lat: 43.7102,
@ -22,9 +23,16 @@ export const Toulon: PointProps = {
lon: 5.95,
};
export function monday(time: string): ScheduleItemProps {
return { day: 1, time: time, margin: 900 };
}
export function wednesday(time: string): ScheduleItemProps {
return { day: 3, time: time, margin: 900 };
}
export function thursday(time: string): ScheduleItemProps {
return { day: 4, time: time, margin: 900 };
}
export function weekdays(time: string): ScheduleItemProps[] {
return [1, 2, 3, 4, 5].map<ScheduleItemProps>((day) => ({
@ -34,22 +42,41 @@ export function weekdays(time: string): ScheduleItemProps[] {
}));
}
export function NiceMarseille(
function createAdPropsDefaults(): CreateAdProps {
return {
id: uuidv4(),
driver: false,
passenger: false,
frequency: Frequency.PUNCTUAL,
fromDate: '',
toDate: '',
schedule: [],
seatsProposed: 1,
seatsRequested: 1,
strict: false,
waypoints: [],
points: [],
driverDuration: 0,
driverDistance: 0,
passengerDuration: 0,
passengerDistance: 0,
fwdAzimuth: 0,
backAzimuth: 0,
};
}
export function driverNiceMarseille(
frequency: Frequency,
dates: string[],
schedule: ScheduleItemProps[],
): CreateAdProps {
return {
id: 'b4b56444-f8d3-4110-917c-e37bba77f383',
...createAdPropsDefaults(),
driver: true,
passenger: false,
frequency: frequency,
fromDate: dates[0],
toDate: dates[1],
schedule: schedule,
seatsProposed: 3,
seatsRequested: 1,
strict: false,
waypoints: [Nice, Marseille],
points: [Nice, SaintRaphael, Toulon, Marseille],
driverDuration: 7668,
@ -60,3 +87,24 @@ export function NiceMarseille(
backAzimuth: 93,
};
}
export function passengerToulonMarseille(
frequency: Frequency,
dates: string[],
schedule: ScheduleItemProps[],
): CreateAdProps {
return {
...createAdPropsDefaults(),
passenger: true,
frequency: frequency,
fromDate: dates[0],
toDate: dates[1],
schedule: schedule,
waypoints: [Toulon, Marseille],
points: [Toulon, Marseille],
driverDuration: 2460,
driverDistance: 64000,
passengerDuration: 2460,
passengerDistance: 64000,
};
}

View File

@ -2,7 +2,7 @@ import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
import { NiceMarseille, wednesday, weekdays } from './ad.fixtures';
import { driverNiceMarseille, wednesday, weekdays } from './ad.fixtures';
import { integrationTestingModule } from './integration.setup';
describe('Ad Repository', () => {
@ -25,7 +25,7 @@ describe('Ad Repository', () => {
it('should create a punctual ad', async () => {
const beforeCount = await prismaService.ad.count();
const createAdProps = NiceMarseille(
const createAdProps = driverNiceMarseille(
Frequency.PUNCTUAL,
['2023-02-01', '2023-02-01'],
[wednesday('08:30')],
@ -41,7 +41,7 @@ describe('Ad Repository', () => {
it('should create a recurrent ad', async () => {
const beforeCount = await prismaService.ad.count();
const createAdProps = NiceMarseille(
const createAdProps = driverNiceMarseille(
Frequency.RECURRENT,
['2023-02-01', '2024-01-31'],
weekdays('08:30'),

View File

@ -0,0 +1,467 @@
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
import { PassengerOrientedSelector } from '@modules/ad/core/application/queries/match/selector/passenger-oriented.selector';
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
import { bareMockGeorouter } from '../unit/georouter.mock';
import {
Marseille,
Nice,
SaintRaphael,
Toulon,
driverNiceMarseille,
monday,
passengerToulonMarseille,
thursday,
wednesday,
} from './ad.fixtures';
import { integrationTestingModule } from './integration.setup';
function baseMatchQuery(
frequency: Frequency,
dates: [string, string],
scheduleItems: ScheduleItemProps[],
waypoints: WaypointDto[],
): MatchQuery {
return new MatchQuery(
{
algorithmType: AlgorithmType.PASSENGER_ORIENTED,
driver: false,
passenger: false,
frequency: frequency,
fromDate: dates[0],
toDate: dates[1],
useAzimuth: false,
useProportion: false,
remoteness: 15000,
schedule: scheduleItems,
strict: false,
waypoints: waypoints,
},
bareMockGeorouter,
);
}
function passengerQueryToulonMarseille(
frequency: Frequency,
dates: [string, string],
scheduleItems: ScheduleItemProps[],
): MatchQuery {
const matchQuery = baseMatchQuery(frequency, dates, scheduleItems, [
{ position: 0, ...Toulon },
{ position: 1, ...Marseille },
]);
matchQuery.passenger = true;
matchQuery.passengerRoute = {
distance: 64000,
duration: 2460,
points: [Toulon, Marseille],
// Not used by this query
fwdAzimuth: 0,
backAzimuth: 0,
distanceAzimuth: 0,
};
return matchQuery;
}
function driverQueryNiceMarseille(
frequency: Frequency,
dates: [string, string],
scheduleItems: ScheduleItemProps[],
): MatchQuery {
const matchQuery = baseMatchQuery(frequency, dates, scheduleItems, [
{ position: 0, ...Nice },
{ position: 1, ...Marseille },
]);
matchQuery.driver = true;
matchQuery.driverRoute = {
distance: 199000,
duration: 7668,
points: [Nice, SaintRaphael, Toulon, Marseille],
// Not used by this query
fwdAzimuth: 0,
backAzimuth: 0,
distanceAzimuth: 0,
};
return matchQuery;
}
describe('PassengerOriented selector', () => {
let prismaService: PrismaService;
let adRepository: AdRepository;
const insertAd = async (adProps: CreateAdProps): Promise<void> => {
const ad = AdEntity.create(adProps);
return adRepository.insertExtra(ad, 'ad');
};
beforeAll(async () => {
({ prismaService, adRepository } = await integrationTestingModule());
});
afterAll(async () => {
await prismaService.$disconnect();
});
beforeEach(async () => {
await prismaService.ad.deleteMany();
});
describe('select', () => {
it('should find a driver that departs on the same day', async () => {
await insertAd(
driverNiceMarseille(
Frequency.PUNCTUAL,
['2023-02-01', '2023-02-01'],
[wednesday('08:30')],
),
);
const passengerOrientedSelector = new PassengerOrientedSelector(
passengerQueryToulonMarseille(
Frequency.PUNCTUAL,
['2023-02-01', '2023-02-01'],
[wednesday('10:00')],
),
adRepository,
);
const candidates = await passengerOrientedSelector.select();
expect(candidates.length).toBe(1);
});
it('should find a passenger that departs on the same day', async () => {
await insertAd(
passengerToulonMarseille(
Frequency.PUNCTUAL,
['2023-02-01', '2023-02-01'],
[wednesday('10:00')],
),
);
const passengerOrientedSelector = new PassengerOrientedSelector(
driverQueryNiceMarseille(
Frequency.PUNCTUAL,
['2023-02-01', '2023-02-01'],
[wednesday('08:30')],
),
adRepository,
);
const candidates = await passengerOrientedSelector.select();
expect(candidates.length).toBe(1);
});
it('should find a driver that departs the day before', async () => {
await insertAd(
driverNiceMarseille(
Frequency.PUNCTUAL,
['2023-02-01', '2023-02-01'],
[wednesday('23:45')],
),
);
const passengerOrientedSelector = new PassengerOrientedSelector(
passengerQueryToulonMarseille(
Frequency.PUNCTUAL,
['2023-02-02', '2023-02-02'],
[thursday('01:15')],
),
adRepository,
);
const candidates = await passengerOrientedSelector.select();
expect(candidates.length).toBe(1);
});
it('should find a passenger that departs the day after', async () => {
await insertAd(
passengerToulonMarseille(
Frequency.PUNCTUAL,
['2023-02-02', '2023-02-02'],
[thursday('01:15')],
),
);
const passengerOrientedSelector = new PassengerOrientedSelector(
driverQueryNiceMarseille(
Frequency.PUNCTUAL,
['2023-02-01', '2023-02-01'],
[wednesday('23:45')],
),
adRepository,
);
const candidates = await passengerOrientedSelector.select();
expect(candidates.length).toBe(1);
});
it('should find a driver that departs shortly after midnight', async () => {
await insertAd(
driverNiceMarseille(
Frequency.PUNCTUAL,
['2023-02-02', '2023-02-02'],
//01:30 in Nice is 00:30 in UTC
[thursday('01:30')],
),
);
const passengerOrientedSelector = new PassengerOrientedSelector(
passengerQueryToulonMarseille(
Frequency.PUNCTUAL,
['2023-02-02', '2023-02-02'],
[thursday('03:00')],
),
adRepository,
);
const candidates = await passengerOrientedSelector.select();
expect(candidates.length).toBe(1);
});
it('should find a passenger that departs shortly after midnight', async () => {
await insertAd(
passengerToulonMarseille(
Frequency.PUNCTUAL,
['2023-02-02', '2023-02-02'],
[thursday('03:00')],
),
);
const passengerOrientedSelector = new PassengerOrientedSelector(
driverQueryNiceMarseille(
Frequency.PUNCTUAL,
['2023-02-02', '2023-02-02'],
[thursday('01:30')],
),
adRepository,
);
const candidates = await passengerOrientedSelector.select();
expect(candidates.length).toBe(1);
});
it('should NOT find a driver that departs the day after', async () => {
await insertAd(
driverNiceMarseille(
Frequency.PUNCTUAL,
['2023-02-02', '2023-02-02'],
[thursday('08:30')],
),
);
const passengerOrientedSelector = new PassengerOrientedSelector(
passengerQueryToulonMarseille(
Frequency.PUNCTUAL,
['2023-02-01', '2023-02-01'],
[wednesday('10:00')],
),
adRepository,
);
const candidates = await passengerOrientedSelector.select();
expect(candidates.length).toBe(0);
});
it('should NOT find a passenger that departs the day before', async () => {
await insertAd(
passengerToulonMarseille(
Frequency.PUNCTUAL,
['2023-02-01', '2023-02-01'],
[wednesday('10:00')],
),
);
const passengerOrientedSelector = new PassengerOrientedSelector(
driverQueryNiceMarseille(
Frequency.PUNCTUAL,
['2023-02-02', '2023-02-02'],
[thursday('08:30')],
),
adRepository,
);
const candidates = await passengerOrientedSelector.select();
expect(candidates.length).toBe(0);
});
it('should find a recurring driver that interesects', async () => {
await Promise.all([
insertAd(
driverNiceMarseille(
Frequency.RECURRENT,
['2023-02-01', '2023-02-28'],
[wednesday('08:30')],
),
),
insertAd(
driverNiceMarseille(
Frequency.RECURRENT,
['2023-02-01', '2023-02-18'],
[wednesday('08:30')],
),
),
insertAd(
driverNiceMarseille(
Frequency.RECURRENT,
['2023-02-12', '2023-02-28'],
[wednesday('08:30')],
),
),
insertAd(
driverNiceMarseille(
Frequency.RECURRENT,
['2023-02-12', '2023-02-18'],
[wednesday('08:30')],
),
),
]);
const passengerOrientedSelector = new PassengerOrientedSelector(
passengerQueryToulonMarseille(
Frequency.RECURRENT,
['2023-02-10', '2023-02-20'],
[wednesday('10:00')],
),
adRepository,
);
const candidates = await passengerOrientedSelector.select();
expect(candidates.length).toBe(4);
});
it("should NOT find a recurring driver that doesn't interesect", async () => {
await Promise.all([
insertAd(
driverNiceMarseille(
Frequency.RECURRENT,
['2023-02-01', '2023-02-10'],
[wednesday('08:30')],
),
),
insertAd(
driverNiceMarseille(
Frequency.RECURRENT,
['2023-02-20', '2023-02-28'],
[wednesday('08:30')],
),
),
]);
const passengerOrientedSelector = new PassengerOrientedSelector(
passengerQueryToulonMarseille(
Frequency.RECURRENT,
['2023-02-12', '2023-02-18'],
[wednesday('10:00')],
),
adRepository,
);
const candidates = await passengerOrientedSelector.select();
expect(candidates.length).toBe(0);
});
it('should find a recurring passenger that interesects', async () => {
await Promise.all([
insertAd(
passengerToulonMarseille(
Frequency.RECURRENT,
['2023-02-01', '2023-02-28'],
[wednesday('10:00')],
),
),
insertAd(
passengerToulonMarseille(
Frequency.RECURRENT,
['2023-02-01', '2023-02-18'],
[wednesday('10:00')],
),
),
insertAd(
passengerToulonMarseille(
Frequency.RECURRENT,
['2023-02-12', '2023-02-28'],
[wednesday('10:00')],
),
),
insertAd(
passengerToulonMarseille(
Frequency.RECURRENT,
['2023-02-12', '2023-02-18'],
[wednesday('10:00')],
),
),
]);
const passengerOrientedSelector = new PassengerOrientedSelector(
driverQueryNiceMarseille(
Frequency.RECURRENT,
['2023-02-10', '2023-02-20'],
[wednesday('08:30')],
),
adRepository,
);
const candidates = await passengerOrientedSelector.select();
expect(candidates.length).toBe(4);
});
it("should NOT find a recurring passenger that doesn't interesect", async () => {
await Promise.all([
insertAd(
passengerToulonMarseille(
Frequency.RECURRENT,
['2023-02-01', '2023-02-10'],
[wednesday('10:00')],
),
),
insertAd(
passengerToulonMarseille(
Frequency.RECURRENT,
['2023-02-20', '2023-02-28'],
[wednesday('10:00')],
),
),
]);
const passengerOrientedSelector = new PassengerOrientedSelector(
driverQueryNiceMarseille(
Frequency.RECURRENT,
['2023-02-12', '2023-02-18'],
[wednesday('08:30')],
),
adRepository,
);
const candidates = await passengerOrientedSelector.select();
expect(candidates.length).toBe(0);
});
it('should find a borderline driver that departs the day before a recurring query', async () => {
await insertAd(
driverNiceMarseille(
Frequency.PUNCTUAL,
['2023-02-01', '2023-02-01'],
[wednesday('23:45')],
),
);
const passengerOrientedSelector = new PassengerOrientedSelector(
passengerQueryToulonMarseille(
Frequency.RECURRENT,
['2023-02-02', '2023-02-28'],
[monday('13:45'), thursday('01:15')],
),
adRepository,
);
const candidates = await passengerOrientedSelector.select();
expect(candidates.length).toBe(1);
});
it('should find a borderline passenger that departs the day after a recurring query', async () => {
await insertAd(
passengerToulonMarseille(
Frequency.PUNCTUAL,
['2023-02-02', '2023-02-02'],
[thursday('01:15')],
),
);
const passengerOrientedSelector = new PassengerOrientedSelector(
driverQueryNiceMarseille(
Frequency.RECURRENT,
['2023-01-01', '2023-02-01'],
[monday('13:45'), wednesday('23:45')],
),
adRepository,
);
const candidates = await passengerOrientedSelector.select();
expect(candidates.length).toBe(1);
});
});
});

View File

@ -72,27 +72,7 @@ matchQuery.driverRoute = {
},
],
};
matchQuery.passengerRoute = {
distance: 150120,
duration: 6540,
fwdAzimuth: 276,
backAzimuth: 96,
distanceAzimuth: 148321,
points: [
{
lat: 48.689445,
lon: 6.17651,
},
{
lat: 48.7566,
lon: 4.3522,
},
{
lat: 48.8566,
lon: 2.3522,
},
],
};
matchQuery.passengerRoute = { ...matchQuery.driverRoute };
const mockMatcherRepository: AdRepositoryPort = {
insertExtra: jest.fn(),