Skip to content

Commit e78b339

Browse files
author
Greg Bowler
committed
Complete Loop tests
1 parent 65cbeee commit e78b339

4 files changed

Lines changed: 130 additions & 134 deletions

File tree

src/Loop.php

Lines changed: 25 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
namespace Gt\Async;
33

44
use Gt\Async\Timer\Timer;
5+
use Gt\Async\Timer\TimerOrder;
56

67
/**
78
* The core event loop class, used to dispatch all events via different added
@@ -32,12 +33,12 @@ public function setSleepFunction(callable $sleepFunction):void {
3233
$this->sleepFunction = $sleepFunction;
3334
}
3435

35-
public function run():void {
36+
public function run(bool $forever = true):void {
3637
do {
3738
$numTriggered = $this->triggerNextTimers();
3839
$this->triggerCount += $numTriggered;
3940
}
40-
while($numTriggered > 0);
41+
while($numTriggered > 0 && $forever);
4142
}
4243

4344
public function getTriggerCount():int {
@@ -57,62 +58,44 @@ public function waitUntil(float $waitUntilEpoch):void {
5758
);
5859
}
5960

60-
// TODO: The epochList is a perfect candidate for one of SPL's Iterators.
61-
// Probably the MultipleIterator...
62-
/** @return array[] */
63-
public function getTimerOrder():array {
64-
$epochList = [];
65-
66-
// Create a list of all timers that have a next run time.
67-
foreach($this->timerList as $timer) {
68-
if($epoch = $timer->getNextRunTime()) {
69-
$epochList[] = [$epoch, $timer];
70-
}
71-
}
72-
73-
// Sort the epoch list so that they are in order of next run time.
74-
usort(
75-
$epochList,
76-
fn($a, $b) => $a[0] < $b[0] ? -1 : 1
77-
);
78-
79-
return $epochList;
80-
}
81-
82-
/** return array[] */
83-
public function getReadyTimers(array $epochList):array {
84-
$now = microtime(true);
85-
$epochList = array_filter($epochList, fn($a) => $a[0] <= $now);
86-
return $epochList;
87-
}
61+
// /** return array[] */
62+
// public function getReadyTimers(array $epochList):array {
63+
// $now = microtime(true);
64+
// $epochList = array_filter($epochList, fn($a) => $a[0] <= $now);
65+
// return $epochList;
66+
// }
8867

