test workload placement

This commit is contained in:
Yisroel Baum 2026-08-19 08:58:17 +03:00
parent 80f8031ecc
commit 465db4a5b8
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
4 changed files with 345 additions and 0 deletions

View file

@ -0,0 +1,129 @@
<?php
namespace Tests\Unit\Schedule;
use App\Schedule\EvenDistributionScheduler;
use App\Schedule\WorkloadPlacement;
use DateTimeImmutable;
use DateTimeZone;
use PHPUnit\Framework\TestCase;
class EvenDistributionSchedulerTest extends TestCase
{
public function test_it_places_one_heavier_day_at_the_requested_position(): void
{
$scheduler = new EvenDistributionScheduler;
$expectedCounts = [
WorkloadPlacement::Start->value => [3, 2, 2, 2, 2],
WorkloadPlacement::Middle->value => [2, 2, 3, 2, 2],
WorkloadPlacement::End->value => [2, 2, 2, 2, 3],
];
foreach (WorkloadPlacement::cases() as $placement) {
$dates = $scheduler->scheduledDates(
assignmentCount: 11,
startDate: $this->utc('2026-08-10'),
targetDate: $this->utc('2026-08-14'),
workloadPlacement: $placement,
);
$this->assertSame(
$expectedCounts[$placement->value],
$this->dailyCounts($dates),
);
}
}
public function test_it_keeps_multiple_heavier_days_together(): void
{
$scheduler = new EvenDistributionScheduler;
$expectedCounts = [
WorkloadPlacement::Start->value => [3, 3, 2, 2, 2],
WorkloadPlacement::Middle->value => [2, 3, 3, 2, 2],
WorkloadPlacement::End->value => [2, 2, 2, 3, 3],
];
foreach (WorkloadPlacement::cases() as $placement) {
$dates = $scheduler->scheduledDates(
assignmentCount: 12,
startDate: $this->utc('2026-08-10'),
targetDate: $this->utc('2026-08-14'),
workloadPlacement: $placement,
);
$this->assertSame(
$expectedCounts[$placement->value],
$this->dailyCounts($dates),
);
}
}
public function test_it_preserves_sparse_distribution_across_the_full_range(): void
{
$dates = (new EvenDistributionScheduler)->scheduledDates(
assignmentCount: 3,
startDate: $this->utc('2026-08-10'),
targetDate: $this->utc('2026-08-16'),
workloadPlacement: WorkloadPlacement::Middle,
);
$this->assertSame(
['2026-08-10', '2026-08-13', '2026-08-16'],
$this->formattedDates($dates),
);
}
public function test_it_distributes_even_and_single_day_schedules(): void
{
$scheduler = new EvenDistributionScheduler;
$evenDates = $scheduler->scheduledDates(
assignmentCount: 10,
startDate: $this->utc('2026-08-10'),
targetDate: $this->utc('2026-08-14'),
workloadPlacement: WorkloadPlacement::End,
);
$singleDayDates = $scheduler->scheduledDates(
assignmentCount: 4,
startDate: $this->utc('2026-08-10'),
targetDate: $this->utc('2026-08-10'),
workloadPlacement: WorkloadPlacement::Middle,
);
$this->assertSame([2, 2, 2, 2, 2], $this->dailyCounts($evenDates));
$this->assertSame(
[
'2026-08-10',
'2026-08-10',
'2026-08-10',
'2026-08-10',
],
$this->formattedDates($singleDayDates),
);
}
/**
* @param list<DateTimeImmutable> $dates
* @return list<int>
*/
private function dailyCounts(array $dates): array
{
return array_values(array_count_values($this->formattedDates($dates)));
}
/**
* @param list<DateTimeImmutable> $dates
* @return list<string>
*/
private function formattedDates(array $dates): array
{
return array_map(function (DateTimeImmutable $date): string {
return $date->format('Y-m-d');
}, $dates);
}
private function utc(string $date): DateTimeImmutable
{
return new DateTimeImmutable($date, new DateTimeZone('UTC'));
}
}

View file

