SprintWeekTest.php 3.6 KB

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