8968
private function triggerNextTimers():int {
90-
$epochList = $this->getTimerOrder();
69+
$timerOrder = new TimerOrder($this->timerList);
70+
9171
// If there are no more timers to run, return early.
92-
if(empty($epochList)) {
72+
if(count($timerOrder) === 0) {
9373
return 0;
9474
}
9575

9676
// Wait until the first epoch is due, then trigger the timer.
97-
$this->waitUntil($epochList[0][0]);
98-
$this->trigger($epochList[0][1]);
77+
$this->waitUntil($timerOrder->getCurrentEpoch());
78+
$this->trigger($timerOrder->getCurrentTimer());
9979

10080
// Triggering the timer may have caused time to pass so that
10181
// other timers are now due.
102-
array_shift($epochList);
103-
$readyList = $this->getReadyTimers($epochList);
104-
$this->executeTimers($readyList);
105-
return 1 + count($readyList);
82+
$timerOrder->next();
83+
$timerOrderReady = $timerOrder->subset();
84+
$this->executeTimers($timerOrderReady);
85+
86+
// This function will always execute at least 1 timer, it will always wait for
87+
// the next one to trigger, but could have triggered more during the wait for
88+
// the first Timer's execution.
89+
return 1 + count($timerOrderReady);
10690
}
10791

10892
private function trigger(Timer $timer):void {
10993
$timer->tick();
11094
}
11195

112-
/** @param array[] $epochList [$epoch, $timer] */
113-
private function executeTimers(array $epochList):void {
114-
foreach($epochList as $timerThing) {
115-
$this->trigger($timerThing[1]);
96+
private function executeTimers(TimerOrder $timerOrder):void {
97+
foreach($timerOrder as $item) {
98+
$this->trigger($item["timer"]);
11699
}
117100
}
118101
}

src/Timer/Timer.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ public function __construct() {
2323
// $this->callbackList[] = $callback;
2424
// }
2525

26+
public function isScheduled():bool {
27+
return !empty($this->triggerTimeQueue);
28+
}
29+
2630
public function getNextRunTime():?float {
2731
return $this->triggerTimeQueue[0] ?? null;
2832
}

src/Timer/TimerOrder.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
namespace Gt\Async\Timer;
3+
4+
use ArrayIterator;
5+
use Countable;
6+
use MultipleIterator;
7+
8+
class TimerOrder extends MultipleIterator implements Countable {
9+
/** @var Timer[] $timerList */
10+
private array $timerList;
11+
12+
/** @param Timer[] $timerList */
13+
public function __construct(array $timerList) {
14+
$timerList = array_filter(
15+
$timerList,
16+
fn(Timer $t) => $t->isScheduled()
17+
);
18+
usort(
19+
$timerList,
20+
fn(Timer $a, Timer $b) =>
21+
$a->getNextRunTime() < $b->getNextRunTime() ? -1 : 1
22+
);
23+
$epochList = array_map(
24+
fn(Timer $t) => $t->getNextRunTime(),
25+
$timerList
26+
);
27+
$this->timerList = $timerList;
28+
29+
parent::__construct(MultipleIterator::MIT_KEYS_ASSOC);
30+
$this->attachIterator(
31+
new ArrayIterator($timerList),
32+
"timer"
33+
);
34+
$this->attachIterator(
35+
new ArrayIterator($epochList),
36+
"epoch"
37+
);
38+
}
39+
40+
public function count():int {
41+
return count($this->timerList);
42+
}
43+
44+
public function getCurrentTimer():Timer {
45+
return $this->current()["timer"];
46+
}
47+
48+
public function getCurrentEpoch():float {
49+
$current = $this->current();
50+
return $current["epoch"];
51+
}
52+
53+
public function subset():TimerOrder {
54+
$timerArray = [];
55+
while($this->valid()) {
56+
$timerArray[] = $this->current()["timer"];
57+
$this->next();
58+
}
59+
60+
return new TimerOrder($timerArray);
61+
}
62+
}

test/phpunit/LoopTest.php

Lines changed: 39 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,10 @@ public function testWaitUntilNegative() {
4949
public function testRunWithTimer() {
5050
$epoch = microtime(true);
5151
$timer = self::createMock(Timer::class);
52+
$timer->method("isScheduled")
53+
->willReturn(true, false);
5254
$timer->method("getNextRunTime")
53-
->willReturn(
54-
$epoch + 1,
55-
null
56-
);
55+
->willReturn($epoch + 1, null);
5756

5857
$sut = new Loop();
5958
$sut->setSleepFunction(function() {});
@@ -66,11 +65,13 @@ public function testRunWithTimer() {
6665
public function testRunWithTimerMultiple() {
6766
$epoch = microtime(true);
6867
$timer = self::createMock(Timer::class);
68+
$timer->method("isScheduled")
69+
->willReturn(true, true, true, false);
6970
$timer->method("getNextRunTime")
7071
->willReturn(
71-
$epoch + 1,
72-
$epoch + 2,
73-
$epoch + 3,
72+
$epoch + 100,
73+
$epoch + 200,
74+
$epoch + 300,
7475
null
7576
);
7677

@@ -82,107 +83,53 @@ public function testRunWithTimerMultiple() {
8283
self::assertEquals(3, $sut->getTriggerCount());
8384
}
8485

85-
public function testRunWithTimerNoNextRunTime() {
86-
$timer = self::createMock(Timer::class);
87-
$timer->method("getNextRunTime")
88-
->willReturn(null);
89-
90-
$sut = new Loop();
91-
$sut->addTimer($timer);
92-
$sut->run();
93-
94-
self::assertEquals(0, $sut->getTriggerCount());
95-
}
96-
97-
public function testGetTimerList() {
98-
$epoch = microtime(true);
99-
100-
$timer1 = self::createMock(Timer::class);
101-
$timer1->method("getNextRunTime")
102-
->willReturn($epoch + 1, null);
103-
$timer2 = self::createMock(Timer::class);
104-
$timer2->method("getNextRunTime")
105-
->willReturn($epoch + 2, null);
106-
107-
$sut = new Loop();
108-
$sut->setSleepFunction(function() {});
109-
$sut->addTimer($timer1);
110-
$sut->addTimer($timer2);
111-
112-
$timerOrder = $sut->getTimerOrder();
113-
self::assertSame($timer1, $timerOrder[0][1]);
114-
self::assertSame($timer2, $timerOrder[1][1]);
115-
}
116-
117-
/**
118-
* The only difference here to the test above is that timer1 is set to
119-
* be due after timer2, so the order of the timers will be different.
120-
*/
121-
public function testGetTimerListOutOfOrder() {
86+
public function testRunWithTimersConcurrent() {
12287
$epoch = microtime(true);
88+
$timerArray = [];
89+
$numExpectedDueTimers = 0;
12390

124-
$timer1 = self::createMock(Timer::class);
125-
$timer1->method("getNextRunTime")
126-
->willReturn($epoch + 2, null);
127-
$timer2 = self::createMock(Timer::class);
128-
$timer2->method("getNextRunTime")
129-
->willReturn($epoch + 1, null);
130-
131-
$sut = new Loop();
132-
$sut->setSleepFunction(function() {});
133-
$sut->addTimer($timer1);
134-
$sut->addTimer($timer2);
135-
136-
$timerOrder = $sut->getTimerOrder();
137-
self::assertSame($timer2, $timerOrder[0][1]);
138-
self::assertSame($timer1, $timerOrder[1][1]);
139-
}
140-
141-
public function testGetTimerListManyReadyManyFuture() {
142-
$epoch = microtime(true);
91+
for($i = 0; $i < 100; $i++) {
92+
// Randomise the epoch by +/- 100 seconds (roughly half of the timers will be due)
93+
$rand = rand(-100, 100);
94+
$timerEpoch = $epoch + $rand;
14395

144-
$timerList = [];
96+
$expectedToBeDue = $timerEpoch <= $epoch;
97+
if($expectedToBeDue) {
98+
$numExpectedDueTimers++;
99+
}
145100

146-
for($i = 0; $i < 100; $i++) {
147-
// Offset the epoch by a random amount between -10 and +10 seconds.
148-
$rand = rand(-1000, 1000) / 100;
149101
$timer = self::createMock(Timer::class);
102+
$timer->method("isScheduled")
103+
->willReturn($expectedToBeDue);
150104
$timer->method("getNextRunTime")
151-
->willReturn($epoch + $rand);
152-
$timerList[] = $timer;
105+
->willReturn($timerEpoch);
106+
$timerArray[] = $timer;
153107
}
154108

155109
$sut = new Loop();
156-
$sut->setSleepFunction(function() {});
157-
158-
foreach($timerList as $timer) {
110+
$sut->setSleepFunction(function(){});
111+
foreach($timerArray as $timer) {
159112
$sut->addTimer($timer);
160113
}
161114

162-
$timerOrder = $sut->getTimerOrder();
163-
$earliest = 0;
164-
foreach($timerOrder as $timer) {
165-
self::assertGreaterThanOrEqual($earliest, $timer[0]);
166-
$earliest = $timer[0];
167-
}
115+
$sut->run(false);
116+
self::assertEquals(
117+
$numExpectedDueTimers,
118+
$sut->getTriggerCount()
119+
);
168120
}
169121

170-
public function testGetReadyTimers() {
171-
$epoch = microtime(true);
122+
public function testRunWithTimerNoNextRunTime() {
123+
$timer = self::createMock(Timer::class);
124+
$timer->method("isScheduled")
125+
->willReturn(false);
126+
$timer->method("getNextRunTime")
127+
->willReturn(null);
172128

173-
$timerFuture = self::createMock(Timer::class);
174-
$timerFuture->method("getNextRunTime")
175-
->willReturn($epoch + 100);
176-
$timerPast = self::createMock(Timer::class);
177-
$timerPast->method("getNextRunTime")
178-
->willReturn($epoch - 100);
179129
$sut = new Loop();
180-
$sut->addTimer($timerFuture);
181-
$sut->addTimer($timerPast);
182-
$allTimers = $sut->getTimerOrder();
183-
$readyTimers = $sut->getReadyTimers($allTimers);
130+
$sut->addTimer($timer);
131+
$sut->run();
184132

185-
self::assertCount(1, $readyTimers);
186-
self::assertSame($timerPast, $readyTimers[0][1]);
133+
self::assertEquals(0, $sut->getTriggerCount());
187134
}
188135
}

0 commit comments

Comments
 (0)