@ -5,6 +5,7 @@ namespace Tests\Unit\Schedule\UseCases;
use App\Element\CreateElementDto;
use App\Exceptions\BadRequestException;
use App\Exceptions\NotFoundException;
use App\Schedule\EvenDistributionScheduler;
use App\Schedule\UseCases\CreateSchedule\CreateSchedule;
use App\Schedule\UseCases\CreateSchedule\CreateScheduleRequest;
use App\Set\CreateSetDto;
@ -108,12 +109,14 @@ class CreateScheduleTest extends TestCase
$setLevelRepository,
$elementRepository,
$scheduleRepository,
new EvenDistributionScheduler,
))->execute(new CreateScheduleRequest(
user: $user,
setId: $set->getId(),
levelId: $chapterLevel->getId(),
startDate: '2026-08-10',
targetDate: '2026-08-12',
workloadPlacement: 'middle',
));
$this->assertSame('chapter', $schedule->getElementKind());
@ -182,12 +185,14 @@ class CreateScheduleTest extends TestCase
$setLevelRepository,
$elementRepository,
new FakeScheduleRepository,
new EvenDistributionScheduler,
))->execute(new CreateScheduleRequest(
user: $user,
setId: $set->getId(),
levelId: $lessonLevel->getId(),
startDate: '2026-08-10',
targetDate: '2026-08-16',
workloadPlacement: 'middle',
));
$this->assertSame(
@ -224,12 +229,14 @@ class CreateScheduleTest extends TestCase
$setLevelRepository,
$elementRepository,
new FakeScheduleRepository,
new EvenDistributionScheduler,
))->execute(new CreateScheduleRequest(
user: $user,
setId: $set->getId(),
levelId: $milestoneLevel->getId(),
startDate: '2020-01-01',
targetDate: '2030-01-01',
workloadPlacement: 'middle',
));
$this->assertSame(
@ -240,6 +247,102 @@ class CreateScheduleTest extends TestCase
);
}
public function test_it_defaults_heavier_days_to_the_middle(): void
{
$user = $this->user();
$setRepository = new FakeSetRepository;
$setLevelRepository = new FakeSetLevelRepository;
$elementRepository = new FakeElementRepository;
$set = $setRepository->create(new CreateSetDto(
name: 'Course',
creator: $user,
));
$lessonLevel = $this->createLevel(
$setLevelRepository,
$set,
'lesson',
);
foreach (range(1, 6) as $number) {
$elementRepository->create(new CreateElementDto(
name: "Lesson {$number}",
level: $lessonLevel,
parentElement: null,
));
}
$schedule = (new CreateSchedule(
$setRepository,
$setLevelRepository,
$elementRepository,
new FakeScheduleRepository,
new EvenDistributionScheduler,
))->execute(new CreateScheduleRequest(
user: $user,
setId: $set->getId(),
levelId: $lessonLevel->getId(),
startDate: '2026-08-10',
targetDate: '2026-08-13',
workloadPlacement: null,
));
$this->assertSame(
[
'2026-08-10',
'2026-08-11',
'2026-08-11',
'2026-08-12',
'2026-08-12',
'2026-08-13',
],
array_map(function ($assignment): string {
return $assignment->getScheduledDate()->format('Y-m-d');
}, $schedule->getAssignments()),
);
}
public function test_it_rejects_an_unknown_workload_placement(): void
{
$user = $this->user();
$setRepository = new FakeSetRepository;
$setLevelRepository = new FakeSetLevelRepository;
$elementRepository = new FakeElementRepository;
$set = $setRepository->create(new CreateSetDto(
name: 'Course',
creator: $user,
));
$lessonLevel = $this->createLevel(
$setLevelRepository,
$set,
'lesson',
);
$elementRepository->create(new CreateElementDto(
name: 'Lesson 1',
level: $lessonLevel,
parentElement: null,
));
$this->expectException(BadRequestException::class);
$this->expectExceptionMessage(
'workloadPlacement must be start, middle, or end',
);
(new CreateSchedule(
$setRepository,
$setLevelRepository,
$elementRepository,
new FakeScheduleRepository,
new EvenDistributionScheduler,
))->execute(new CreateScheduleRequest(
user: $user,
setId: $set->getId(),
levelId: $lessonLevel->getId(),
startDate: '2026-08-10',
targetDate: '2026-08-13',
workloadPlacement: 'sideways',
));
}
public function test_it_rejects_an_unknown_set(): void
{
$this->expectException(NotFoundException::class);
@ -250,12 +353,14 @@ class CreateScheduleTest extends TestCase
new FakeSetLevelRepository,
new FakeElementRepository,
new FakeScheduleRepository,
new EvenDistributionScheduler,
))->execute(new CreateScheduleRequest(
user: $this->user(),
setId: 999,
levelId: 1,
startDate: '2026-08-10',
targetDate: '2026-08-12',
workloadPlacement: 'middle',
));
}
@ -276,12 +381,14 @@ class CreateScheduleTest extends TestCase
new FakeSetLevelRepository,
new FakeElementRepository,
new FakeScheduleRepository,
new EvenDistributionScheduler,
))->execute(new CreateScheduleRequest(
user: $user,
setId: $set->getId(),
levelId: null,
startDate: '2026-08-10',
targetDate: '2026-08-12',
workloadPlacement: 'middle',
));
}
@ -312,12 +419,14 @@ class CreateScheduleTest extends TestCase
$setLevelRepository,
new FakeElementRepository,
new FakeScheduleRepository,
new EvenDistributionScheduler,
))->execute(new CreateScheduleRequest(
user: $user,
setId: $bible->getId(),
levelId: $moduleLevel->getId(),
startDate: '2026-08-10',
targetDate: '2026-08-12',
workloadPlacement: 'middle',
));
}
@ -344,12 +453,14 @@ class CreateScheduleTest extends TestCase
$setLevelRepository,
new FakeElementRepository,
new FakeScheduleRepository,
new EvenDistributionScheduler,
))->execute(new CreateScheduleRequest(
user: $user,
setId: $set->getId(),
levelId: $chapterLevel->getId(),
startDate: '2026-08-10',
targetDate: '2026-08-12',
workloadPlacement: 'middle',
));
}
@ -376,12 +487,14 @@ class CreateScheduleTest extends TestCase
$setLevelRepository,
new FakeElementRepository,
new FakeScheduleRepository,
new EvenDistributionScheduler,
))->execute(new CreateScheduleRequest(
user: $user,
setId: $set->getId(),
levelId: $chapterLevel->getId(),
startDate: '2026-02-30',
targetDate: '2026-08-12',
workloadPlacement: 'middle',
));
}
@ -410,12 +523,14 @@ class CreateScheduleTest extends TestCase
$setLevelRepository,
new FakeElementRepository,
new FakeScheduleRepository,
new EvenDistributionScheduler,
))->execute(new CreateScheduleRequest(
user: $user,
setId: $set->getId(),
levelId: $chapterLevel->getId(),
startDate: '2026-08-12',
targetDate: '2026-08-10',
workloadPlacement: 'middle',
));
}