44
55namespace Rx \Functional ;
66
7- use PHPUnit_Framework_ExpectationFailedException ;
8- use Rx \Scheduler \ VirtualTimeScheduler ;
7+ use Rx \ Notification ;
8+ use Rx \Observable ;
99use Rx \TestCase ;
10+ use Rx \MarbleDiagramException ;
1011use Rx \Testing \ColdObservable ;
1112use Rx \Testing \HotObservable ;
13+ use Rx \Testing \Recorded ;
1214use Rx \Testing \Subscription ;
1315use 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