Skip to content

Commit 23e32ca

Browse files
authored
Marble testing (#133)
* Preliminary marble testing * more marble testing features * fix correct type for str_repeat() * added marker groups support * Added expectObservable and expectSubscriptions to FunctionalTestCase * Added exception message support to expectObservable()->toBe() * Added dispose support to expectObservable() * Rename MarbleDiagramError to MarbleDiagramException * Grouped marbles now happen at the same time * Grouped subscription marbles happen at the same time * Added interfaces so we get type hinting for `toBe` * Fixed continues to actually continue the loop
1 parent 7c058d5 commit 23e32ca

File tree

3 files changed

+578
-2
lines changed

3 files changed

+578
-2
lines changed

test/Rx/Functional/FunctionalTestCase.php

Lines changed: 260 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
namespace Rx\Functional;
66

7-
use PHPUnit_Framework_ExpectationFailedException;
8-
use Rx\Scheduler\VirtualTimeScheduler;
7+
use Rx\Notification;
8+
use Rx\Observable;
99
use Rx\TestCase;
10+
use Rx\MarbleDiagramException;
1011
use Rx\Testing\ColdObservable;
1112
use Rx\Testing\HotObservable;
13+
use Rx\Testing\Recorded;
1214
use Rx\Testing\Subscription;
1315
use Rx\Testing\TestScheduler;
1416

@@ -17,11 +19,17 @@ abstract class FunctionalTestCase extends TestCase
1719
/** @var TestScheduler */
1820
protected $scheduler;
1921

22+
const TIME_FACTOR = 10;
23+
2024
public function setup()
2125
{
2226
$this->scheduler = $this->createTestScheduler();
2327
}
2428

29+
/**
30+
* @param Recorded[] $expected
31+
* @param Recorded[] $recorded
32+
*/
2533
public function assertMessages(array $expected, array $recorded)
2634
{
2735
if (count($expected) !== count($recorded)) {
@@ -37,6 +45,27 @@ public function assertMessages(array $expected, array $recorded)
3745
$this->assertTrue(true); // success
3846
}
3947

48+
/**
49+
* @param Recorded[] $expected
50+
* @param Recorded[] $recorded
51+
*/
52+
public function assertMessagesNotEqual(array $expected, array $recorded)
53+
{
54+
if (count($expected) !== count($recorded)) {
55+
$this->assertTrue(true);
56+
return;
57+
}
58+
59+
for ($i = 0, $count = count($expected); $i < $count; $i++) {
60+
if (!$expected[$i]->equals($recorded[$i])) {
61+
$this->assertTrue(true);
62+
return;
63+
}
64+
}
65+
66+
$this->fail('Expected messages do match the actual');
67+
}
68+
4069
public function assertSubscription(HotObservable $observable, Subscription $expected)
4170
{
4271
$subscriptionCount = count($observable->getSubscriptions());
@@ -92,6 +121,16 @@ protected function createColdObservable(array $events)
92121
return new ColdObservable($this->scheduler, $events);
93122
}
94123

124+
protected function createCold(string $events, array $eventMap = [], \Exception $customError = null)
125+
{
126+
return new ColdObservable($this->scheduler, $this->convertMarblesToMessages($events, $eventMap, $customError));
127+
}
128+
129+
protected function createHot(string $events, array $eventMap = [], \Exception $customError = null)
130+
{
131+
return new HotObservable($this->scheduler, $this->convertMarblesToMessages($events, $eventMap, $customError, 200));
132+
}
133+
95134
protected function createHotObservable(array $events)
96135
{
97136
return new HotObservable($this->scheduler, $events);
@@ -101,4 +140,223 @@ protected function createTestScheduler()
101140
{
102141
return new TestScheduler();
103142
}
143+
144+
protected function convertMarblesToMessages(string $marbles, array $eventMap = [], \Exception $customError = null, $subscribePoint = 0)
145+
{
146+
/** @var Recorded $events */
147+
$events = [];
148+
$groupTime = -1;
149+
150+
// calculate subscription time
151+
$timeOffset = $subscribePoint;
152+
$subMarker = strpos($marbles, '^');
153+
if ($subMarker !== false) {
154+
$timeOffset -= $subMarker * self::TIME_FACTOR;
155+
}
156+
157+
for ($i = 0; $i < strlen($marbles); $i++) {
158+
$now = $groupTime === -1 ? $timeOffset + $i * self::TIME_FACTOR : $groupTime;
159+
160+
switch ($marbles[$i]) {
161+
case ' ':
162+
case '^':
163+
case '-': // nothing
164+
continue 2;
165+
case '#': // error
166+
$events[] = onError($now, $customError ?? new \Exception());
167+
continue 2;
168+
case '|':
169+
$events[] = onCompleted($now);
170+
continue 2;
171+
case '(':
172+
if ($groupTime !== -1) {
173+
throw new MarbleDiagramException('We\'re already inside a group');
174+
}
175+
$groupTime = $now;
176+
continue 2;
177+
case ')':
178+
if ($groupTime === -1) {
179+
throw new MarbleDiagramException('We\'re already outside of a group');
180+
}
181+
$groupTime = -1;
182+
continue 2;
183+
default:
184+
$eventKey = $marbles[$i];
185+
$events[] = onNext($now, isset($eventMap[$eventKey]) ? $eventMap[$eventKey] : $marbles[$i]);
186+
continue 2;
187+
}
188+
}
189+
190+
return $events;
191+
}
192+
193+
protected function convertMessagesToMarbles($messages)
194+
{
195+
$output = '';
196+
$lastTime = 199;
197+
198+
/** @var Recorded $message */
199+
foreach ($messages as $message) {
200+
$time = $message->getTime();
201+
/** @var Notification $value */
202+
$value = $message->getValue();
203+
$output .= str_repeat('-', (int)(($time - $lastTime - 1) / self::TIME_FACTOR));
204+
205+
$lastTime = $time;
206+
207+
$value->accept(
208+
function ($x) use (&$output) {
209+
$output .= $x;
210+
},
211+
function (\Exception $e) use (&$output) {
212+
$output .= '#';
213+
},
214+
function () use (&$output) {
215+
$output .= '|';
216+
}
217+
);
218+
}
219+
220+
return $output;
221+
}
222+
223+
protected function convertMarblesToSubscriptions(string $marbles, $startTime = 0)
224+
{
225+
$latestSubscription = null;
226+
$events = [];
227+
$groupTime = -1;
228+
229+
for ($i = 0; $i < strlen($marbles); $i++) {
230+
$now = $groupTime === -1 ? $startTime + $i * self::TIME_FACTOR : $groupTime;
231+
232+
switch ($marbles[$i]) {
233+
case ' ':
234+
case '-':
235+
continue 2;
236+
case '(':
237+
if ($groupTime !== -1) {
238+
throw new MarbleDiagramException('We\'re already inside a group');
239+
}
240+
$groupTime = $now;
241+
continue 2;
242+
case ')':
243+
if ($groupTime === -1) {
244+
throw new MarbleDiagramException('We\'re already outside of a group');
245+
}
246+
$groupTime = -1;
247+
continue 2;
248+
case '^': // subscribe
249+
if ($latestSubscription) {
250+
throw new MarbleDiagramException('Trying to subscribe before unsubscribing the previous subscription.');
251+
}
252+
$latestSubscription = $now;
253+
continue 2;
254+
case '!': // unsubscribe
255+
if (!$latestSubscription) {
256+
throw new MarbleDiagramException('Trying to unsubscribe before subscribing.');
257+
}
258+
$events[] = new Subscription($latestSubscription, $now);
259+
$latestSubscription = null;
260+
break;
261+
default:
262+
throw new MarbleDiagramException('Only "^" and "!" markers are allowed in this diagram.');
263+
}
264+
}
265+
if ($latestSubscription) {
266+
$events[] = new Subscription($latestSubscription);
267+
}
268+
return $events;
269+
}
270+
271+
protected function convertMarblesToDisposeTime(string $marbles, $startTime = 0)
272+
{
273+
$groupTime = -1;
274+
$disposeAt = 1000;
275+
276+
for ($i = 0; $i < strlen($marbles); $i++) {
277+
$now = $groupTime === -1 ? $startTime + $i * self::TIME_FACTOR : $groupTime++;
278+
279+
switch ($marbles[$i]) {
280+
case ' ':
281+
continue 2;
282+
case '!': // unsubscribe
283+
$disposeAt = $now;
284+
break;
285+
default:
286+
throw new MarbleDiagramException('Only " " and "!" markers are allowed in this diagram.');
287+
}
288+
}
289+
290+
return $disposeAt;
291+
}
292+
293+
public function expectObservable(Observable $observable, string $disposeMarble = null): ExpectObservableToBe
294+
{
295+
if ($disposeMarble) {
296+
$disposeAt = $this->convertMarblesToDisposeTime($disposeMarble, 200);
297+
298+
$results = $this->scheduler->startWithDispose(function () use ($observable) {
299+
return $observable;
300+
}, $disposeAt);
301+
} else {
302+
$results = $this->scheduler->startWithCreate(function () use ($observable) {
303+
return $observable;
304+
});
305+
}
306+
307+
$messages = $results->getMessages();
308+
309+
return new class($messages) extends FunctionalTestCase implements ExpectObservableToBe
310+
{
311+
private $messages;
312+
313+
public function __construct(array $messages)
314+
{
315+
parent::__construct();
316+
$this->messages = $messages;
317+
}
318+
319+
public function toBe(string $expected, array $values = [], string $errorMessage = null)
320+
{
321+
$error = $errorMessage ? new \Exception($errorMessage) : null;
322+
323+
$this->assertEquals(
324+
$this->convertMarblesToMessages($expected, $values, $error, 200),
325+
$this->messages
326+
);
327+
}
328+
};
329+
}
330+
331+
public function expectSubscriptions(array $subscriptions): ExpectSubscriptionsToBe
332+
{
333+
return new class($subscriptions) extends FunctionalTestCase implements ExpectSubscriptionsToBe
334+
{
335+
private $subscriptions;
336+
337+
public function __construct(array $subscriptions)
338+
{
339+
parent::__construct();
340+
$this->subscriptions = $subscriptions;
341+
}
342+
343+
public function toBe(string $subscriptionsMarbles)
344+
{
345+
$this->assertEquals(
346+
$this->convertMarblesToSubscriptions($subscriptionsMarbles, 200),
347+
$this->subscriptions
348+
);
349+
}
350+
};
351+
}
352+
}
353+
354+
interface ExpectSubscriptionsToBe
355+
{
356+
public function toBe(string $subscriptionsMarbles);
357+
}
358+
359+
interface ExpectObservableToBe
360+
{
361+
public function toBe(string $expected, array $values = [], string $errorMessage = null);
104362
}

0 commit comments

Comments
 (0)