diff --git a/src/Progressable.php b/src/Progressable.php index 389fd98..bb62626 100644 --- a/src/Progressable.php +++ b/src/Progressable.php @@ -285,6 +285,45 @@ public function getEstimatedTimeRemaining(): ?int { return (int) round($remainingProgress / $rate); } + /** + * Get the estimated time remaining in seconds for the overall progress. + * + * @throws UniqueNameNotSetException + */ + public function getOverallEstimatedTimeRemaining(): ?int { + $overallProgress = $this->getOverallProgress(0); + + if ($overallProgress >= 100) { + return 0; + } + + $progressData = $this->getOverallProgressData(); + $earliestStartTime = null; + + foreach ($progressData as $localData) { + if (isset($localData['start_time'])) { + if ($earliestStartTime === null || $localData['start_time'] < $earliestStartTime) { + $earliestStartTime = $localData['start_time']; + } + } + } + + if ($earliestStartTime === null || $overallProgress <= 0) { + return null; + } + + $elapsed = Carbon::now()->timestamp - $earliestStartTime; + + if ($elapsed <= 0) { + return null; + } + + $rate = $overallProgress / $elapsed; // progress per second + $remainingProgress = 100 - $overallProgress; + + return (int) round($remainingProgress / $rate); + } + /** * Remove this instance from the overall progress calculation. * @@ -659,6 +698,7 @@ public function toArray(): array { 'is_complete' => $this->isComplete(), 'is_overall_complete' => $hasUniqueName ? $this->isOverallComplete() : null, 'estimated_time_remaining' => $hasUniqueName ? $this->getEstimatedTimeRemaining() : null, + 'overall_estimated_time_remaining' => $hasUniqueName ? $this->getOverallEstimatedTimeRemaining() : null, 'message' => $this->getStatusMessage(), 'metadata' => $this->getMetadata(), 'total_steps' => $this->getTotalSteps(), diff --git a/tests/EtaTest.php b/tests/EtaTest.php index e13e1e1..87f9bcd 100644 --- a/tests/EtaTest.php +++ b/tests/EtaTest.php @@ -137,4 +137,69 @@ public function test_start_time_persistence(): void { $this->assertEquals(40, $obj2->getEstimatedTimeRemaining()); } + + public function test_overall_eta_is_null_initially(): void { + $this->setOverallUniqueName('test_overall_eta_init_'.$this->testId); + if (method_exists($this, 'getOverallEstimatedTimeRemaining')) { + $this->assertNull($this->getOverallEstimatedTimeRemaining()); + } else { + $this->markTestSkipped('getOverallEstimatedTimeRemaining not implemented yet'); + } + } + + public function test_overall_eta_calculation(): void { + if (! method_exists($this, 'getOverallEstimatedTimeRemaining')) { + $this->markTestSkipped('getOverallEstimatedTimeRemaining not implemented yet'); + } + + Carbon::setTestNow(Carbon::now()); + $uniqueName = 'test_overall_eta_calc_'.$this->testId; + + $this->setOverallUniqueName($uniqueName); + $this->setLocalProgress(0); // Start time set at T0 + + // Create new instance simulating another process or request + $obj2 = new class { + use Progressable; + }; + $obj2->setOverallUniqueName($uniqueName); + + // Advance time by 10 seconds + Carbon::setTestNow(Carbon::now()->addSeconds(10)); + + // obj1 progress to 10% + $this->setLocalProgress(10); + + // obj2 progress to 10% + $obj2->setLocalProgress(10); + + // Overall progress = 10%. Elapsed = 10s. Rate = 1% / s. + // Remaining 90%. ETA = 90s. + $this->assertEquals(90, $this->getOverallEstimatedTimeRemaining()); + $this->assertEquals(90, $obj2->getOverallEstimatedTimeRemaining()); + + // Advance time by another 10 seconds (total 20s) + Carbon::setTestNow(Carbon::now()->addSeconds(10)); + + // obj1 progress to 50% + $this->setLocalProgress(50); + + // obj2 progress to 50% + $obj2->setLocalProgress(50); + + // Overall progress = 50%. Elapsed = 20s. Rate = 2.5% / s. + // Remaining 50%. ETA = 50 / 2.5 = 20s. + $this->assertEquals(20, $this->getOverallEstimatedTimeRemaining()); + $this->assertEquals(20, $obj2->getOverallEstimatedTimeRemaining()); + } + + public function test_overall_eta_is_zero_when_complete(): void { + if (! method_exists($this, 'getOverallEstimatedTimeRemaining')) { + $this->markTestSkipped('getOverallEstimatedTimeRemaining not implemented yet'); + } + + $this->setOverallUniqueName('test_overall_eta_complete_'.$this->testId); + $this->setLocalProgress(100); + $this->assertEquals(0, $this->getOverallEstimatedTimeRemaining()); + } } diff --git a/tests/ProgressableTest.php b/tests/ProgressableTest.php index 545b26b..ef71eab 100644 --- a/tests/ProgressableTest.php +++ b/tests/ProgressableTest.php @@ -568,6 +568,7 @@ public function test_to_array(): void { $this->assertFalse($array['is_complete']); $this->assertFalse($array['is_overall_complete']); $this->assertNull($array['estimated_time_remaining']); + $this->assertNull($array['overall_estimated_time_remaining']); $this->assertEquals('Halfway there', $array['message']); $this->assertEquals(['foo' => 'bar'], $array['metadata']); $this->assertEquals(10, $array['total_steps']); @@ -587,6 +588,7 @@ public function test_to_array_without_unique_name(): void { 'is_complete' => false, 'is_overall_complete' => null, 'estimated_time_remaining' => null, + 'overall_estimated_time_remaining' => null, 'message' => 'Halfway there', 'metadata' => ['foo' => 'bar'], 'total_steps' => 10,