diff --git a/src/Notification/OnNextNotification.php b/src/Notification/OnNextNotification.php index 57f5f946..17a1d5df 100644 --- a/src/Notification/OnNextNotification.php +++ b/src/Notification/OnNextNotification.php @@ -4,6 +4,7 @@ namespace Rx\Notification; +use Rx\Observable; use Rx\ObserverInterface; use Rx\Notification; @@ -30,6 +31,11 @@ protected function doAccept($onNext, $onError, $onCompleted) public function __toString(): string { + if ($this->value instanceof Observable) { + $messages = materializeObservable($this->value); + return '[' . implode(', ', $messages) . ']'; + } + return 'OnNext(' . json_encode($this->value) . ')'; } diff --git a/src/Testing/ColdObservable.php b/src/Testing/ColdObservable.php index fce41240..df536525 100644 --- a/src/Testing/ColdObservable.php +++ b/src/Testing/ColdObservable.php @@ -26,14 +26,13 @@ public function __construct(TestScheduler $scheduler, array $messages = []) protected function _subscribe(ObserverInterface $observer): DisposableInterface { + $currentObservable = $this; + $disposable = new CompositeDisposable(); + $scheduler = $this->scheduler; + $isDisposed = false; $this->subscriptions[] = new Subscription($this->scheduler->getClock()); $index = count($this->subscriptions) - 1; - $currentObservable = $this; - $disposable = new CompositeDisposable(); - $scheduler = $this->scheduler; - $isDisposed = false; - foreach ($this->messages as $message) { $notification = $message->getValue(); $time = $message->getTime(); @@ -53,7 +52,8 @@ protected function _subscribe(ObserverInterface $observer): DisposableInterface $subscriptions = &$this->subscriptions; return new CallbackDisposable(function () use (&$currentObservable, $index, $observer, $scheduler, &$subscriptions, &$isDisposed) { - $isDisposed = true; + $isDisposed = true; + $subscriptions[$index] = new Subscription($subscriptions[$index]->getSubscribed(), $scheduler->getClock()); }); diff --git a/src/Testing/HotObservable.php b/src/Testing/HotObservable.php index a86bdfed..bd0b2a26 100644 --- a/src/Testing/HotObservable.php +++ b/src/Testing/HotObservable.php @@ -46,15 +46,12 @@ public function __construct(TestScheduler $scheduler, array $messages) protected function _subscribe(ObserverInterface $observer): DisposableInterface { - $currentObservable = $this; - + $currentObservable = $this; $this->observers[] = $observer; + $subscriptions = &$this->subscriptions; $this->subscriptions[] = new Subscription($this->scheduler->getClock()); - - $subscriptions = &$this->subscriptions; - - $index = count($this->subscriptions) - 1; - $scheduler = $this->scheduler; + $index = count($this->subscriptions) - 1; + $scheduler = $this->scheduler; return new CallbackDisposable(function () use (&$currentObservable, $index, $observer, $scheduler, &$subscriptions) { $currentObservable->removeObserver($observer); diff --git a/src/Testing/MockObserver.php b/src/Testing/MockObserver.php index a30a050c..63a3eee7 100644 --- a/src/Testing/MockObserver.php +++ b/src/Testing/MockObserver.php @@ -16,16 +16,18 @@ class MockObserver implements ObserverInterface { private $scheduler; private $messages = []; + private $startTime = 0; - public function __construct(TestScheduler $scheduler) + public function __construct(TestScheduler $scheduler, int $startTime = 0) { $this->scheduler = $scheduler; + $this->startTime = $startTime; } public function onNext($value) { $this->messages[] = new Recorded( - $this->scheduler->getClock(), + $this->scheduler->getClock() - $this->startTime, new OnNextNotification($value) ); } @@ -33,7 +35,7 @@ public function onNext($value) public function onError(\Throwable $error) { $this->messages[] = new Recorded( - $this->scheduler->getClock(), + $this->scheduler->getClock() - $this->startTime, new OnErrorNotification($error) ); } @@ -41,7 +43,7 @@ public function onError(\Throwable $error) public function onCompleted() { $this->messages[] = new Recorded( - $this->scheduler->getClock(), + $this->scheduler->getClock() - $this->startTime, new OnCompletedNotification() ); } diff --git a/src/Testing/TestScheduler.php b/src/Testing/TestScheduler.php index 4d7312b6..d9838127 100644 --- a/src/Testing/TestScheduler.php +++ b/src/Testing/TestScheduler.php @@ -7,7 +7,6 @@ use Rx\Disposable\EmptyDisposable; use Rx\DisposableInterface; use Rx\ObserverInterface; -use Rx\Scheduler; use Rx\Scheduler\VirtualTimeScheduler; class TestScheduler extends VirtualTimeScheduler diff --git a/test/Rx/Functional/FunctionalTestCase.php b/test/Rx/Functional/FunctionalTestCase.php index db7771b2..040f6fc3 100644 --- a/test/Rx/Functional/FunctionalTestCase.php +++ b/test/Rx/Functional/FunctionalTestCase.php @@ -19,11 +19,19 @@ abstract class FunctionalTestCase extends TestCase /** @var TestScheduler */ protected $scheduler; + /** @var TestScheduler */ + static protected $globalScheduler; + const TIME_FACTOR = 10; public function setup() { $this->scheduler = $this->createTestScheduler(); + self::$globalScheduler = $this->scheduler; + } + + static function getScheduler() { + return self::$globalScheduler; } /** @@ -320,7 +328,7 @@ public function toBe(string $expected, array $values = [], string $errorMessage { $error = $errorMessage ? new \Exception($errorMessage) : null; - $this->assertEquals( + $this->assertMessages( $this->convertMarblesToMessages($expected, $values, $error, 200), $this->messages ); @@ -342,7 +350,7 @@ public function __construct(array $subscriptions) public function toBe(string $subscriptionsMarbles) { - $this->assertEquals( + $this->assertMessages( $this->convertMarblesToSubscriptions($subscriptionsMarbles, 200), $this->subscriptions ); diff --git a/test/Rx/Functional/MarbleTest.php b/test/Rx/Functional/MarbleTest.php index 28330aa5..59c395c1 100644 --- a/test/Rx/Functional/MarbleTest.php +++ b/test/Rx/Functional/MarbleTest.php @@ -308,4 +308,24 @@ public function testCountMarble() $this->expectObservable($e1->count())->toBe($expected, ['x' => 3]); $this->expectSubscriptions($e1->getSubscriptions())->toBe($subs); } + + public function testGroupMarble() + { + $hot = '--1---2---3---4---5---|'; + $expected = '--x---y---------------|'; + $coldx = '1-------3-------5---|'; + $coldy = '2-------4-------|'; + + $e1 = $this->createHot($hot); + $x = $this->createCold($coldx); + $y = $this->createCold($coldy); + + $expectedValues = ['x' => $x, 'y' => $y]; + + $source = $e1->groupBy(function ($val) { + return (int)$val % 2; + }); + + $this->expectObservable($source)->toBe($expected, $expectedValues); + } } diff --git a/test/Rx/Testing/RecordedTest.php b/test/Rx/Testing/RecordedTest.php index 10c88443..ff667049 100644 --- a/test/Rx/Testing/RecordedTest.php +++ b/test/Rx/Testing/RecordedTest.php @@ -4,24 +4,214 @@ namespace Rx\Testing; -use Rx\TestCase; +use Rx\Functional\FunctionalTestCase; +use Rx\Observable; -class RecordedTest extends TestCase +class RecordedTest extends FunctionalTestCase { - public function testRecordedWillUseStrictCompareIfNoEqualsMethod() + + /** + * @test + */ + public function compare_basic_types() + { + $r1 = new Recorded(100, 42); + $r2 = new Recorded(100, 42); + $this->assertTrue($r1->equals($r2)); + + $r3 = new Recorded(100, 42); + $r4 = new Recorded(150, 42); + $this->assertFalse($r3->equals($r4)); + + $r5 = new Recorded(100, 42); + $r6 = new Recorded(100, 24); + $this->assertFalse($r5->equals($r6)); + } + + /** + * @test + */ + public function compare_cold_observables() + { + $records1 = onNext(100, $this->createColdObservable([ + onNext(150, 1), + onNext(200, 2), + onNext(250, 3), + onCompleted(300), + ])); + $records2 = onNext(100, $this->createColdObservable([ + onNext(150, 1), + onNext(200, 2), + onNext(250, 3), + onCompleted(300), + ])); + + $this->assertMessages([$records1], [$records2]); + $this->assertTrue($records1->equals($records2)); + } + + /** + * @test + */ + public function compare_cold_observables_not_equal() + { + $records1 = onNext(100, $this->createColdObservable([ + onNext(150, 1), + onNext(200, 42), // this is wrong + onNext(250, 3), + onCompleted(300), + ])); + $records2 = onNext(100, $this->createColdObservable([ + onNext(150, 1), + onNext(200, 2), + onNext(250, 3), + onCompleted(300), + ])); + + $this->assertMessagesNotEqual([$records1], [$records2]); + $this->assertFalse($records1->equals($records2)); + } + + /** + * @test + */ + public function compare_with_range_cold_observable() + { + $records1 = onNext(100, Observable::range(1, 3, $this->scheduler)); + $records2 = onNext(100, $this->createColdObservable([ + onNext(1, 1), + onNext(2, 2), + onNext(3, 3), + onCompleted(4), + ])); + + $this->assertMessages([$records1], [$records2]); + } + + /** + * @test + */ + public function compare_with_delayed_range_cold_observable() + { + $records1 = onNext(100, $this->createColdObservable([ + onNext(50, 1), + onNext(100, 2), + onNext(150, 3), + onCompleted(200) + ])->delay(100, $this->scheduler)); + + $records2 = onNext(100, $this->createColdObservable([ + onNext(150, 1), + onNext(200, 2), + onNext(250, 3), + onCompleted(300), + ])); + + $this->assertMessages([$records1], [$records2]); + } + + /** + * @test + */ + public function observables_at_different_time_with_same_records_arent_equal() + { + $records1 = onNext(50, $this->createColdObservable([ + onNext(50, 1), + onNext(100, 2), + ])); + $records2 = onNext(100, $this->createColdObservable([ + onNext(50, 1), + onNext(100, 2), + ])); + + $this->assertFalse($records1->equals($records2)); + $this->assertEquals('[OnNext(1)@50, OnNext(2)@100]@50', $records1->__toString()); + } + + /** + * @test + */ + public function observables_with_inner_records_at_different_time_arent_equal() + { + $records1 = onNext(100, $this->createColdObservable([ + onNext(50, 1), + onNext(150, 2), + ])); + $records2 = onNext(100, $this->createColdObservable([ + onNext(50, 1), + onNext(100, 2), + ])); + + $this->assertFalse($records1->equals($records2)); + $this->assertEquals('[OnNext(1)@50, OnNext(2)@150]@100', $records1->__toString()); + } + + /** + * @test + */ + public function observables_with_more_nested_inner_observables() { - $recorded1 = new Recorded(1, 5); - $recorded2 = new Recorded(1, "5"); - $recorded3 = new Recorded(1, 5); + $records1 = onNext(100, $this->createColdObservable([ + onNext(50, 1), + onNext(100, 2), + onNext(150, $this->createColdObservable([ + onNext(10, 3), + onNext(20, 4), + onNext(30, $this->createColdObservable([ + onNext(10, 5), + onNext(20, 6), + ])), + ])->delay(100, $this->scheduler)), + ])); + $records2 = onNext(100, $this->createColdObservable([ + onNext(50, 1), + onNext(100, 2), + onNext(150, $this->createColdObservable([ + onNext(110, 3), + onNext(120, 4), + onNext(130, $this->createColdObservable([ + onNext(10, 5), + onNext(20, 6), + ])), + ])), + ])); - $this->assertFalse($recorded1->equals($recorded2)); - $this->assertTrue($recorded1->equals($recorded3)); + $this->assertMessages([$records1], [$records2]); + $this->assertTrue($records1->equals($records2)); } - public function testRecordedToString() + /** + * @test + */ + public function observables_with_difference_in_nested_inner_observables() { - $recorded = new Recorded(1, 5); + $records1 = onNext(100, $this->createColdObservable([ + onNext(50, 1), + onNext(100, 2), + onNext(150, $this->createColdObservable([ + onNext(10, 3), + onNext(20, 4), + onNext(30, $this->createColdObservable([ + onNext(10, 5), + onNext(20, 6), + ])), + ])->delay(100, $this->scheduler)), + ])); + $records2 = onNext(100, $this->createColdObservable([ + onNext(50, 1), + onNext(100, 2), + onNext(150, $this->createColdObservable([ + onNext(110, 3), + onNext(120, 4), + onNext(130, $this->createColdObservable([ + onNext(10, 5), + onNext(20, 42), // this is wrong + ])), + ])), + ])); - $this->assertEquals("5@1", $recorded->__toString()); + $this->scheduler->start(); + $this->assertFalse($records1->equals($records2)); } + } diff --git a/test/helper-functions.php b/test/helper-functions.php index 81ae91b7..1bdf6d4d 100644 --- a/test/helper-functions.php +++ b/test/helper-functions.php @@ -2,28 +2,51 @@ declare(strict_types = 1); +use Rx\Observable; +use Rx\Testing\MockObserver; use Rx\Testing\Recorded; use Rx\Testing\Subscription; +use Rx\Functional\FunctionalTestCase; use Rx\Notification\OnCompletedNotification; use Rx\Notification\OnErrorNotification; use Rx\Notification\OnNextNotification; -function onError(int $dueTime, $error, callable $comparer = null) { +function onError(int $dueTime, $error, callable $comparer = null) +{ return new Recorded($dueTime, new OnErrorNotification($error), $comparer); } -function onNext(int $dueTime, $value, callable $comparer = null) { +function onNext(int $dueTime, $value, callable $comparer = null) +{ return new Recorded($dueTime, new OnNextNotification($value), $comparer); } -function onCompleted(int $dueTime, callable $comparer = null) { +function onCompleted(int $dueTime, callable $comparer = null) +{ return new Recorded($dueTime, new OnCompletedNotification(), $comparer); } -function subscribe(int $start, int $end = PHP_INT_MAX) { +function subscribe(int $start, int $end = PHP_INT_MAX) +{ return new Subscription($start, $end); } -function RxIdentity($x) { +function RxIdentity($x) +{ return $x; } + +function materializeObservable(Observable $observable): array +{ + $startTime = FunctionalTestCase::getScheduler()->getClock(); + + $observer = new MockObserver(FunctionalTestCase::getScheduler(), $startTime); + + $sub = $observable->subscribe($observer); + + FunctionalTestCase::getScheduler()->start(); + + $sub->dispose(); + + return $observer->getMessages(); +}