001package org.reactivestreams.tck;
002
003import org.reactivestreams.spi.Publisher;
004import org.reactivestreams.spi.Subscriber;
005import org.reactivestreams.spi.Subscription;
006import org.reactivestreams.tck.support.Optional;
007
008import java.util.LinkedList;
009import java.util.List;
010import java.util.concurrent.ArrayBlockingQueue;
011import java.util.concurrent.CopyOnWriteArrayList;
012import java.util.concurrent.CountDownLatch;
013import java.util.concurrent.TimeUnit;
014
015import static org.testng.Assert.fail;
016
017public class TestEnvironment {
018  public static final int TEST_BUFFER_SIZE = 16;
019
020  private final long defaultTimeoutMillis;
021
022  private CopyOnWriteArrayList<Throwable> asyncErrors = new CopyOnWriteArrayList<Throwable>();
023
024  /**
025   * Tests must specify the timeout for expected outcome of asynchronous
026   * interactions. Longer timeout does not invalidate the correctness of
027   * the implementation, but can in some cases result in longer time to
028   * run the tests.
029   */
030  public TestEnvironment(long defaultTimeoutMillis) {
031    this.defaultTimeoutMillis = defaultTimeoutMillis;
032  }
033
034  // keeping method around
035  public long defaultTimeoutMillis() { return defaultTimeoutMillis; }
036
037  // don't use the name `fail` as it would collide with other `fail` definitions like the one in scalatest's traits
038  public void flop(String msg) {
039    try {
040      fail(msg);
041    } catch (Throwable t) {
042      asyncErrors.add(t);
043      throw new RuntimeException(t);
044    }
045  }
046
047
048  public <T extends Throwable> void expectThrowingOf(Class<T> clazz, String errorMsg, Runnable block) throws Throwable {
049    try {
050      block.run();
051      flop(errorMsg);
052    } catch (Throwable e) {
053      if (clazz.isInstance(e)) {
054        // ok
055      } else if (org.reactivestreams.tck.support.NonFatal.apply(e)) {
056        flop(errorMsg + " but " + e);
057      } else {
058        throw e;
059      }
060    }
061  }
062
063  public <T> void subscribe(Publisher<T> pub, TestSubscriber<T> sub) throws InterruptedException {
064    subscribe(pub, sub, defaultTimeoutMillis);
065  }
066
067  public <T> void subscribe(Publisher<T> pub, TestSubscriber<T> sub, long timeoutMillis) throws InterruptedException {
068      pub.subscribe(sub);
069      sub.subscription.expectCompletion(timeoutMillis, String.format("Could not subscribe %s to Publisher %s", sub, pub));
070      verifyNoAsyncErrors();
071    }
072
073
074  public <T> ManualSubscriber<T> newManualSubscriber(Publisher<T> pub) throws InterruptedException {
075    return newManualSubscriber(pub, defaultTimeoutMillis());
076  }
077  public <T> ManualSubscriber<T> newManualSubscriber(Publisher<T> pub, long timeoutMillis) throws InterruptedException {
078    ManualSubscriberWithSubscriptionSupport<T> sub = new ManualSubscriberWithSubscriptionSupport<T>(this);
079    subscribe(pub, sub, timeoutMillis);
080    return sub;
081  }
082
083  public void verifyNoAsyncErrors() {
084    for (Throwable e : asyncErrors) {
085      if (e instanceof AssertionError) throw (AssertionError) e;
086      else fail("Async error during test execution: " + e);
087    }
088  }
089
090  // ---- classes ----
091
092  static class ManualSubscriberWithSubscriptionSupport<T> extends ManualSubscriber<T> {
093
094    public ManualSubscriberWithSubscriptionSupport(TestEnvironment env) {
095      super(env);
096    }
097
098    public void onNext(T element) {
099      if (subscription.isCompleted()) {
100        super.onNext(element);
101      } else {
102        env.flop("Subscriber::onNext(" + element + ") called before Subscriber::onSubscribe");
103      }
104    }
105
106    public void onComplete() {
107      if (subscription.isCompleted()) {
108        super.onComplete();
109      } else {
110        env.flop("Subscriber::onComplete() called before Subscriber::onSubscribe");
111      }
112    }
113
114    public void onSubscribe(Subscription s) {
115      if (!subscription.isCompleted()) {
116        subscription.complete(s);
117      } else {
118        env.flop("Subscriber::onSubscribe called on an already-subscribed Subscriber");
119      }
120    }
121
122    public void onError(Throwable cause) {
123      if (subscription.isCompleted()) {
124        super.onError(cause);
125      } else {
126        env.flop("Subscriber::onError(" + cause + ") called before Subscriber::onSubscribe");
127      }
128    }
129  }
130
131  static class TestSubscriber<T> implements Subscriber<T> {
132    volatile Promise<Subscription> subscription;
133
134    protected final TestEnvironment env;
135
136    public TestSubscriber(TestEnvironment env) {
137      this.env = env;
138      subscription = new Promise<Subscription>(env);
139    }
140
141    @Override
142    public void onError(Throwable cause)  {
143      env.flop(String.format("Unexpected Subscriber::onError(%s)", cause));
144    }
145    
146    @Override
147    public void onComplete() {
148      env.flop("Unexpected Subscriber::onComplete()");
149    }
150    
151    @Override
152    public void onNext(T element) {
153      env.flop(String.format("Unexpected Subscriber::onNext(%s)", element));
154    }
155    
156    public void onSubscribe(Subscription subscription) {
157      env.flop(String.format("Unexpected Subscriber::onSubscribe(%s)", subscription));
158    }
159      
160    public void cancel() {
161      if (subscription.isCompleted()) {
162        subscription.value().cancel();
163        subscription = new Promise<Subscription>(env);
164      } else env.flop("Cannot cancel a subscription before having received it");
165    }
166  }
167
168  static class ManualSubscriber<T> extends TestSubscriber<T> {
169    Receptacle<T> received = new Receptacle<T>(env);
170
171    public ManualSubscriber(TestEnvironment env) {
172      super(env);
173    }
174
175    @Override
176    public void onNext(T element) {
177      received.add(element);
178    }
179
180    @Override
181    public void onComplete() {
182      received.complete();
183    }
184
185    void requestMore(int elements) {
186      subscription.value().requestMore(elements);
187    }
188
189    public T requestNextElement() throws InterruptedException {
190      return requestNextElement(env.defaultTimeoutMillis());
191    }
192
193    public T requestNextElement(long timeoutMillis) throws InterruptedException {
194      return requestNextElement(timeoutMillis, "Did not receive expected element");
195    }
196
197    public T requestNextElement(String errorMsg) throws InterruptedException {
198      return requestNextElement(env.defaultTimeoutMillis(), errorMsg);
199    }
200
201    public T requestNextElement(long timeoutMillis, String errorMsg) throws InterruptedException {
202      requestMore(1);
203      return nextElement(timeoutMillis, errorMsg);
204    }
205
206    public Optional<T> requestNextElementOrEndOfStream(String errorMsg) throws InterruptedException {
207      return requestNextElementOrEndOfStream(env.defaultTimeoutMillis(), errorMsg);
208    }
209
210    public Optional<T> requestNextElementOrEndOfStream(long timeoutMillis) throws InterruptedException {
211      return requestNextElementOrEndOfStream(timeoutMillis, "Did not receive expected stream completion");
212    }
213
214    public Optional<T> requestNextElementOrEndOfStream(long timeoutMillis, String errorMsg) throws InterruptedException {
215      requestMore(1);
216      return nextElementOrEndOfStream(timeoutMillis, errorMsg);
217    }
218
219    public void requestEndOfStream() throws InterruptedException {
220      requestEndOfStream(env.defaultTimeoutMillis(), "Did not receive expected stream completion");
221    }
222
223    public void requestEndOfStream(long timeoutMillis) throws InterruptedException {
224      requestEndOfStream(timeoutMillis, "Did not receive expected stream completion");
225    }
226
227    public void requestEndOfStream(String errorMsg) throws InterruptedException {
228      requestEndOfStream(env.defaultTimeoutMillis(), errorMsg);
229    }
230
231    public void requestEndOfStream(long timeoutMillis, String errorMsg) throws InterruptedException {
232      requestMore(1);
233      expectCompletion(timeoutMillis, errorMsg);
234    }
235
236    public List<T> requestNextElements(int elements, long timeoutMillis, String errorMsg) throws InterruptedException {
237      requestMore(elements);
238      return nextElements(elements, timeoutMillis, errorMsg);
239    }
240
241    public T nextElement() throws InterruptedException {
242      return nextElement(env.defaultTimeoutMillis());
243    }
244
245    public T nextElement(long timeoutMillis) throws InterruptedException {
246      return nextElement(timeoutMillis, "Did not receive expected element");
247    }
248
249    public T nextElement(String errorMsg) throws InterruptedException {
250      return nextElement(env.defaultTimeoutMillis(), errorMsg);
251    }
252
253    public T nextElement(long timeoutMillis, String errorMsg) throws InterruptedException {
254      return received.next(timeoutMillis, errorMsg);
255    }
256
257    public Optional<T> nextElementOrEndOfStream(long timeoutMillis) throws InterruptedException {
258      return nextElementOrEndOfStream(timeoutMillis, "Did not receive expected stream completion");
259    }
260
261    public Optional<T> nextElementOrEndOfStream(long timeoutMillis, String errorMsg) throws InterruptedException {
262      return received.nextOrEndOfStream(timeoutMillis, errorMsg);
263    }
264
265    public List<T> nextElements(int elements) throws InterruptedException {
266      return nextElements(elements, env.defaultTimeoutMillis(), "Did not receive expected element or completion");
267    }
268
269    public List<T> nextElements(int elements, String errorMsg) throws InterruptedException {
270      return nextElements(elements, env.defaultTimeoutMillis(), errorMsg);
271    }
272
273    public List<T> nextElements(int elements, long timeoutMillis) throws InterruptedException {
274      return nextElements(elements, timeoutMillis, "Did not receive expected element or completion");
275    }
276
277    public List<T> nextElements(int elements, long timeoutMillis, String errorMsg) throws InterruptedException {
278      return received.nextN(elements, timeoutMillis, errorMsg);
279    }
280
281    void expectNext(T expected) throws InterruptedException {
282      expectNext(expected, env.defaultTimeoutMillis());
283    }
284
285    void expectNext(T expected, long timeoutMillis) throws InterruptedException {
286      T received = nextElement(timeoutMillis, "Did not receive expected element on downstream");
287      if (!received.equals(expected)) {
288        env.flop(String.format("Expected element %s on downstream but received %s", expected, received));
289      }
290    }
291
292    void expectCompletion(long timeoutMillis) throws InterruptedException {
293      expectCompletion(timeoutMillis, "Did not receive expected stream completion");
294    }
295
296    void expectCompletion(String errorMsg) throws InterruptedException {
297      expectCompletion(env.defaultTimeoutMillis(), errorMsg);
298    }
299
300    void expectCompletion(long timeoutMillis, String errorMsg) throws InterruptedException {
301      received.expectCompletion(timeoutMillis, errorMsg);
302    }
303
304    public void expectNone() throws InterruptedException {
305      expectNone(env.defaultTimeoutMillis());
306    }
307
308    public void expectNone(String errMsgPrefix) throws InterruptedException {
309      received.expectNone(env.defaultTimeoutMillis(), errMsgPrefix);
310    }
311
312    public void expectNone(long withinMillis) throws InterruptedException {
313      received.expectNone(withinMillis, "Did not expect an element but got ");
314    }
315
316  }
317
318  static class ManualPublisher<T> implements Publisher<T> {
319    protected final TestEnvironment env;
320
321    Optional<Subscriber<T>> subscriber = Optional.empty();
322    Receptacle<Integer> requests;
323    Latch cancelled;
324
325    public ManualPublisher(TestEnvironment env) {
326      this.env = env;
327      requests = new Receptacle<Integer>(env);
328      cancelled = new Latch(env);
329    }
330
331    @Override
332    public void subscribe(Subscriber<T> s) {
333      if (subscriber.isEmpty()) {
334        subscriber = Optional.of(s);
335
336        Subscription subs = new Subscription() {
337          @Override
338          public void requestMore(int elements) {
339            requests.add(elements);
340          }
341
342          @Override
343          public void cancel() {
344            cancelled.close();
345          }
346        };
347        s.onSubscribe(subs);
348
349      } else {
350        env.flop("TestPublisher doesn't support more than one Subscriber");
351      }
352    }
353
354    public void sendNext(T element) {
355      if (subscriber.isDefined()) subscriber.get().onNext(element);
356      else env.flop("Cannot sendNext before subscriber subscription");
357    }
358
359    public void sendCompletion() {
360      if (subscriber.isDefined()) subscriber.get().onComplete();
361      else env.flop("Cannot sendCompletion before subscriber subscription");
362    }
363
364    public void sendError(Throwable cause) {
365      if (subscriber.isDefined()) subscriber.get().onError(cause);
366      else env.flop("Cannot sendError before subscriber subscription");
367    }
368
369    public int nextRequestMore() throws InterruptedException {
370      return nextRequestMore(env.defaultTimeoutMillis());
371    }
372
373    public int nextRequestMore(long timeoutMillis) throws InterruptedException {
374      return requests.next(timeoutMillis, "Did not receive expected `requestMore` call");
375    }
376
377    public int expectRequestMore() throws InterruptedException {
378      return expectRequestMore(env.defaultTimeoutMillis());
379    }
380
381    public int expectRequestMore(long timeoutMillis) throws InterruptedException {
382      int requested = nextRequestMore(timeoutMillis);
383      if (requested <= 0) {
384        env.flop(String.format("Requests cannot be zero or negative but received requestMore(%s)", requested));
385        return 0; // keep compiler happy
386      } else
387        return requested;
388    }
389
390    public void expectExactRequestMore(int expected) throws InterruptedException {
391      expectExactRequestMore(expected, env.defaultTimeoutMillis());
392    }
393
394    public void expectExactRequestMore(int expected, long timeoutMillis) throws InterruptedException {
395      int requested = expectRequestMore(timeoutMillis);
396      if (requested != expected)
397        env.flop(String.format("Received `requestMore(%d)` on upstream but expected `requestMore(%d)`", requested, expected));
398    }
399
400    public void expectNoRequestMore() throws InterruptedException {
401      expectNoRequestMore(env.defaultTimeoutMillis());
402    }
403
404    public void expectNoRequestMore(long timeoutMillis) throws InterruptedException {
405      requests.expectNone(timeoutMillis, "Received an unexpected call to: requestMore");
406    }
407
408    public void expectCancelling() throws InterruptedException {
409      expectCancelling(env.defaultTimeoutMillis());
410    }
411
412    public void expectCancelling(long timeoutMillis) throws InterruptedException {
413      cancelled.expectClose(timeoutMillis, "Did not receive expected cancelling of upstream subscription");
414    }
415  }
416
417  /** like a CountDownLatch, but resettable and with some convenience methods */
418  static class Latch {
419    private final TestEnvironment env;
420    volatile private CountDownLatch countDownLatch = new CountDownLatch(1);
421
422    public Latch(TestEnvironment env) {
423      this.env = env;
424    }
425
426    public void reOpen() {
427      countDownLatch = new CountDownLatch(1);
428    }
429
430    public boolean isClosed() {
431      return countDownLatch.getCount() == 0;
432    }
433
434    public void close() {
435      countDownLatch.countDown();
436    }
437
438    public void assertClosed(String openErrorMsg) {
439      if (!isClosed()) {
440        env.flop(openErrorMsg);
441      }
442    }
443
444    public void assertOpen(String closedErrorMsg) {
445      if (isClosed()) {
446        env.flop(closedErrorMsg);
447      }
448    }
449
450    public void expectClose(long timeoutMillis, String notClosedErrorMsg) throws InterruptedException {
451      countDownLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
452      if (countDownLatch.getCount() > 0) {
453        env.flop(String.format("%s within %d ms", notClosedErrorMsg, timeoutMillis));
454      }
455    }
456  }
457
458  // simple promise for *one* value, which cannot be reset
459  static class Promise<T> {
460    private final TestEnvironment env;
461
462    public Promise(TestEnvironment env) {
463      this.env = env;
464    }
465
466    private ArrayBlockingQueue<T> abq = new ArrayBlockingQueue<T>(1);
467    volatile private T _value = null;
468
469    public T value() {
470      if (isCompleted()) {
471        return _value;
472      } else {
473        env.flop("Cannot access promise value before completion");
474        return null;
475      }
476    }
477
478    public boolean isCompleted() {
479      return _value != null;
480    }
481
482    public void complete(T value) {
483      abq.add(value);
484    }
485
486    public void assertCompleted(String errorMsg) {
487      if(!isCompleted())
488          env.flop(errorMsg);
489    }
490
491    public void assertUncompleted(String errorMsg) {
492    if(isCompleted())
493      env.flop(errorMsg);
494    }
495
496
497    public void expectCompletion(long timeoutMillis, String errorMsg) throws InterruptedException {
498      if (!isCompleted()) {
499        T val = abq.poll(timeoutMillis, TimeUnit.MILLISECONDS);
500        if (val == null) {
501          env.flop(String.format("%s within %d ms", errorMsg, timeoutMillis));
502        } else {
503          _value = val;
504        }
505      }
506    }
507  }
508
509   // a "Promise" for multiple values, which also supports "end-of-stream reached"
510  static class Receptacle<T> {
511    final int QUEUE_SIZE = 2 * TEST_BUFFER_SIZE;
512     private final TestEnvironment env;
513
514     private ArrayBlockingQueue<Optional<T>> abq = new ArrayBlockingQueue<Optional<T>>(QUEUE_SIZE);
515
516     Receptacle(TestEnvironment env) {
517       this.env = env;
518     }
519
520     public void add(T value) {
521      abq.add(Optional.of(value));
522    }
523
524    public void complete() {
525      abq.add(Optional.<T>empty());
526    }
527
528    public T next(long timeoutMillis, String errorMsg) throws InterruptedException {
529      Optional<T> value = abq.poll(timeoutMillis, TimeUnit.MILLISECONDS);
530
531      if (value.isEmpty()) {
532        env.flop("Expected element but got end-of-stream");
533      } else if (value.get() == null) {
534        env.flop(String.format("%s within %d ms", errorMsg, timeoutMillis));
535      } else {
536        return value.get();
537      }
538
539      return null; // keep compiler happy
540    }
541
542    public Optional<T> nextOrEndOfStream(long timeoutMillis, String errorMsg) throws InterruptedException {
543      Optional<T> value = abq.poll(timeoutMillis, TimeUnit.MILLISECONDS);
544      if (value.isDefined()) {
545        return value;
546      } else {
547        env.flop(String.format("%s within %d ms", errorMsg, timeoutMillis));
548        return null; // keep compiler happy
549      }
550    }
551
552    public List<T> nextN(int elements, long timeoutMillis, String errorMsg) throws InterruptedException {
553      List<T> result = new LinkedList<T>();
554      int remaining = elements;
555      while (remaining > 0) {
556        result.add(next(timeoutMillis, errorMsg)); // TODO: fix error messages showing wrong timeout info
557        remaining--;
558      }
559
560      return result;
561    }
562
563
564    public void expectCompletion(long timeoutMillis, String errorMsg) throws InterruptedException {
565      Optional<T> value = abq.poll(timeoutMillis, TimeUnit.MILLISECONDS);
566
567      if (value.isEmpty()) {
568        // ok
569      } else if (value.get() == null) {
570        env.flop(String.format("%s within %d ms", errorMsg, timeoutMillis));
571      } else {
572        env.flop("Expected end-of-stream but got " + value.get());
573      }
574    }
575
576    void expectNone(long withinMillis, String errorMsgPrefix) throws InterruptedException {
577      Thread.sleep(withinMillis);
578      Optional<T> value = abq.poll();
579
580      if (value == null) {
581        // ok
582      } else if (value.isDefined()) {
583        env.flop(errorMsgPrefix + value.get());
584      } else {
585        env.flop("Expected no element but got end-of-stream");
586      }
587    }
588  }
589}
590