diff --git a/README.md b/README.md index 03f57d7..e317c8e 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ class MyFirstTask | `getOverallProgress(?int $precision)` | Get average progress of all instances | | `resetOverallProgress()` | Clear all progress data for the group | | `removeLocalFromOverall()` | Remove this instance from overall calculation | +| `getEstimatedTimeRemaining()` | Get estimated time remaining in seconds for this instance | +| `getOverallEstimatedTimeRemaining()` | Get estimated time remaining in seconds for the overall progress | #### Status Checks diff --git a/src/Progressable.php b/src/Progressable.php index f3cd5c0..4fe078d 100644 --- a/src/Progressable.php +++ b/src/Progressable.php @@ -267,6 +267,43 @@ public function isOverallComplete(): bool { return true; } + /** + * Get the estimated time remaining in seconds for the overall progress. + */ + public function getOverallEstimatedTimeRemaining(): ?int { + $overallProgress = $this->getOverallProgress(); + + if ($overallProgress >= 100) { + return 0; + } + + $progressData = $this->getOverallProgressData(); + + if (empty($progressData)) { + return null; + } + + $startTimes = array_filter(array_column($progressData, 'start_time'), function ($time) { + return $time !== null; + }); + + if (empty($startTimes) || $overallProgress <= 0) { + return null; + } + + $minStartTime = min($startTimes); + $elapsed = Carbon::now()->timestamp - $minStartTime; + + if ($elapsed <= 0) { + return null; + } + + $rate = $overallProgress / $elapsed; + $remainingProgress = 100 - $overallProgress; + + return (int) round($remainingProgress / $rate); + } + /** * Get the estimated time remaining in seconds. */ @@ -671,6 +708,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/IsOverallCompleteBugTest.php b/tests/IsOverallCompleteBugTest.php index 601ad79..f3d97ab 100644 --- a/tests/IsOverallCompleteBugTest.php +++ b/tests/IsOverallCompleteBugTest.php @@ -1,24 +1,22 @@ setOverallUniqueName('test-overall-complete-bug'); $a->setLocalKey('a'); $a->setLocalProgress(100); - $b = new DummyProgressableOverallComplete(); + $b = new DummyProgressableOverallComplete; $b->setOverallUniqueName('test-overall-complete-bug'); $b->setLocalKey('b'); $b->setLocalProgress(99.4); diff --git a/tests/OverallEtaTest.php b/tests/OverallEtaTest.php new file mode 100644 index 0000000..033ee83 --- /dev/null +++ b/tests/OverallEtaTest.php @@ -0,0 +1,77 @@ +testId = uniqid('test_', true); + + // Reset properties + $this->progress = 0; + unset($this->overallUniqueName); + } + + public function test_overall_eta_is_null_initially(): void { + $this->setOverallUniqueName('test_overall_eta_init_'.$this->testId); + $this->assertNull($this->getOverallEstimatedTimeRemaining()); + } + + public function test_overall_eta_calculation(): void { + Carbon::setTestNow(Carbon::now()); + + $uniqueName = 'test_overall_eta_calc_'.$this->testId; + $this->setOverallUniqueName($uniqueName); + + // First process starts at T0 + $this->setLocalKey('process_1'); + $this->setLocalProgress(0); + + // Second process starts at T+5s + Carbon::setTestNow(Carbon::now()->addSeconds(5)); + + $obj2 = new class { + use Progressable; + }; + $obj2->setOverallUniqueName($uniqueName); + $obj2->setLocalKey('process_2'); + $obj2->setLocalProgress(0); + + // Advance to T+10s + Carbon::setTestNow(Carbon::now()->addSeconds(5)); + + // Set process 1 to 20% and process 2 to 0% -> overall = 10% + // Min start time is T0, so elapsed is 10s. + // Rate = 10% / 10s = 1% per second. + // Remaining = 90%. ETA = 90s. + $this->setLocalProgress(20); + + $this->assertEquals(90, $this->getOverallEstimatedTimeRemaining()); + $this->assertEquals(90, $obj2->getOverallEstimatedTimeRemaining()); + } + + public function test_overall_eta_is_zero_when_complete(): void { + $uniqueName = 'test_overall_eta_complete_'.$this->testId; + + $this->setOverallUniqueName($uniqueName); + $this->setLocalKey('process_1'); + $this->setLocalProgress(100); + + $obj2 = new class { + use Progressable; + }; + $obj2->setOverallUniqueName($uniqueName); + $obj2->setLocalKey('process_2'); + $obj2->setLocalProgress(100); + + $this->assertEquals(0, $this->getOverallEstimatedTimeRemaining()); + } +} diff --git a/tests/ProgressableTest.php b/tests/ProgressableTest.php index 545b26b..5d3bb30 100644 --- a/tests/ProgressableTest.php +++ b/tests/ProgressableTest.php @@ -587,6 +587,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,