SprintWeekTest.php 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Domain;
  4. use App\Domain\SprintWeek;
  5. use InvalidArgumentException;
  6. use PHPUnit\Framework\TestCase;
  7. /**
  8. * Phase 12: SprintWeek's weekday helpers. Pure logic — no DB.
  9. */
  10. final class SprintWeekTest extends TestCase
  11. {
  12. public function testPopcountCoversAllFiveBits(): void
  13. {
  14. $this->assertSame(0, SprintWeek::popcount(0));
  15. $this->assertSame(5, SprintWeek::popcount(31));
  16. $this->assertSame(1, SprintWeek::popcount(1));
  17. $this->assertSame(4, SprintWeek::popcount(15));
  18. // bits outside the 5-day range are clamped away
  19. $this->assertSame(5, SprintWeek::popcount(0xFFFF));
  20. }
  21. public function testMaskToDaysReturnsCanonicalOrder(): void
  22. {
  23. $this->assertSame(['Mo', 'Di', 'Mi', 'Do', 'Fr'], SprintWeek::maskToDays(31));
  24. $this->assertSame(['Mo', 'Fr'], SprintWeek::maskToDays(0b10001));
  25. $this->assertSame([], SprintWeek::maskToDays(0));
  26. }
  27. public function testDaysToMaskRoundTripsBothDirections(): void
  28. {
  29. foreach ([0, 1, 2, 7, 15, 17, 31] as $m) {
  30. $days = SprintWeek::maskToDays($m);
  31. $this->assertSame($m, SprintWeek::daysToMask($days), "round-trip for mask={$m}");
  32. }
  33. }
  34. public function testDaysToMaskRejectsUnknownLabel(): void
  35. {
  36. $this->expectException(InvalidArgumentException::class);
  37. SprintWeek::daysToMask(['Mo', 'Sa']); // Sa not in Mo–Fr set
  38. }
  39. public function testDaysToMaskRejectsDuplicates(): void
  40. {
  41. $this->expectException(InvalidArgumentException::class);
  42. SprintWeek::daysToMask(['Mo', 'Mo']);
  43. }
  44. public function testHasDayMatchesMaskBit(): void
  45. {
  46. $w = new SprintWeek(
  47. id: 1, sprintId: 1, sortOrder: 1, isoWeek: 1,
  48. startDate: '2026-01-05', maxWorkingDays: 3.0,
  49. activeDaysMask: 0b00111, // Mo Di Mi
  50. );
  51. $this->assertTrue($w->hasDay('Mo'));
  52. $this->assertTrue($w->hasDay('Mi'));
  53. $this->assertFalse($w->hasDay('Do'));
  54. $this->assertFalse($w->hasDay('Fr'));
  55. // Unknown label is a miss, not an error.
  56. $this->assertFalse($w->hasDay('Sa'));
  57. }
  58. public function testWithMaskRecomputesMaxWorkingDays(): void
  59. {
  60. $w = new SprintWeek(
  61. id: 1, sprintId: 1, sortOrder: 1, isoWeek: 1,
  62. startDate: '2026-01-05', maxWorkingDays: 5.0,
  63. activeDaysMask: 31,
  64. );
  65. $dropped = $w->withMask(0b01111); // Mo Di Mi Do
  66. $this->assertSame(15, $dropped->activeDaysMask);
  67. $this->assertSame(4.0, $dropped->maxWorkingDays);
  68. $this->assertSame(['Mo', 'Di', 'Mi', 'Do'], $dropped->activeDays());
  69. // Bits outside Mo–Fr are trimmed.
  70. $this->assertSame(31, $w->withMask(0xFF)->activeDaysMask);
  71. }
  72. public function testAuditSnapshotIncludesMask(): void
  73. {
  74. $w = new SprintWeek(
  75. id: 77, sprintId: 9, sortOrder: 2, isoWeek: 8,
  76. startDate: '2026-02-16', maxWorkingDays: 2.0,
  77. activeDaysMask: 0b00011,
  78. );
  79. $snap = $w->toAuditSnapshot();
  80. $this->assertSame(0b00011, $snap['active_days_mask']);
  81. $this->assertSame(2.0, $snap['max_working_days']);
  82. }
  83. }