├── src ├── main │ └── java │ │ ├── rx │ │ └── marble │ │ │ ├── ISetupSubscriptionsTest.java │ │ │ ├── TestableObservable.java │ │ │ ├── ExpectObservableException.java │ │ │ ├── ExpectSubscriptionsException.java │ │ │ ├── ISetupTest.java │ │ │ ├── SetupTestSupport.java │ │ │ ├── ExceptionHelper.java │ │ │ ├── MapHelper.java │ │ │ ├── SubscriptionLog.java │ │ │ ├── Recorded.java │ │ │ ├── junit │ │ │ └── MarbleRule.java │ │ │ ├── ColdObservable.java │ │ │ ├── HotObservable.java │ │ │ ├── Parser.java │ │ │ ├── RecordedStreamComparator.java │ │ │ └── MarbleScheduler.java │ │ ├── org │ │ └── reactivestreams │ │ │ ├── ISetupSubscriptionsTest.java │ │ │ ├── SchedulerFactory.java │ │ │ ├── TestablePublisher.java │ │ │ ├── ExpectPublisherException.java │ │ │ ├── ExpectSubscriptionsException.java │ │ │ ├── Scheduler.java │ │ │ ├── ISetupTest.java │ │ │ ├── SetupTestSupport.java │ │ │ ├── ExceptionHelper.java │ │ │ ├── SubscriptionLog.java │ │ │ ├── Recorded.java │ │ │ ├── ColdPublisher.java │ │ │ ├── HotPublisher.java │ │ │ ├── Parser.java │ │ │ ├── RecordedStreamComparator.java │ │ │ ├── Notification.java │ │ │ └── MarbleSchedulerState.java │ │ ├── io │ │ └── reactivex │ │ │ └── marble │ │ │ ├── ExpectFlowableException.java │ │ │ ├── ExpectSubscriptionsException.java │ │ │ ├── SchedulerAdapter.java │ │ │ ├── MapHelper.java │ │ │ ├── ObserverAdapter.java │ │ │ ├── HotObservable.java │ │ │ ├── ColdObservable.java │ │ │ ├── junit │ │ │ └── MarbleRule.java │ │ │ └── MarbleScheduler.java │ │ └── reactor │ │ ├── SchedulerAdapter.java │ │ ├── MapHelper.java │ │ ├── HotFlux.java │ │ ├── ColdFlux.java │ │ ├── MarbleScheduler.java │ │ └── junit │ │ └── MarbleRule.java └── test │ └── java │ ├── rx │ └── marble │ │ ├── ExceptionHelperTest.java │ │ ├── junit │ │ └── DemoTest.java │ │ ├── HotObservableTest.java │ │ ├── RecordedStreamComparatorTest.java │ │ ├── ParserTest.java │ │ ├── ColdObservableTest.java │ │ └── MarbleSchedulerTest.java │ ├── org │ └── reactivestreams │ │ ├── ExceptionHelperTest.java │ │ ├── ParserTest.java │ │ └── MarbleSchedulerTest.java │ ├── reactor │ ├── junit │ │ └── DemoTest.java │ ├── HotFluxTest.java │ └── ColdFluxTest.java │ └── io │ └── reactivex │ └── marble │ ├── junit │ └── DemoTest.java │ ├── HotObservableTest.java │ ├── RecordedStreamComparatorTest.java │ └── ColdObservableTest.java ├── .gitignore ├── .travis.yml ├── pom.xml └── README.md /src/main/java/rx/marble/ISetupSubscriptionsTest.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | 4 | public interface ISetupSubscriptionsTest { 5 | void toBe(String... marbles); 6 | } -------------------------------------------------------------------------------- /src/main/java/org/reactivestreams/ISetupSubscriptionsTest.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | 4 | public interface ISetupSubscriptionsTest { 5 | void toBe(String... marbles); 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | *.iml 11 | .idea 12 | -------------------------------------------------------------------------------- /src/main/java/org/reactivestreams/SchedulerFactory.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | /** 4 | * Created by Alexandre Victoor on 18/04/2017. 5 | */ 6 | public interface SchedulerFactory { 7 | 8 | Scheduler create(); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/rx/marble/TestableObservable.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | import java.util.List; 4 | 5 | interface TestableObservable { 6 | 7 | List getSubscriptions(); 8 | 9 | List> getMessages(); 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/main/java/io/reactivex/marble/ExpectFlowableException.java: -------------------------------------------------------------------------------- 1 | package io.reactivex.marble; 2 | 3 | public class ExpectFlowableException extends RuntimeException { 4 | 5 | public ExpectFlowableException(String message) { 6 | super(message); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/io/reactivex/marble/ExpectSubscriptionsException.java: -------------------------------------------------------------------------------- 1 | package io.reactivex.marble; 2 | 3 | public class ExpectSubscriptionsException extends RuntimeException { 4 | 5 | public ExpectSubscriptionsException(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/reactivestreams/TestablePublisher.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | import java.util.List; 4 | 5 | public interface TestablePublisher extends Publisher { 6 | 7 | List getSubscriptions(); 8 | 9 | List> getMessages(); 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/main/java/rx/marble/ExpectObservableException.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | public class ExpectObservableException extends RuntimeException { 4 | 5 | public ExpectObservableException(String message, String caller) { 6 | super(message + "\n\n from assertion at " + caller + "\n\n----------------------\n"); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/rx/marble/ExpectSubscriptionsException.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | public class ExpectSubscriptionsException extends RuntimeException { 4 | 5 | public ExpectSubscriptionsException(String message, String caller) { 6 | super(message + "\n\n from assertion at " + caller + "\n\n----------------------\n"); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/reactivestreams/ExpectPublisherException.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | public class ExpectPublisherException extends RuntimeException { 4 | 5 | public ExpectPublisherException(String message, String caller) { 6 | super(message + "\n\n from assertion at " + caller + "\n\n----------------------\n"); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/reactivestreams/ExpectSubscriptionsException.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | public class ExpectSubscriptionsException extends RuntimeException { 4 | 5 | public ExpectSubscriptionsException(String message, String caller) { 6 | super(message + "\n\n from assertion at " + caller + "\n\n----------------------\n"); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/reactivestreams/Scheduler.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | /** 6 | * Created by Alexandre Victoor on 18/04/2017. 7 | */ 8 | public interface Scheduler { 9 | 10 | void schedule(Runnable run, long delay, TimeUnit unit); 11 | 12 | long now(TimeUnit unit); 13 | 14 | void dispose(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/rx/marble/ISetupTest.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | 4 | import java.util.Map; 5 | 6 | public interface ISetupTest { 7 | 8 | void toBe(String marble, 9 | Map values, 10 | Exception errorValue); 11 | 12 | void toBe(String marble, 13 | Map values); 14 | 15 | void toBe(String marble); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/reactivestreams/ISetupTest.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | 4 | import java.util.Map; 5 | 6 | public interface ISetupTest { 7 | 8 | void toBe(String marble, 9 | Map values, 10 | Exception errorValue); 11 | 12 | void toBe(String marble, 13 | Map values); 14 | 15 | void toBe(String marble); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/rx/marble/SetupTestSupport.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | 4 | import java.util.Map; 5 | 6 | public abstract class SetupTestSupport implements ISetupTest { 7 | 8 | public void toBe(String marble, 9 | Map values) { 10 | 11 | toBe(marble, values, null); 12 | } 13 | 14 | public void toBe(String marble) { 15 | toBe(marble, null); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/reactivestreams/SetupTestSupport.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | import java.util.Map; 4 | 5 | public abstract class SetupTestSupport implements ISetupTest { 6 | 7 | public void toBe(String marble, 8 | Map values) { 9 | 10 | toBe(marble, values, null); 11 | } 12 | 13 | public void toBe(String marble) { 14 | toBe(marble, null); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/rx/marble/ExceptionHelperTest.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | 4 | import org.junit.Test; 5 | 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | public class ExceptionHelperTest { 9 | 10 | @Test 11 | public void should_return_line_outside_callee() { 12 | String caller = new Dummy().findCaller(); 13 | assertThat(caller).contains("should_return_line_outside_callee"); 14 | } 15 | 16 | public static class Dummy { 17 | public String findCaller() { 18 | return ExceptionHelper.findCallerInStackTrace(getClass()); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/rx/marble/ExceptionHelper.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | 4 | public class ExceptionHelper { 5 | 6 | 7 | public static String findCallerInStackTrace(Class callee) { 8 | StackTraceElement[] stackTrace = new Exception().getStackTrace(); 9 | StackTraceElement current = null; 10 | for (StackTraceElement element : stackTrace) { 11 | current = element; 12 | if (!element.getClassName().endsWith(callee.getSimpleName()) 13 | && !element.getClassName().endsWith(ExceptionHelper.class.getSimpleName())) { 14 | break; 15 | } 16 | } 17 | return current.getClassName() 18 | + "." + current.getMethodName() 19 | + "(" + current.getFileName() + ":" + current.getLineNumber() + ")"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/reactor/SchedulerAdapter.java: -------------------------------------------------------------------------------- 1 | package reactor; 2 | 3 | import reactor.core.scheduler.Scheduler; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | 7 | /** 8 | * Created by Alexandre Victoor on 18/04/2017. 9 | */ 10 | class SchedulerAdapter implements org.reactivestreams.Scheduler { 11 | 12 | private final Scheduler scheduler; 13 | private final Scheduler.Worker worker; 14 | 15 | public SchedulerAdapter(Scheduler scheduler) { 16 | this.scheduler = scheduler; 17 | this.worker = scheduler.createWorker(); 18 | } 19 | 20 | @Override 21 | public void schedule(Runnable run, long delay, TimeUnit unit) { 22 | worker.schedule(run, delay, unit); 23 | } 24 | 25 | @Override 26 | public long now(TimeUnit unit) { 27 | return scheduler.now(unit); 28 | } 29 | 30 | @Override 31 | public void dispose() { 32 | worker.dispose(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/io/reactivex/marble/SchedulerAdapter.java: -------------------------------------------------------------------------------- 1 | package io.reactivex.marble; 2 | 3 | import io.reactivex.Scheduler; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | 7 | /** 8 | * Created by Alexandre Victoor on 18/04/2017. 9 | */ 10 | class SchedulerAdapter implements org.reactivestreams.Scheduler { 11 | 12 | private final Scheduler scheduler; 13 | private final Scheduler.Worker worker; 14 | 15 | public SchedulerAdapter(Scheduler scheduler) { 16 | this.scheduler = scheduler; 17 | this.worker = scheduler.createWorker(); 18 | } 19 | 20 | @Override 21 | public void schedule(Runnable run, long delay, TimeUnit unit) { 22 | worker.schedule(run, delay, unit); 23 | } 24 | 25 | @Override 26 | public long now(TimeUnit unit) { 27 | return scheduler.now(unit); 28 | } 29 | 30 | @Override 31 | public void dispose() { 32 | worker.dispose(); 33 | } 34 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | addons: 2 | sonarqube: true 3 | jdk: 4 | - oraclejdk8 5 | env: 6 | global: 7 | secure: ZVme7bez6+KfZ7gPCHDaS3LmGUW1L5mkD9yQUJyfw9/gDEgFDWopOIgmbQIxRcbZeanr3WEm75AvK5IjrgGvSJi4XLgHqa6fTPRYPYWgpgidfPsi0WgW/37bRbUE5nnvdFEciMVlWEUTnChz05Xwi8Mq0UsbCOEvkzh6dziQhd/KkMbe5ydfjB0bYSVvjJcggVpVz66VoUUi12DPS2sZCu8S/yXz9WUspOZ2ueU5JGdqqo+cUXDDLjSncZcoXujHJkLnbkpnqCG4XKMhhQJVOWf1P6IhpNbg2zy8GX/0UOXrkyQWVqYCQ4bfo4yhrCoA3jGsQBEfoc/vTOnEnSeUeyNoJZZQD2nVfo1AVINZIEqcP3k6Z5LImGpegsoRGMv7nSjHWCvpWLAMa+NIpW1oHAewcCVjVNtTEiNPSBc02/F6ceqYAQHQlgiO5GQJak3FxYUzLrE4q+QDz2qmwgvDQHUWccZlacEupWHsfQEki4GsnaswsBlX7dT5jUJUUiUc5ltBT6SutjzZRcbYhehO8oQeZ3l5cwenD+5cYZZqlVKQbeuCbtmeOssRHig3TU9NnRFKwKXJWM0Wyy/TgWHoHWyx6Apl2Sis7o6wiPu3K9gC6kLjIRLTh1b+xxLXk0XZzYqYBRbGlJO8D8U2P6nqJf71SDO4/3eTGJtfxvQPc/s= 8 | script: 9 | # the following command line builds the project, runs the tests with coverage and then execute the SonarQube analysis 10 | - mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent install sonar:sonar -Dsonar.login=$SONAR_TOKEN 11 | -------------------------------------------------------------------------------- /src/main/java/org/reactivestreams/ExceptionHelper.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | 4 | public class ExceptionHelper { 5 | 6 | 7 | public static String findCallerInStackTrace(Class... callees) { 8 | StackTraceElement[] stackTrace = new Exception().getStackTrace(); 9 | StackTraceElement current = null; 10 | for (StackTraceElement element : stackTrace) { 11 | current = element; 12 | boolean elementFromCallees = false; 13 | String elementClassName = element.getClassName(); 14 | for (Class callee : callees) { 15 | elementFromCallees |= elementClassName.endsWith(callee.getSimpleName()); 16 | } 17 | if (!elementFromCallees 18 | && !elementClassName.endsWith(ExceptionHelper.class.getSimpleName())) { 19 | break; 20 | } 21 | } 22 | return current.getClassName() 23 | + "." + current.getMethodName() 24 | + "(" + current.getFileName() + ":" + current.getLineNumber() + ")"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/org/reactivestreams/ExceptionHelperTest.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | 4 | import org.junit.Test; 5 | 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | public class ExceptionHelperTest { 9 | 10 | @Test 11 | public void should_return_line_outside_callee() { 12 | String caller = new Dummy().findCaller(); 13 | assertThat(caller).contains("should_return_line_outside_callee"); 14 | } 15 | 16 | @Test 17 | public void should_return_line_outside_callees() { 18 | String caller = new Foo().findCaller(); 19 | assertThat(caller).contains("should_return_line_outside_callees"); 20 | } 21 | 22 | public static class Dummy { 23 | public String findCaller() { 24 | return ExceptionHelper.findCallerInStackTrace(getClass()); 25 | } 26 | } 27 | 28 | public static class Foo { 29 | public String findCaller() { 30 | return new Bar().findCaller(getClass()); 31 | } 32 | } 33 | 34 | public static class Bar { 35 | public String findCaller(Class clazz) { 36 | return ExceptionHelper.findCallerInStackTrace(clazz, getClass()); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/java/reactor/MapHelper.java: -------------------------------------------------------------------------------- 1 | package reactor; 2 | 3 | import java.util.Collections; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | /** 8 | * Helper inspired from guava's ImmutableMap.of() factory method 9 | */ 10 | public class MapHelper { 11 | 12 | public static Map of(String k, V v) { 13 | return Collections.singletonMap(k,v); 14 | } 15 | 16 | public static Map of(String k1, V v1, String k2, V v2) { 17 | Map map = new HashMap<>(); 18 | map.put(k1, v1); 19 | map.put(k2, v2); 20 | return map; 21 | } 22 | 23 | public static Map of(String k1, V v1, String k2, V v2, String k3, V v3) { 24 | Map result = of(k1, v1, k2, v2); 25 | result.put(k3, v3); 26 | return result; 27 | } 28 | 29 | public static Map of(String k1, V v1, String k2, V v2, String k3, V v3, String k4, V v4) { 30 | Map result = of(k1, v1, k2, v2, k3, v3); 31 | result.put(k4, v4); 32 | return result; 33 | } 34 | 35 | public static Map of(String k1, V v1, String k2, V v2, String k3, V v3, String k4, V v4, String k5, V v5) { 36 | Map result = of(k1, v1, k2, v2, k3, v3, k4, v4); 37 | result.put(k5, v5); 38 | return result; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/rx/marble/MapHelper.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | import java.util.Collections; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | /** 8 | * Helper inspired from guava's ImmutableMap.of() factory method 9 | */ 10 | public class MapHelper { 11 | 12 | public static Map of(String k, V v) { 13 | return Collections.singletonMap(k,v); 14 | } 15 | 16 | public static Map of(String k1, V v1, String k2, V v2) { 17 | Map map = new HashMap<>(); 18 | map.put(k1, v1); 19 | map.put(k2, v2); 20 | return map; 21 | } 22 | 23 | public static Map of(String k1, V v1, String k2, V v2, String k3, V v3) { 24 | Map result = of(k1, v1, k2, v2); 25 | result.put(k3, v3); 26 | return result; 27 | } 28 | 29 | public static Map of(String k1, V v1, String k2, V v2, String k3, V v3, String k4, V v4) { 30 | Map result = of(k1, v1, k2, v2, k3, v3); 31 | result.put(k4, v4); 32 | return result; 33 | } 34 | 35 | public static Map of(String k1, V v1, String k2, V v2, String k3, V v3, String k4, V v4, String k5, V v5) { 36 | Map result = of(k1, v1, k2, v2, k3, v3, k4, v4); 37 | result.put(k5, v5); 38 | return result; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/io/reactivex/marble/MapHelper.java: -------------------------------------------------------------------------------- 1 | package io.reactivex.marble; 2 | 3 | import java.util.Collections; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | /** 8 | * Helper inspired from guava's ImmutableMap.of() factory method 9 | */ 10 | public class MapHelper { 11 | 12 | public static Map of(String k, V v) { 13 | return Collections.singletonMap(k,v); 14 | } 15 | 16 | public static Map of(String k1, V v1, String k2, V v2) { 17 | Map map = new HashMap<>(); 18 | map.put(k1, v1); 19 | map.put(k2, v2); 20 | return map; 21 | } 22 | 23 | public static Map of(String k1, V v1, String k2, V v2, String k3, V v3) { 24 | Map result = of(k1, v1, k2, v2); 25 | result.put(k3, v3); 26 | return result; 27 | } 28 | 29 | public static Map of(String k1, V v1, String k2, V v2, String k3, V v3, String k4, V v4) { 30 | Map result = of(k1, v1, k2, v2, k3, v3); 31 | result.put(k4, v4); 32 | return result; 33 | } 34 | 35 | public static Map of(String k1, V v1, String k2, V v2, String k3, V v3, String k4, V v4, String k5, V v5) { 36 | Map result = of(k1, v1, k2, v2, k3, v3, k4, v4); 37 | result.put(k5, v5); 38 | return result; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/rx/marble/SubscriptionLog.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | /** 4 | * Created by Alexandre Victoor on 08/06/2016. 5 | */ 6 | public class SubscriptionLog { 7 | public final long subscribe; 8 | public final long unsubscribe; 9 | 10 | public SubscriptionLog(long subscribe) { 11 | this.subscribe = subscribe; 12 | this.unsubscribe = Long.MAX_VALUE; 13 | } 14 | 15 | public SubscriptionLog(long subscribe, long unsubscribe) { 16 | this.subscribe = subscribe; 17 | this.unsubscribe = unsubscribe; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return "SubscriptionLog{" + 23 | "subscribe=" + subscribe + 24 | ", unsubscribe=" + unsubscribe + 25 | '}'; 26 | } 27 | 28 | @Override 29 | public boolean equals(Object o) { 30 | if (this == o) return true; 31 | if (o == null || getClass() != o.getClass()) return false; 32 | 33 | SubscriptionLog that = (SubscriptionLog) o; 34 | 35 | if (subscribe != that.subscribe) return false; 36 | return unsubscribe == that.unsubscribe; 37 | 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | int result = (int) (subscribe ^ (subscribe >>> 32)); 43 | result = 31 * result + (int) (unsubscribe ^ (unsubscribe >>> 32)); 44 | return result; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/reactor/HotFlux.java: -------------------------------------------------------------------------------- 1 | package reactor; 2 | 3 | import org.reactivestreams.*; 4 | import reactor.core.publisher.Flux; 5 | import reactor.core.scheduler.Scheduler; 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | /** 11 | * Created by Alexandre Victoor on 18/04/2017. 12 | */ 13 | public class HotFlux extends Flux implements TestablePublisher { 14 | 15 | private final TestablePublisher publisher; 16 | 17 | protected HotFlux(TestablePublisher publisher) { 18 | this.publisher = publisher; 19 | } 20 | 21 | @Override 22 | public void subscribe(Subscriber s) { 23 | publisher.subscribe(s); 24 | } 25 | 26 | @Override 27 | public List getSubscriptions() { 28 | return publisher.getSubscriptions(); 29 | } 30 | 31 | @Override 32 | public List> getMessages() { 33 | return publisher.getMessages(); 34 | } 35 | 36 | public static HotFlux create(Scheduler scheduler, Recorded... notifications) { 37 | return create(scheduler, Arrays.asList(notifications)); 38 | } 39 | 40 | public static HotFlux create(Scheduler scheduler, List> notifications) { 41 | HotPublisher hotPublisher = new HotPublisher<>(new SchedulerAdapter(scheduler), notifications); 42 | return new HotFlux<>(hotPublisher); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/reactivex/marble/ObserverAdapter.java: -------------------------------------------------------------------------------- 1 | package io.reactivex.marble; 2 | 3 | import io.reactivex.Observer; 4 | import io.reactivex.disposables.Disposable; 5 | import org.reactivestreams.Subscriber; 6 | import org.reactivestreams.Subscription; 7 | 8 | /** 9 | * Created by Alexandre Victoor on 23/04/2017. 10 | */ 11 | class ObserverAdapter implements Subscriber { 12 | 13 | private Observer observer; 14 | 15 | ObserverAdapter(Observer observer) { 16 | this.observer = observer; 17 | } 18 | 19 | @Override 20 | public void onSubscribe(final Subscription subscription) { 21 | observer.onSubscribe(new Disposable() { 22 | 23 | private boolean disposed = false; 24 | 25 | @Override 26 | public void dispose() { 27 | disposed = true; 28 | subscription.cancel(); 29 | } 30 | 31 | @Override 32 | public boolean isDisposed() { 33 | return disposed; 34 | } 35 | }); 36 | subscription.request(Long.MAX_VALUE); 37 | 38 | } 39 | 40 | @Override 41 | public void onNext(T t) { 42 | observer.onNext(t); 43 | } 44 | 45 | @Override 46 | public void onError(Throwable t) { 47 | observer.onError(t); 48 | } 49 | 50 | @Override 51 | public void onComplete() { 52 | observer.onComplete(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/org/reactivestreams/SubscriptionLog.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | /** 4 | * Created by Alexandre Victoor on 08/06/2016. 5 | */ 6 | public class SubscriptionLog { 7 | public final long subscribe; 8 | public final long unsubscribe; 9 | 10 | public SubscriptionLog(long subscribe) { 11 | this.subscribe = subscribe; 12 | this.unsubscribe = Long.MAX_VALUE; 13 | } 14 | 15 | public SubscriptionLog(long subscribe, long unsubscribe) { 16 | this.subscribe = subscribe; 17 | this.unsubscribe = unsubscribe; 18 | } 19 | 20 | public boolean doesNeverEnd() { 21 | return unsubscribe == Long.MAX_VALUE; 22 | } 23 | 24 | @Override 25 | public String toString() { 26 | return "SubscriptionLog{" + 27 | "subscribe=" + subscribe + 28 | ", unsubscribe=" + unsubscribe + 29 | '}'; 30 | } 31 | 32 | @Override 33 | public boolean equals(Object o) { 34 | if (this == o) return true; 35 | if (o == null || getClass() != o.getClass()) return false; 36 | 37 | SubscriptionLog that = (SubscriptionLog) o; 38 | 39 | if (subscribe != that.subscribe) return false; 40 | return unsubscribe == that.unsubscribe; 41 | 42 | } 43 | 44 | @Override 45 | public int hashCode() { 46 | int result = (int) (subscribe ^ (subscribe >>> 32)); 47 | result = 31 * result + (int) (unsubscribe ^ (unsubscribe >>> 32)); 48 | return result; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/io/reactivex/marble/HotObservable.java: -------------------------------------------------------------------------------- 1 | package io.reactivex.marble; 2 | 3 | import io.reactivex.Observable; 4 | import io.reactivex.Observer; 5 | import io.reactivex.Scheduler; 6 | import org.reactivestreams.*; 7 | 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | 12 | public class HotObservable extends Observable implements TestablePublisher { 13 | 14 | private final TestablePublisher publisher; 15 | 16 | protected HotObservable(TestablePublisher publisher) { 17 | this.publisher = publisher; 18 | } 19 | 20 | @Override 21 | public void subscribe(Subscriber s) { 22 | publisher.subscribe(s); 23 | } 24 | 25 | @Override 26 | protected void subscribeActual(final Observer observer) { 27 | publisher.subscribe(new ObserverAdapter<>(observer)); 28 | } 29 | 30 | @Override 31 | public List getSubscriptions() { 32 | return publisher.getSubscriptions(); 33 | } 34 | 35 | @Override 36 | public List> getMessages() { 37 | return publisher.getMessages(); 38 | } 39 | 40 | public static HotObservable create(Scheduler scheduler, Recorded... notifications) { 41 | return create(scheduler, Arrays.asList(notifications)); 42 | } 43 | 44 | public static HotObservable create(Scheduler scheduler, List> notifications) { 45 | HotPublisher hotPublisher = new HotPublisher<>(new SchedulerAdapter(scheduler), notifications); 46 | return new HotObservable<>(hotPublisher); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/reactor/ColdFlux.java: -------------------------------------------------------------------------------- 1 | package reactor; 2 | 3 | import org.reactivestreams.*; 4 | import reactor.core.publisher.Flux; 5 | import reactor.core.scheduler.Scheduler; 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | /** 11 | * Created by Alexandre Victoor on 18/04/2017. 12 | */ 13 | public class ColdFlux extends Flux implements TestablePublisher { 14 | 15 | private final TestablePublisher publisher; 16 | 17 | protected ColdFlux(TestablePublisher publisher) { 18 | this.publisher = publisher; 19 | } 20 | 21 | @Override 22 | public void subscribe(Subscriber s) { 23 | publisher.subscribe(s); 24 | } 25 | 26 | @Override 27 | public List getSubscriptions() { 28 | return publisher.getSubscriptions(); 29 | } 30 | 31 | @Override 32 | public List> getMessages() { 33 | return publisher.getMessages(); 34 | } 35 | 36 | public static ColdFlux create(Scheduler scheduler, Recorded... notifications) { 37 | return create(scheduler, Arrays.asList(notifications)); 38 | } 39 | 40 | public static ColdFlux create(final Scheduler scheduler, List> notifications) { 41 | 42 | ColdPublisher coldPublisher = new ColdPublisher<>(new SchedulerFactory() { 43 | @Override 44 | public org.reactivestreams.Scheduler create() { 45 | return new SchedulerAdapter(scheduler); 46 | } 47 | }, notifications); 48 | 49 | return new ColdFlux<>(coldPublisher); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/io/reactivex/marble/ColdObservable.java: -------------------------------------------------------------------------------- 1 | package io.reactivex.marble; 2 | 3 | 4 | import io.reactivex.Observable; 5 | import io.reactivex.Observer; 6 | import io.reactivex.Scheduler; 7 | import org.reactivestreams.*; 8 | 9 | import java.util.Arrays; 10 | import java.util.List; 11 | 12 | 13 | public class ColdObservable extends Observable implements TestablePublisher { 14 | 15 | private final TestablePublisher publisher; 16 | 17 | protected ColdObservable(TestablePublisher publisher) { 18 | this.publisher = publisher; 19 | } 20 | 21 | @Override 22 | public void subscribe(Subscriber s) { 23 | publisher.subscribe(s); 24 | } 25 | 26 | @Override 27 | protected void subscribeActual(Observer observer) { 28 | publisher.subscribe(new ObserverAdapter<>(observer)); 29 | } 30 | 31 | @Override 32 | public List getSubscriptions() { 33 | return publisher.getSubscriptions(); 34 | } 35 | 36 | @Override 37 | public List> getMessages() { 38 | return publisher.getMessages(); 39 | } 40 | 41 | public static ColdObservable create(Scheduler scheduler, Recorded... notifications) { 42 | return create(scheduler, Arrays.asList(notifications)); 43 | } 44 | 45 | public static ColdObservable create(final Scheduler scheduler, List> notifications) { 46 | 47 | ColdPublisher coldPublisher = new ColdPublisher<>(new SchedulerFactory() { 48 | @Override 49 | public org.reactivestreams.Scheduler create() { 50 | return new SchedulerAdapter(scheduler); 51 | } 52 | }, notifications); 53 | 54 | return new ColdObservable<>(coldPublisher); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/reactivestreams/Recorded.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | /** 4 | * Created by Alexandre Victoor on 05/06/2016. 5 | */ 6 | public class Recorded { 7 | 8 | public final Notification value; 9 | public final long time; 10 | 11 | public Recorded(long time, Notification value) { 12 | this.time = time; 13 | this.value = value; 14 | } 15 | 16 | @Override 17 | public String toString() { 18 | 19 | final String valueString; 20 | 21 | switch (value.getKind()) { 22 | case OnComplete: 23 | valueString = "On Complete"; 24 | break; 25 | case OnError: 26 | valueString = "On Error"; 27 | break; 28 | default: 29 | valueString = "On Next: " + value.getValue(); 30 | } 31 | 32 | 33 | return "{\n" + 34 | " time = " + time + 35 | "\n " + valueString + 36 | "\n}"; 37 | } 38 | 39 | @Override 40 | public boolean equals(Object o) { 41 | if (this == o) return true; 42 | if (o == null || getClass() != o.getClass()) return false; 43 | 44 | Recorded recorded = (Recorded) o; 45 | 46 | if (time != recorded.time) return false; 47 | 48 | return !(value != null ? ! notificationsAreEqual(value, recorded.value) : recorded.value != null); 49 | } 50 | 51 | private boolean notificationsAreEqual(Notification first, Notification second) { 52 | if (first == null || second == null) { 53 | return false; 54 | } 55 | 56 | if (first.isOnError() && second.isOnError()) { 57 | // we do not do deep comparisons on exceptions 58 | return true; 59 | } 60 | return first.equals(second); 61 | } 62 | 63 | @Override 64 | public int hashCode() { 65 | int result = value != null ? value.hashCode() : 0; 66 | result = 31 * result + (int) (time ^ (time >>> 32)); 67 | return result; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/rx/marble/Recorded.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | import rx.Notification; 4 | 5 | /** 6 | * Created by Alexandre Victoor on 05/06/2016. 7 | */ 8 | public class Recorded { 9 | 10 | public final Notification value; 11 | public final long time; 12 | 13 | public Recorded(long time, Notification value) { 14 | this.time = time; 15 | this.value = value; 16 | } 17 | 18 | 19 | @Override 20 | public String toString() { 21 | 22 | final String valueString; 23 | switch (value.getKind()) { 24 | case OnCompleted: 25 | valueString = "On Completed"; 26 | break; 27 | case OnError: 28 | valueString = "On Error"; 29 | break; 30 | default: 31 | valueString = "On Next: " + value.getValue(); 32 | } 33 | 34 | 35 | return "{\n" + 36 | " time = " + time + 37 | "\n " + valueString + 38 | "\n}"; 39 | } 40 | 41 | @Override 42 | public boolean equals(Object o) { 43 | if (this == o) return true; 44 | if (o == null || getClass() != o.getClass()) return false; 45 | 46 | Recorded recorded = (Recorded) o; 47 | 48 | if (time != recorded.time) return false; 49 | 50 | return !(value != null ? ! notificationsAreEqual(value, recorded.value) : recorded.value != null); 51 | } 52 | 53 | private boolean notificationsAreEqual(Notification first, Notification second) { 54 | if (first == null || second == null) { 55 | return false; 56 | } 57 | if ((first.getKind() == second.getKind()) && (first.getKind() == Notification.Kind.OnError)) { 58 | // we do not do deep comparisons on exceptions 59 | return true; 60 | } 61 | return first.equals(second); 62 | } 63 | 64 | @Override 65 | public int hashCode() { 66 | int result = value != null ? value.hashCode() : 0; 67 | result = 31 * result + (int) (time ^ (time >>> 32)); 68 | return result; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/rx/marble/junit/DemoTest.java: -------------------------------------------------------------------------------- 1 | package rx.marble.junit; 2 | 3 | 4 | import org.junit.Rule; 5 | import org.junit.Test; 6 | import rx.Observable; 7 | import rx.functions.Func1; 8 | import rx.functions.Func2; 9 | import rx.marble.ColdObservable; 10 | 11 | import java.util.Map; 12 | 13 | import static rx.marble.MapHelper.of; 14 | import static rx.marble.junit.MarbleRule.*; 15 | 16 | public class DemoTest { 17 | 18 | @Rule 19 | public MarbleRule marble = new MarbleRule(); 20 | 21 | @Test 22 | public void should_map() { 23 | // given 24 | Observable input = hot("a-b-c-d"); 25 | // when 26 | Observable output = input.map(new Func1() { 27 | @Override 28 | public String call(String s) { 29 | return s.toUpperCase(); 30 | } 31 | }); 32 | // then 33 | expectObservable(output).toBe("A-B-C-D"); 34 | } 35 | 36 | @Test 37 | public void should_sum() { 38 | // given 39 | Observable input = cold("a-b-c-d", of("a", 1, "b", 2, "c", 3, "d", 4)); 40 | // when 41 | Observable output = input.scan(new Func2() { 42 | @Override 43 | public Integer call(Integer first, Integer second) { 44 | return first + second; 45 | } 46 | }); 47 | // then 48 | expectObservable(output).toBe("A-B-C-D", of("A", 1, "B", 3, "C", 6, "D", 10)); 49 | } 50 | 51 | @Test 52 | public void should_be_awesome() { 53 | Map values = of("a", 1, "b", 2); 54 | ColdObservable myObservable 55 | = cold( "---a---b--|", values); 56 | String subs = "^---------!"; 57 | expectObservable(myObservable).toBe("---a---b--|", values); 58 | expectSubscriptions(myObservable.getSubscriptions()).toBe(subs); 59 | } 60 | 61 | @Test 62 | public void should_use_unsubscription_diagram() { 63 | Observable source = hot("---^-a-b-|"); 64 | String unsubscribe = "---!"; 65 | String expected = "--a"; 66 | expectObservable(source, unsubscribe).toBe(expected); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/rx/marble/junit/MarbleRule.java: -------------------------------------------------------------------------------- 1 | package rx.marble.junit; 2 | 3 | 4 | import org.junit.rules.TestRule; 5 | import org.junit.runner.Description; 6 | import org.junit.runners.model.Statement; 7 | import rx.Observable; 8 | import rx.marble.*; 9 | 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | public class MarbleRule implements TestRule { 14 | 15 | private static ThreadLocal schedulerHolder = new ThreadLocal<>(); 16 | 17 | public final MarbleScheduler scheduler; 18 | 19 | public MarbleRule() { 20 | scheduler = new MarbleScheduler(); 21 | } 22 | 23 | public MarbleRule(long frameTimeFactor) { 24 | scheduler = new MarbleScheduler(frameTimeFactor); 25 | } 26 | 27 | public static HotObservable hot(String marbles, Map values) { 28 | return schedulerHolder.get().createHotObservable(marbles, values); 29 | } 30 | 31 | public static HotObservable hot(String marbles) { 32 | return schedulerHolder.get().createHotObservable(marbles); 33 | } 34 | 35 | public static ColdObservable cold(String marbles, Map values) { 36 | return schedulerHolder.get().createColdObservable(marbles, values); 37 | } 38 | 39 | public static ColdObservable cold(String marbles) { 40 | return schedulerHolder.get().createColdObservable(marbles); 41 | } 42 | 43 | public static ISetupTest expectObservable(Observable actual) { 44 | return schedulerHolder.get().expectObservable(actual); 45 | } 46 | 47 | public static ISetupTest expectObservable(Observable actual, String unsubscriptionMarbles) { 48 | return schedulerHolder.get().expectObservable(actual, unsubscriptionMarbles); 49 | } 50 | 51 | public static ISetupSubscriptionsTest expectSubscriptions(List subscriptions) { 52 | return schedulerHolder.get().expectSubscriptions(subscriptions); 53 | } 54 | 55 | @Override 56 | public Statement apply(final Statement base, Description description) { 57 | return new Statement() { 58 | @Override 59 | public void evaluate() throws Throwable { 60 | schedulerHolder.set(scheduler); 61 | try { 62 | base.evaluate(); 63 | scheduler.flush(); 64 | } finally { 65 | schedulerHolder.remove(); 66 | } 67 | 68 | } 69 | }; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/reactor/junit/DemoTest.java: -------------------------------------------------------------------------------- 1 | package reactor.junit; 2 | 3 | import org.junit.Rule; 4 | import org.junit.Test; 5 | import reactor.ColdFlux; 6 | import reactor.core.publisher.Flux; 7 | import reactor.core.publisher.Mono; 8 | 9 | import java.util.Map; 10 | import java.util.function.BiFunction; 11 | import java.util.function.Function; 12 | 13 | import static reactor.MapHelper.*; 14 | import static reactor.junit.MarbleRule.expectFlux; 15 | import static reactor.junit.MarbleRule.*; 16 | 17 | /** 18 | * Created by Alexandre Victoor on 26/04/2017. 19 | */ 20 | public class DemoTest { 21 | 22 | @Rule 23 | public MarbleRule marble = new MarbleRule(); 24 | 25 | @Test 26 | public void should_map() { 27 | // given 28 | Flux input = hot("a-b-c-d"); 29 | // when 30 | Flux output = input.map(new Function() { 31 | @Override 32 | public String apply(String s) { 33 | return s.toUpperCase(); 34 | } 35 | }); 36 | // then 37 | expectFlux(output).toBe("A-B-C-D"); 38 | } 39 | 40 | @Test 41 | public void should_sum() { 42 | // given 43 | Flux input = cold("a-b-c-d", of("a", 1, "b", 2, "c", 3, "d", 4)); 44 | // when 45 | Flux output = input.scan(new BiFunction() { 46 | @Override 47 | public Integer apply(Integer first, Integer second) { 48 | return first + second; 49 | } 50 | }); 51 | // then 52 | expectFlux(output).toBe("A-B-C-D", of("A", 1, "B", 3, "C", 6, "D", 10)); 53 | } 54 | 55 | @Test 56 | public void should_be_awesome() { 57 | Map values = of("a", 1, "b", 2); 58 | ColdFlux myFlux 59 | = cold( "---a---b--|", values); 60 | String subs = "^---------!"; 61 | expectFlux(myFlux).toBe("---a---b--|", values); 62 | expectSubscriptions(myFlux.getSubscriptions()).toBe(subs); 63 | } 64 | 65 | @Test 66 | public void should_use_unsubscription_diagram() { 67 | Flux source = hot("---^-a-b-|"); 68 | String unsubscribe = "---!"; 69 | String expected = "--a"; 70 | expectFlux(source, unsubscribe).toBe(expected); 71 | } 72 | 73 | @Test 74 | public void should_map_mono() { 75 | // given 76 | Mono input = hot("--(a|)").next(); 77 | // when 78 | Mono output = input.map(new Function() { 79 | @Override 80 | public String apply(String s) { 81 | return s.toUpperCase(); 82 | } 83 | }); 84 | // then 85 | expectMono(output).toBe("--(A|)"); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/reactor/MarbleScheduler.java: -------------------------------------------------------------------------------- 1 | package reactor; 2 | 3 | import org.reactivestreams.*; 4 | import reactor.core.publisher.Flux; 5 | import reactor.test.scheduler.VirtualTimeScheduler; 6 | 7 | import java.time.Instant; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | /** 13 | * Created by Alexandre Victoor on 20/04/2017. 14 | */ 15 | public class MarbleScheduler extends VirtualTimeScheduler { 16 | private final MarbleSchedulerState state; 17 | private final long frameTimeFactor; 18 | 19 | public MarbleScheduler() { 20 | this(10); 21 | } 22 | 23 | public MarbleScheduler(long frameTimeFactor) { 24 | this.frameTimeFactor = frameTimeFactor; 25 | state = new MarbleSchedulerState(frameTimeFactor, new MarbleSchedulerState.ISchedule() { 26 | @Override 27 | public long now() { 28 | return MarbleScheduler.this.now(TimeUnit.MILLISECONDS); 29 | } 30 | 31 | @Override 32 | public void schedule(Runnable runnable, long time) { 33 | MarbleScheduler.this.schedule(runnable, time, TimeUnit.MILLISECONDS); 34 | } 35 | }, getClass()); 36 | } 37 | 38 | 39 | public ColdFlux createColdFlux(String marbles, Map values) { 40 | List> notifications = Parser.parseMarbles(marbles, values, null, frameTimeFactor); 41 | return ColdFlux.create(this, notifications); 42 | } 43 | 44 | public ColdFlux createColdFlux(String marbles) { 45 | return createColdFlux(marbles, null); 46 | } 47 | 48 | public HotFlux createHotFlux(String marbles, Map values) { 49 | List> notifications = Parser.parseMarbles(marbles, values, null, frameTimeFactor); 50 | return HotFlux.create(this, notifications); 51 | } 52 | 53 | public HotFlux createHotFlux(String marbles) { 54 | return createHotFlux(marbles, null); 55 | } 56 | 57 | 58 | public long createTime(String marbles) { 59 | int endIndex = marbles.indexOf("|"); 60 | if (endIndex == -1) { 61 | throw new RuntimeException("Marble diagram for time should have a completion marker '|'"); 62 | } 63 | 64 | return endIndex * frameTimeFactor; 65 | } 66 | 67 | 68 | public void flush() { 69 | advanceTimeTo(Instant.ofEpochMilli(Long.MAX_VALUE)); 70 | state.flush(); 71 | } 72 | 73 | public ISetupTest expectFlux(Flux flux) { 74 | return expectFlux(flux, null); 75 | } 76 | 77 | public ISetupTest expectFlux(Flux flux, String unsubscriptionMarbles) { 78 | return state.expectPublisher(flux, unsubscriptionMarbles); 79 | } 80 | 81 | public ISetupSubscriptionsTest expectSubscriptions(List subscriptions) { 82 | return state.expectSubscriptions(subscriptions); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/org/reactivestreams/ColdPublisher.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | /** 9 | * Created by Alexandre Victoor on 18/04/2017. 10 | */ 11 | public class ColdPublisher implements TestablePublisher { 12 | 13 | private final SchedulerFactory schedulerFactory; 14 | private final List> recordedNotifications; 15 | private final List subscriptions = new ArrayList<>(); 16 | 17 | 18 | public ColdPublisher(SchedulerFactory schedulerFactory, List> notifications) { 19 | this.schedulerFactory = schedulerFactory; 20 | this.recordedNotifications = notifications; 21 | } 22 | 23 | @Override 24 | public void subscribe(final Subscriber observer) { 25 | final Scheduler scheduler = schedulerFactory.create(); 26 | final SubscriptionLog subscriptionLog = new SubscriptionLog(scheduler.now(TimeUnit.MILLISECONDS)); 27 | subscriptions.add(subscriptionLog); 28 | final int subscriptionIndex = subscriptions.size() - 1; 29 | for (final Recorded event: recordedNotifications) { 30 | scheduler.schedule(new Runnable() { 31 | @Override 32 | public void run() { 33 | event.value.accept(observer); 34 | if (!event.value.isOnNext()) { 35 | endSubscriptions(event.time); 36 | } 37 | } 38 | }, event.time, TimeUnit.MILLISECONDS); 39 | } 40 | 41 | observer.onSubscribe(new Subscription() { 42 | 43 | private boolean disposed = false; 44 | 45 | @Override 46 | public void request(long n) { 47 | // TODO 48 | } 49 | 50 | @Override 51 | public void cancel() { 52 | disposed = true; 53 | subscriptions.set( 54 | subscriptionIndex, 55 | new SubscriptionLog(subscriptionLog.subscribe, scheduler.now(TimeUnit.MILLISECONDS)) 56 | ); 57 | scheduler.dispose(); 58 | } 59 | 60 | }); 61 | } 62 | 63 | private void endSubscriptions(long time) { 64 | for (int i = 0; i < subscriptions.size(); i++) { 65 | SubscriptionLog subscription = subscriptions.get(i); 66 | if (subscription.doesNeverEnd()) { 67 | subscriptions.set(i, new SubscriptionLog(subscription.subscribe, time)); 68 | } 69 | } 70 | } 71 | 72 | @Override 73 | public List getSubscriptions() { 74 | return Collections.unmodifiableList(subscriptions); 75 | } 76 | 77 | @Override 78 | public List> getMessages() { 79 | return Collections.unmodifiableList(recordedNotifications); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/reactor/junit/MarbleRule.java: -------------------------------------------------------------------------------- 1 | package reactor.junit; 2 | 3 | import org.junit.rules.TestRule; 4 | import org.junit.runner.Description; 5 | import org.junit.runners.model.Statement; 6 | import org.reactivestreams.ISetupSubscriptionsTest; 7 | import org.reactivestreams.ISetupTest; 8 | import org.reactivestreams.SubscriptionLog; 9 | import reactor.ColdFlux; 10 | import reactor.HotFlux; 11 | import reactor.MarbleScheduler; 12 | import reactor.core.publisher.Flux; 13 | import reactor.core.publisher.Mono; 14 | 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | /** 19 | * Created by Alexandre Victoor on 26/04/2017. 20 | */ 21 | public class MarbleRule implements TestRule { 22 | 23 | private static ThreadLocal schedulerHolder = new ThreadLocal<>(); 24 | 25 | public final MarbleScheduler scheduler; 26 | 27 | public MarbleRule() { 28 | scheduler = new MarbleScheduler(); 29 | } 30 | 31 | public MarbleRule(long frameTimeFactor) { 32 | scheduler = new MarbleScheduler(frameTimeFactor); 33 | } 34 | 35 | public static HotFlux hot(String marbles, Map values) { 36 | return schedulerHolder.get().createHotFlux(marbles, values); 37 | } 38 | 39 | public static HotFlux hot(String marbles) { 40 | return schedulerHolder.get().createHotFlux(marbles); 41 | } 42 | 43 | public static ColdFlux cold(String marbles, Map values) { 44 | return schedulerHolder.get().createColdFlux(marbles, values); 45 | } 46 | 47 | public static ColdFlux cold(String marbles) { 48 | return schedulerHolder.get().createColdFlux(marbles); 49 | } 50 | 51 | public static ISetupTest expectFlux(Flux actual) { 52 | return schedulerHolder.get().expectFlux(actual); 53 | } 54 | 55 | public static ISetupTest expectFlux(Flux actual, String unsubscriptionMarbles) { 56 | return schedulerHolder.get().expectFlux(actual, unsubscriptionMarbles); 57 | } 58 | 59 | public static ISetupTest expectMono(Mono actual) { 60 | return schedulerHolder.get().expectFlux(actual.flux()); 61 | } 62 | 63 | public static ISetupTest expectMono(Mono actual, String unsubscriptionMarbles) { 64 | return schedulerHolder.get().expectFlux(actual.flux(), unsubscriptionMarbles); 65 | } 66 | 67 | public static ISetupSubscriptionsTest expectSubscriptions(List subscriptions) { 68 | return schedulerHolder.get().expectSubscriptions(subscriptions); 69 | } 70 | 71 | @Override 72 | public Statement apply(final Statement base, Description description) { 73 | return new Statement() { 74 | @Override 75 | public void evaluate() throws Throwable { 76 | schedulerHolder.set(scheduler); 77 | try { 78 | base.evaluate(); 79 | scheduler.flush(); 80 | } finally { 81 | schedulerHolder.remove(); 82 | } 83 | 84 | } 85 | }; 86 | } 87 | } -------------------------------------------------------------------------------- /src/main/java/org/reactivestreams/HotPublisher.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | /** 9 | * Created by Alexandre Victoor on 18/04/2017. 10 | */ 11 | public class HotPublisher implements Publisher, TestablePublisher { 12 | 13 | private final List> notifications; 14 | private final List> observers = new ArrayList<>(); 15 | private final Scheduler scheduler; 16 | List subscriptions = new ArrayList<>(); 17 | 18 | public HotPublisher(Scheduler scheduler, List> notifications) { 19 | this.scheduler = scheduler; 20 | this.notifications = notifications; 21 | scheduleNotifications(); 22 | } 23 | 24 | private void scheduleNotifications() { 25 | for (final Recorded event : notifications) { 26 | scheduler.schedule(new Runnable() { 27 | @Override 28 | public void run() { 29 | for (Subscriber observer : new ArrayList<>(observers)){ 30 | event.value.accept(observer); 31 | if (!event.value.isOnNext()) { 32 | endSubscriptions(event.time); 33 | } 34 | } 35 | } 36 | }, event.time, TimeUnit.MILLISECONDS); 37 | } 38 | } 39 | 40 | private void endSubscriptions(long time) { 41 | for (int i = 0; i < subscriptions.size(); i++) { 42 | SubscriptionLog subscription = subscriptions.get(i); 43 | if (subscription.doesNeverEnd()) { 44 | subscriptions.set(i, new SubscriptionLog(subscription.subscribe, time)); 45 | } 46 | } 47 | } 48 | 49 | @Override 50 | public void subscribe(final Subscriber subscriber) { 51 | 52 | observers.add(subscriber); 53 | 54 | final SubscriptionLog subscriptionLog = new SubscriptionLog(scheduler.now(TimeUnit.MILLISECONDS)); 55 | subscriptions.add(subscriptionLog); 56 | final int subscriptionIndex = subscriptions.size() - 1; 57 | 58 | subscriber.onSubscribe(new Subscription() { 59 | @Override 60 | public void request(long l) { 61 | // TODO check if useful 62 | } 63 | 64 | @Override 65 | public void cancel() { 66 | observers.remove(subscriber); 67 | subscriptions.set( 68 | subscriptionIndex, 69 | new SubscriptionLog(subscriptionLog.subscribe, scheduler.now(TimeUnit.MILLISECONDS)) 70 | ); 71 | } 72 | }); 73 | } 74 | 75 | @Override 76 | public List getSubscriptions() { 77 | return Collections.unmodifiableList(subscriptions); 78 | } 79 | 80 | @Override 81 | public List> getMessages() { 82 | return Collections.unmodifiableList(notifications); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/rx/marble/ColdObservable.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | import rx.Observable; 4 | import rx.Scheduler; 5 | import rx.Subscriber; 6 | import rx.functions.Action0; 7 | import rx.subscriptions.Subscriptions; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.Collections; 12 | import java.util.List; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | 16 | public class ColdObservable extends Observable implements TestableObservable { 17 | 18 | private final List> notifications; 19 | private List subscriptions = new ArrayList<>(); 20 | 21 | private ColdObservable(OnSubscribe f, List> notifications) { 22 | super(f); 23 | this.notifications = notifications; 24 | } 25 | 26 | @Override 27 | public List getSubscriptions() { 28 | return Collections.unmodifiableList(subscriptions); 29 | } 30 | 31 | @Override 32 | public List> getMessages() { 33 | return Collections.unmodifiableList(notifications); 34 | } 35 | 36 | public static ColdObservable create(Scheduler scheduler, Recorded... notifications) { 37 | return create(scheduler, Arrays.asList(notifications)); 38 | } 39 | 40 | public static ColdObservable create(Scheduler scheduler, List> notifications) { 41 | OnSubscribeHandler onSubscribeFunc = new OnSubscribeHandler<>(scheduler, notifications); 42 | ColdObservable observable = new ColdObservable<>(onSubscribeFunc, notifications); 43 | onSubscribeFunc.observable = observable; 44 | return observable; 45 | } 46 | 47 | 48 | private static class OnSubscribeHandler implements Observable.OnSubscribe { 49 | 50 | private final Scheduler scheduler; 51 | private final List> notifications; 52 | public ColdObservable observable; 53 | 54 | public OnSubscribeHandler(Scheduler scheduler, List> notifications) { 55 | this.scheduler = scheduler; 56 | this.notifications = notifications; 57 | } 58 | 59 | public void call(final Subscriber subscriber) { 60 | final SubscriptionLog subscriptionLog = new SubscriptionLog(scheduler.now()); 61 | observable.subscriptions.add(subscriptionLog); 62 | final int subscriptionIndex = observable.getSubscriptions().size() - 1; 63 | Scheduler.Worker worker = scheduler.createWorker(); 64 | subscriber.add(worker); // not scheduling after unsubscribe 65 | 66 | for (final Recorded notification: notifications) { 67 | worker.schedule(new Action0() { 68 | @Override 69 | public void call() { 70 | notification.value.accept(subscriber); 71 | } 72 | }, notification.time, TimeUnit.MILLISECONDS); 73 | } 74 | 75 | subscriber.add((Subscriptions.create(new Action0() { 76 | @Override 77 | public void call() { 78 | // on unsubscribe 79 | observable.subscriptions.set( 80 | subscriptionIndex, 81 | new SubscriptionLog(subscriptionLog.subscribe, scheduler.now()) 82 | ); 83 | } 84 | }))); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/rx/marble/HotObservable.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | import rx.Observable; 4 | import rx.Scheduler; 5 | import rx.Subscriber; 6 | import rx.functions.Action0; 7 | import rx.subscriptions.Subscriptions; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.Collections; 12 | import java.util.List; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | 16 | public class HotObservable extends Observable implements TestableObservable { 17 | 18 | private final List> notifications; 19 | List subscriptions = new ArrayList<>(); 20 | 21 | protected HotObservable(OnSubscribe f, List> notifications) { 22 | super(f); 23 | this.notifications = notifications; 24 | } 25 | 26 | @Override 27 | public List getSubscriptions() { 28 | return Collections.unmodifiableList(subscriptions); 29 | } 30 | 31 | @Override 32 | public List> getMessages() { 33 | return Collections.unmodifiableList(notifications); 34 | } 35 | 36 | public static HotObservable create(Scheduler scheduler, Recorded... notifications) { 37 | return create(scheduler, Arrays.asList(notifications)); 38 | } 39 | 40 | public static HotObservable create(Scheduler scheduler, List> notifications) { 41 | OnSubscribeHandler onSubscribeFunc = new OnSubscribeHandler<>(scheduler, notifications); 42 | HotObservable observable = new HotObservable<>(onSubscribeFunc, notifications); 43 | onSubscribeFunc.observable = observable; 44 | return observable; 45 | } 46 | 47 | private static class OnSubscribeHandler implements Observable.OnSubscribe { 48 | 49 | private final Scheduler scheduler; 50 | private final List> subscribers = new ArrayList<>(); 51 | public HotObservable observable; 52 | 53 | public OnSubscribeHandler(Scheduler scheduler, List> notifications) { 54 | this.scheduler = scheduler; 55 | Scheduler.Worker worker = scheduler.createWorker(); 56 | for (final Recorded event : notifications) { 57 | worker.schedule(new Action0() { 58 | @Override 59 | public void call() { 60 | List> subscribers 61 | = new ArrayList<>(OnSubscribeHandler.this.subscribers); 62 | for (Subscriber subscriber : subscribers){ 63 | event.value.accept(subscriber); 64 | } 65 | } 66 | }, event.time, TimeUnit.MILLISECONDS); 67 | } 68 | } 69 | 70 | public void call(final Subscriber subscriber) { 71 | final SubscriptionLog subscriptionLog = new SubscriptionLog(scheduler.now()); 72 | observable.subscriptions.add(subscriptionLog); 73 | final int subscriptionIndex = observable.getSubscriptions().size() - 1; 74 | 75 | subscribers.add(subscriber); 76 | 77 | subscriber.add((Subscriptions.create(new Action0() { 78 | @Override 79 | public void call() { 80 | // on unsubscribe 81 | observable.subscriptions.set( 82 | subscriptionIndex, 83 | new SubscriptionLog(subscriptionLog.subscribe, scheduler.now()) 84 | ); 85 | subscribers.remove(subscriber); 86 | } 87 | }))); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/io/reactivex/marble/junit/MarbleRule.java: -------------------------------------------------------------------------------- 1 | package io.reactivex.marble.junit; 2 | 3 | 4 | import io.reactivex.*; 5 | import io.reactivex.marble.*; 6 | import org.junit.rules.TestRule; 7 | import org.junit.runner.Description; 8 | import org.junit.runners.model.Statement; 9 | import org.reactivestreams.ISetupSubscriptionsTest; 10 | import org.reactivestreams.ISetupTest; 11 | import org.reactivestreams.SubscriptionLog; 12 | 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | public class MarbleRule implements TestRule { 17 | 18 | private static ThreadLocal schedulerHolder = new ThreadLocal<>(); 19 | 20 | public final MarbleScheduler scheduler; 21 | 22 | public MarbleRule() { 23 | scheduler = new MarbleScheduler(); 24 | } 25 | 26 | public MarbleRule(long frameTimeFactor) { 27 | scheduler = new MarbleScheduler(frameTimeFactor); 28 | } 29 | 30 | public static HotObservable hot(String marbles, Map values) { 31 | return schedulerHolder.get().createHotObservable(marbles, values); 32 | } 33 | 34 | public static HotObservable hot(String marbles) { 35 | return schedulerHolder.get().createHotObservable(marbles); 36 | } 37 | 38 | public static ColdObservable cold(String marbles, Map values) { 39 | return schedulerHolder.get().createColdObservable(marbles, values); 40 | } 41 | 42 | public static ColdObservable cold(String marbles) { 43 | return schedulerHolder.get().createColdObservable(marbles); 44 | } 45 | 46 | public static ISetupTest expectObservable(Observable actual) { 47 | return schedulerHolder.get().expectObservable(actual); 48 | } 49 | 50 | public static ISetupTest expectObservable(Observable actual, String unsubscriptionMarbles) { 51 | return schedulerHolder.get().expectObservable(actual, unsubscriptionMarbles); 52 | } 53 | 54 | public static ISetupTest expectFlowable(Flowable actual) { 55 | return schedulerHolder.get().expectFlowable(actual); 56 | } 57 | 58 | public static ISetupTest expectFlowable(Flowable actual, String unsubscriptionMarbles) { 59 | return schedulerHolder.get().expectFlowable(actual, unsubscriptionMarbles); 60 | } 61 | 62 | public static ISetupTest expectSingle(Single actual) { 63 | return schedulerHolder.get().expectFlowable(actual.toFlowable()); 64 | } 65 | 66 | public static ISetupTest expectSingle(Single actual, String unsubscriptionMarbles) { 67 | return schedulerHolder.get().expectFlowable(actual.toFlowable(), unsubscriptionMarbles); 68 | } 69 | 70 | public static ISetupTest expectMaybe(Maybe actual) { 71 | return schedulerHolder.get().expectFlowable(actual.toFlowable()); 72 | } 73 | 74 | public static ISetupTest expectCompletable(Completable actual, String unsubscriptionMarbles) { 75 | return schedulerHolder.get().expectFlowable(actual.toFlowable(), unsubscriptionMarbles); 76 | } 77 | 78 | public static ISetupTest expectCompletable(Completable actual) { 79 | return schedulerHolder.get().expectFlowable(actual.toFlowable()); 80 | } 81 | 82 | public static ISetupTest expectMaybe(Maybe actual, String unsubscriptionMarbles) { 83 | return schedulerHolder.get().expectObservable(actual.toObservable(), unsubscriptionMarbles); 84 | } 85 | 86 | public static ISetupSubscriptionsTest expectSubscriptions(List subscriptions) { 87 | return schedulerHolder.get().expectSubscriptions(subscriptions); 88 | } 89 | 90 | @Override 91 | public Statement apply(final Statement base, Description description) { 92 | return new Statement() { 93 | @Override 94 | public void evaluate() throws Throwable { 95 | schedulerHolder.set(scheduler); 96 | try { 97 | base.evaluate(); 98 | scheduler.flush(); 99 | } finally { 100 | schedulerHolder.remove(); 101 | } 102 | 103 | } 104 | }; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/test/java/io/reactivex/marble/junit/DemoTest.java: -------------------------------------------------------------------------------- 1 | package io.reactivex.marble.junit; 2 | 3 | 4 | import io.reactivex.*; 5 | import io.reactivex.annotations.NonNull; 6 | import io.reactivex.functions.BiFunction; 7 | import io.reactivex.functions.Consumer; 8 | import io.reactivex.functions.Function; 9 | import io.reactivex.marble.ColdObservable; 10 | import org.junit.Rule; 11 | import org.junit.Test; 12 | 13 | import java.util.Map; 14 | import java.util.concurrent.TimeUnit; 15 | 16 | import static io.reactivex.marble.MapHelper.of; 17 | import static io.reactivex.marble.junit.MarbleRule.*; 18 | 19 | public class DemoTest { 20 | 21 | @Rule 22 | public MarbleRule marble = new MarbleRule(); 23 | 24 | @Test 25 | public void should_map() { 26 | // given 27 | Observable input = hot("a-b-c-d"); 28 | // when 29 | Observable output = input.map(new Function() { 30 | @Override 31 | public String apply(String s) { 32 | return s.toUpperCase(); 33 | } 34 | }); 35 | // then 36 | expectObservable(output).toBe("A-B-C-D"); 37 | } 38 | 39 | @Test 40 | public void should_sum() { 41 | // given 42 | Observable input = cold("a-b-c-d", of("a", 1, "b", 2, "c", 3, "d", 4)); 43 | // when 44 | Observable output = input.scan(new BiFunction() { 45 | @Override 46 | public Integer apply(Integer first, Integer second) { 47 | return first + second; 48 | } 49 | }); 50 | // then 51 | expectObservable(output).toBe("A-B-C-D", of("A", 1, "B", 3, "C", 6, "D", 10)); 52 | } 53 | 54 | @Test 55 | public void should_be_awesome() { 56 | Map values = of("a", 1, "b", 2); 57 | ColdObservable myObservable 58 | = cold( "---a---b--|", values); 59 | String subs = "^---------!"; 60 | expectObservable(myObservable).toBe("---a---b--|", values); 61 | expectSubscriptions(myObservable.getSubscriptions()).toBe(subs); 62 | } 63 | 64 | @Test 65 | public void should_use_unsubscription_diagram() { 66 | Observable source = hot("---^-a-b-|"); 67 | String unsubscribe = "---!"; 68 | String expected = "--a"; 69 | expectObservable(source, unsubscribe).toBe(expected); 70 | } 71 | 72 | @Test 73 | public void should_map_flowable() { 74 | // given 75 | Observable input = hot("a-b-c-d"); 76 | // when 77 | Flowable output = input.toFlowable(BackpressureStrategy.BUFFER).map(new Function() { 78 | @Override 79 | public String apply(String s) { 80 | return s.toUpperCase(); 81 | } 82 | }); 83 | // then 84 | expectFlowable(output).toBe("A-B-C-D"); 85 | } 86 | 87 | @Test 88 | public void should_use_unsubscription_diagram_with_flowable() { 89 | Flowable source = hot("---^-a-b-|").toFlowable(BackpressureStrategy.DROP); 90 | String unsubscribe = "---!"; 91 | String expected = "--a"; 92 | expectFlowable(source, unsubscribe).toBe(expected); 93 | } 94 | 95 | @Test 96 | public void should_map_single() { 97 | // given 98 | Observable input = hot("--(a|)"); 99 | // when 100 | Single output = input.single("error").map(new Function() { 101 | @Override 102 | public String apply(String s) { 103 | return s.toUpperCase(); 104 | } 105 | }); 106 | // then 107 | expectSingle(output).toBe("--(A|)"); 108 | } 109 | 110 | @Test 111 | public void should_map_maybe() { 112 | // given 113 | Observable input = hot("--(a|)"); 114 | // when 115 | Maybe output = input.singleElement().map(new Function() { 116 | @Override 117 | public String apply(String s) { 118 | return s.toUpperCase(); 119 | } 120 | }); 121 | // then 122 | expectMaybe(output).toBe("--(A|)"); 123 | } 124 | 125 | @Test 126 | public void should_delay_completable() { 127 | // given 128 | Observable input = hot("--|"); 129 | // when 130 | Completable output = input.ignoreElements().delay(10, TimeUnit.MILLISECONDS, marble.scheduler); 131 | 132 | // then 133 | expectCompletable(output).toBe("---|"); 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/io/reactivex/marble/MarbleScheduler.java: -------------------------------------------------------------------------------- 1 | package io.reactivex.marble; 2 | 3 | 4 | import io.reactivex.BackpressureStrategy; 5 | import io.reactivex.Flowable; 6 | import io.reactivex.Observable; 7 | import io.reactivex.Scheduler; 8 | import io.reactivex.annotations.NonNull; 9 | import io.reactivex.schedulers.TestScheduler; 10 | import org.reactivestreams.*; 11 | import org.reactivestreams.ExpectSubscriptionsException; 12 | 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | 18 | public class MarbleScheduler extends Scheduler { 19 | 20 | private final TestScheduler testScheduler = new TestScheduler(); 21 | 22 | private final MarbleSchedulerState state; 23 | private final long frameTimeFactor; 24 | 25 | public MarbleScheduler() { 26 | this(10); 27 | } 28 | 29 | public MarbleScheduler(long frameTimeFactor) { 30 | this.frameTimeFactor = frameTimeFactor; 31 | state = new PatchedSchedulerState(frameTimeFactor, new MarbleSchedulerState.ISchedule() { 32 | @Override 33 | public long now() { 34 | return MarbleScheduler.this.now(TimeUnit.MILLISECONDS); 35 | } 36 | 37 | @Override 38 | public void schedule(Runnable runnable, long time) { 39 | MarbleScheduler.this.createWorker().schedule(runnable, time, TimeUnit.MILLISECONDS); 40 | } 41 | }, getClass()); 42 | } 43 | 44 | @Override 45 | public long now(@NonNull TimeUnit unit) { 46 | return testScheduler.now(unit); 47 | } 48 | 49 | public void advanceTimeBy(long delayTime, TimeUnit unit) { 50 | testScheduler.advanceTimeBy(delayTime, unit); 51 | } 52 | 53 | public void advanceTimeTo(long delayTime, TimeUnit unit) { 54 | testScheduler.advanceTimeTo(delayTime, unit); 55 | } 56 | 57 | public void triggerActions() { 58 | testScheduler.triggerActions(); 59 | } 60 | 61 | @Override 62 | @NonNull 63 | public Worker createWorker() { 64 | return testScheduler.createWorker(); 65 | } 66 | 67 | public ColdObservable createColdObservable(String marbles, Map values) { 68 | List> notifications = Parser.parseMarbles(marbles, values, null, frameTimeFactor); 69 | return ColdObservable.create(this, notifications); 70 | } 71 | 72 | public ColdObservable createColdObservable(String marbles) { 73 | return createColdObservable(marbles, null); 74 | } 75 | 76 | public HotObservable createHotObservable(String marbles, Map values) { 77 | List> notifications = Parser.parseMarbles(marbles, values, null, frameTimeFactor); 78 | return HotObservable.create(this, notifications); 79 | } 80 | 81 | public HotObservable createHotObservable(String marbles) { 82 | return createHotObservable(marbles, null); 83 | } 84 | 85 | 86 | public long createTime(String marbles) { 87 | int endIndex = marbles.indexOf("|"); 88 | if (endIndex == -1) { 89 | throw new RuntimeException("Marble diagram for time should have a completion marker '|'"); 90 | } 91 | 92 | return endIndex * frameTimeFactor; 93 | } 94 | 95 | public void flush() { 96 | testScheduler.advanceTimeTo(Long.MAX_VALUE, TimeUnit.MILLISECONDS); 97 | try { 98 | state.flush(); 99 | } catch (ExpectPublisherException ex) { 100 | throw new ExpectFlowableException(ex.getMessage()); 101 | } catch (ExpectSubscriptionsException ex) { 102 | throw new io.reactivex.marble.ExpectSubscriptionsException(ex.getMessage()); 103 | } 104 | } 105 | 106 | public ISetupTest expectObservable(Observable observable) { 107 | return expectObservable(observable, null); 108 | } 109 | 110 | public ISetupTest expectObservable(Observable observable, String unsubscriptionMarbles) { 111 | return state.expectPublisher(observable.toFlowable(BackpressureStrategy.BUFFER), unsubscriptionMarbles); 112 | } 113 | 114 | public ISetupTest expectFlowable(Flowable flowable) { 115 | return expectFlowable(flowable, null); 116 | } 117 | 118 | public ISetupTest expectFlowable(Flowable flowable, String unsubscriptionMarbles) { 119 | return state.expectPublisher(flowable, unsubscriptionMarbles); 120 | } 121 | 122 | public ISetupSubscriptionsTest expectSubscriptions(List subscriptions) { 123 | return state.expectSubscriptions(subscriptions); 124 | } 125 | 126 | public static class PatchedSchedulerState extends MarbleSchedulerState { 127 | 128 | public PatchedSchedulerState(long frameTimeFactor, ISchedule scheduler, Class schedulerClass) { 129 | super(frameTimeFactor, scheduler, schedulerClass); 130 | } 131 | 132 | @Override 133 | protected Object materializeInnerStreamWhenNeeded(Object value) { 134 | if (value instanceof Observable) { 135 | Flowable flowable = ((Observable) value).toFlowable(BackpressureStrategy.BUFFER); 136 | return materializeInnerPublisher(flowable, scheduler); 137 | } 138 | return super.materializeInnerStreamWhenNeeded(value); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/rx/marble/Parser.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | import rx.Notification; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | /** 10 | * Created by Alexandre Victoor on 05/06/2016. 11 | */ 12 | public class Parser { 13 | 14 | 15 | public static List> parseMarbles(String marbles, 16 | Map values, 17 | Exception errorValue, 18 | long frameTimeFactor, 19 | boolean materializeInnerObservables) { 20 | 21 | if (marbles.indexOf('!') != -1) { 22 | throw new IllegalArgumentException("Conventional marble diagrams cannot have the unsubscription marker '!'"); 23 | } 24 | 25 | int len = marbles.length(); 26 | List> testMessages = new ArrayList<>(); 27 | int subIndex = marbles.indexOf('^'); 28 | long frameOffset = subIndex == -1 ? 0 : (subIndex * -frameTimeFactor); 29 | 30 | long groupStart = -1; 31 | 32 | for (int i = 0; i < len; i++) { 33 | long frame = i * frameTimeFactor + frameOffset; 34 | Notification notification = null; 35 | char c = marbles.charAt(i); 36 | switch (c) { 37 | case '-': 38 | case ' ': 39 | break; 40 | case '(': 41 | groupStart = frame; 42 | break; 43 | case ')': 44 | groupStart = -1; 45 | break; 46 | case '|': 47 | notification = Notification.createOnCompleted(); 48 | break; 49 | case '^': 50 | break; 51 | case '#': 52 | notification = Notification.createOnError(errorValue); 53 | break; 54 | default: 55 | T value; 56 | if (values == null) { 57 | value = (T)String.valueOf(c); 58 | } else { 59 | value = values.get(String.valueOf(c)); 60 | if (materializeInnerObservables && value instanceof ColdObservable) { 61 | value = (T)((ColdObservable)value).getMessages(); 62 | } 63 | } 64 | notification = Notification.createOnNext(value); 65 | break; 66 | } 67 | 68 | if (notification != null) { 69 | long messageFrame = groupStart > -1 ? groupStart : frame; 70 | testMessages.add(new Recorded<>(messageFrame, notification)); 71 | } 72 | } 73 | return testMessages; 74 | } 75 | 76 | public static List> parseMarbles(String marbles, Map values, long frameTimeFactor) { 77 | return parseMarbles(marbles, values, null, frameTimeFactor); 78 | } 79 | 80 | public static List> parseMarbles(String marbles, Map values, Exception errorValue, long frameTimeFactor) { 81 | return parseMarbles(marbles, values, errorValue, frameTimeFactor, false); 82 | } 83 | 84 | public static List> parseMarbles(String marbles, long frameTimeFactor) { 85 | return parseMarbles(marbles, null, frameTimeFactor); 86 | } 87 | 88 | public static SubscriptionLog parseMarblesAsSubscriptions(String marbles, long frameTimeFactor) { 89 | int len = marbles.length(); 90 | long groupStart = -1; 91 | long subscriptionFrame = Long.MAX_VALUE; 92 | long unsubscriptionFrame = Long.MAX_VALUE; 93 | 94 | for (int i = 0; i < len; i++) { 95 | long frame = i * frameTimeFactor; 96 | char c = marbles.charAt(i); 97 | switch (c) { 98 | case '-': 99 | case ' ': 100 | break; 101 | case '(': 102 | groupStart = frame; 103 | break; 104 | case ')': 105 | groupStart = -1; 106 | break; 107 | case '^': 108 | if (subscriptionFrame != Long.MAX_VALUE) { 109 | throw new IllegalArgumentException("Found a second subscription point \'^\' in a " + 110 | "subscription marble diagram. There can only be one."); 111 | } 112 | subscriptionFrame = groupStart > -1 ? groupStart : frame; 113 | break; 114 | case '!': 115 | if (unsubscriptionFrame != Long.MAX_VALUE) { 116 | throw new IllegalArgumentException("Found a second subscription point \'^\' in a " + 117 | "subscription marble diagram. There can only be one."); 118 | } 119 | unsubscriptionFrame = groupStart > -1 ? groupStart : frame; 120 | break; 121 | default: 122 | throw new IllegalArgumentException("There can only be \'^\' and \'!\' markers in a " + 123 | "subscription marble diagram. Found instead \'' + c + '\'."); 124 | } 125 | } 126 | 127 | if (unsubscriptionFrame < 0) { 128 | return new SubscriptionLog(subscriptionFrame); 129 | } 130 | return new SubscriptionLog(subscriptionFrame, unsubscriptionFrame); 131 | 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/org/reactivestreams/Parser.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | /** 8 | * Created by Alexandre Victoor on 05/06/2016. 9 | */ 10 | public class Parser { 11 | 12 | 13 | public static List> parseMarbles(String marbles, 14 | Map values, 15 | Exception errorValue, 16 | long frameTimeFactor, 17 | boolean materializeInnerObservables) { 18 | 19 | if (marbles.indexOf('!') != -1) { 20 | throw new IllegalArgumentException("Conventional marble diagrams cannot have the unsubscription marker '!'"); 21 | } 22 | 23 | int len = marbles.length(); 24 | List> testMessages = new ArrayList<>(); 25 | int subIndex = marbles.indexOf('^'); 26 | long frameOffset = subIndex == -1 ? 0 : (subIndex * -frameTimeFactor); 27 | 28 | long groupStart = -1; 29 | 30 | for (int i = 0; i < len; i++) { 31 | long frame = i * frameTimeFactor + frameOffset; 32 | Notification notification = null; 33 | char c = marbles.charAt(i); 34 | switch (c) { 35 | case '-': 36 | case ' ': 37 | break; 38 | case '(': 39 | groupStart = frame; 40 | break; 41 | case ')': 42 | groupStart = -1; 43 | break; 44 | case '|': 45 | notification = Notification.createOnComplete(); 46 | break; 47 | case '^': 48 | break; 49 | case '#': 50 | notification = Notification.createOnError(errorValue); 51 | break; 52 | default: 53 | T value; 54 | if (values == null) { 55 | value = (T)String.valueOf(c); 56 | } else { 57 | value = values.get(String.valueOf(c)); 58 | if (materializeInnerObservables && value instanceof TestablePublisher) { 59 | value = (T)((TestablePublisher)value).getMessages(); 60 | } 61 | } 62 | notification = Notification.createOnNext(value); 63 | break; 64 | } 65 | 66 | if (notification != null) { 67 | long messageFrame = groupStart > -1 ? groupStart : frame; 68 | testMessages.add(new Recorded<>(messageFrame, notification)); 69 | } 70 | } 71 | return testMessages; 72 | } 73 | 74 | public static List> parseMarbles(String marbles, Map values, long frameTimeFactor) { 75 | return parseMarbles(marbles, values, null, frameTimeFactor); 76 | } 77 | 78 | public static List> parseMarbles(String marbles, Map values, Exception errorValue, long frameTimeFactor) { 79 | return parseMarbles(marbles, values, errorValue, frameTimeFactor, false); 80 | } 81 | 82 | public static List> parseMarbles(String marbles, long frameTimeFactor) { 83 | return parseMarbles(marbles, null, frameTimeFactor); 84 | } 85 | 86 | public static SubscriptionLog parseMarblesAsSubscriptions(String marbles, long frameTimeFactor) { 87 | int len = marbles.length(); 88 | long groupStart = -1; 89 | long subscriptionFrame = Long.MAX_VALUE; 90 | long unsubscriptionFrame = Long.MAX_VALUE; 91 | 92 | for (int i = 0; i < len; i++) { 93 | long frame = i * frameTimeFactor; 94 | char c = marbles.charAt(i); 95 | switch (c) { 96 | case '-': 97 | case ' ': 98 | break; 99 | case '(': 100 | groupStart = frame; 101 | break; 102 | case ')': 103 | groupStart = -1; 104 | break; 105 | case '^': 106 | if (subscriptionFrame != Long.MAX_VALUE) { 107 | throw new IllegalArgumentException("Found a second subscription point \'^\' in a " + 108 | "subscription marble diagram. There can only be one."); 109 | } 110 | subscriptionFrame = groupStart > -1 ? groupStart : frame; 111 | break; 112 | case '!': 113 | if (unsubscriptionFrame != Long.MAX_VALUE) { 114 | throw new IllegalArgumentException("Found a second subscription point \'^\' in a " + 115 | "subscription marble diagram. There can only be one."); 116 | } 117 | unsubscriptionFrame = groupStart > -1 ? groupStart : frame; 118 | break; 119 | default: 120 | throw new IllegalArgumentException("There can only be \'^\' and \'!\' markers in a " + 121 | "subscription marble diagram. Found instead \'' + c + '\'."); 122 | } 123 | } 124 | 125 | if (unsubscriptionFrame < 0) { 126 | return new SubscriptionLog(subscriptionFrame); 127 | } 128 | return new SubscriptionLog(subscriptionFrame, unsubscriptionFrame); 129 | 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/test/java/reactor/HotFluxTest.java: -------------------------------------------------------------------------------- 1 | package reactor; 2 | 3 | import io.reactivex.subscribers.TestSubscriber; 4 | import org.junit.Test; 5 | import org.reactivestreams.Notification; 6 | import org.reactivestreams.Recorded; 7 | import org.reactivestreams.SubscriptionLog; 8 | import reactor.core.publisher.Flux; 9 | import reactor.test.scheduler.VirtualTimeScheduler; 10 | 11 | import java.time.Duration; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | 17 | public class HotFluxTest { 18 | 19 | @Test 20 | public void should_send_notification_occurring_after_subscribe() { 21 | // given 22 | VirtualTimeScheduler scheduler = VirtualTimeScheduler.create(); 23 | Recorded event = new Recorded<>(10, Notification.createOnNext("Hello world!")); 24 | Flux hotFlux = HotFlux.create(scheduler, event); 25 | // when 26 | TestSubscriber subscriber = new TestSubscriber<>(); 27 | hotFlux.subscribe(subscriber); 28 | // then 29 | scheduler.advanceTimeBy(Duration.ofMillis(10)); 30 | subscriber.assertValue("Hello world!"); 31 | } 32 | 33 | @Test 34 | public void should_not_send_notification_occurring_before_subscribe() { 35 | // given 36 | VirtualTimeScheduler scheduler = VirtualTimeScheduler.create(); 37 | Recorded event = new Recorded<>(10, Notification.createOnNext("Hello world!")); 38 | final Flux hotFlux = HotFlux.create(scheduler, event); 39 | // when 40 | final TestSubscriber subscriber = new TestSubscriber<>(); 41 | scheduler.schedule(new Runnable() { 42 | @Override 43 | public void run() { 44 | hotFlux.subscribe(subscriber); 45 | } 46 | }, 15, TimeUnit.MILLISECONDS); 47 | // then 48 | scheduler.advanceTimeBy(Duration.ofNanos(Long.MAX_VALUE)); 49 | subscriber.assertNoValues(); 50 | } 51 | 52 | @Test 53 | public void should_not_send_notification_occurring_after_unsubscribe() { 54 | // given 55 | VirtualTimeScheduler scheduler = VirtualTimeScheduler.create(); 56 | Recorded event = new Recorded<>(10, Notification.createOnNext("Hello world!")); 57 | final Flux hotFlux = HotFlux.create(scheduler, event); 58 | // when 59 | final TestSubscriber subscriber = new TestSubscriber<>(); 60 | hotFlux.subscribe(subscriber); 61 | 62 | scheduler.schedule(new Runnable() { 63 | @Override 64 | public void run() { 65 | subscriber.dispose(); 66 | } 67 | }, 5, TimeUnit.MILLISECONDS); 68 | // then 69 | scheduler.advanceTimeBy(Duration.ofNanos(Long.MAX_VALUE)); 70 | subscriber.assertNoValues(); 71 | } 72 | 73 | @Test 74 | public void should_keep_track_of_subscriptions() { 75 | // given 76 | VirtualTimeScheduler scheduler = VirtualTimeScheduler.create(); 77 | final HotFlux hotFlux = HotFlux.create(scheduler); 78 | // when 79 | final TestSubscriber subscriber = new TestSubscriber<>(); 80 | 81 | scheduler.schedule(new Runnable() { 82 | @Override 83 | public void run() { 84 | hotFlux.subscribe(subscriber); 85 | } 86 | }, 42, TimeUnit.MILLISECONDS); 87 | // then 88 | scheduler.advanceTimeBy(Duration.ofMillis(42)); 89 | assertThat(hotFlux.getSubscriptions()) 90 | .containsExactly( 91 | new SubscriptionLog(42, Long.MAX_VALUE) 92 | ); 93 | } 94 | 95 | @Test 96 | public void should_keep_track_of_unsubscriptions() { 97 | // given 98 | VirtualTimeScheduler scheduler = VirtualTimeScheduler.create(); 99 | final HotFlux hotFlux = HotFlux.create(scheduler); 100 | // when 101 | final TestSubscriber subscriber = new TestSubscriber<>(); 102 | hotFlux.subscribe(subscriber); 103 | scheduler.schedule(new Runnable() { 104 | @Override 105 | public void run() { 106 | subscriber.dispose(); 107 | } 108 | }, 42, TimeUnit.MILLISECONDS); 109 | // then 110 | scheduler.advanceTimeBy(Duration.ofMillis(42)); 111 | assertThat(hotFlux.getSubscriptions()) 112 | .containsExactly( 113 | new SubscriptionLog(0, 42) 114 | ); 115 | } 116 | 117 | @Test 118 | public void should_keep_track_of_several_subscriptions() { 119 | // given 120 | VirtualTimeScheduler scheduler = VirtualTimeScheduler.create(); 121 | final HotFlux hotFlux = HotFlux.create(scheduler); 122 | // when 123 | final TestSubscriber subscriber1 = new TestSubscriber<>(); 124 | final TestSubscriber subscriber2 = new TestSubscriber<>(); 125 | hotFlux.subscribe(subscriber1); 126 | scheduler.schedule(new Runnable() { 127 | @Override 128 | public void run() { 129 | hotFlux.subscribe(subscriber2); 130 | } 131 | }, 36, TimeUnit.MILLISECONDS); 132 | scheduler.schedule(new Runnable() { 133 | @Override 134 | public void run() { 135 | subscriber1.dispose(); 136 | } 137 | }, 42, TimeUnit.MILLISECONDS); 138 | // then 139 | scheduler.advanceTimeBy(Duration.ofMillis(42)); 140 | assertThat(hotFlux.getSubscriptions()) 141 | .containsExactly( 142 | new SubscriptionLog(0, 42), 143 | new SubscriptionLog(36, Long.MAX_VALUE) 144 | ); 145 | } 146 | 147 | } -------------------------------------------------------------------------------- /src/test/java/io/reactivex/marble/HotObservableTest.java: -------------------------------------------------------------------------------- 1 | package io.reactivex.marble; 2 | 3 | import io.reactivex.observers.TestObserver; 4 | import io.reactivex.schedulers.TestScheduler; 5 | import org.junit.Test; 6 | import org.reactivestreams.Notification; 7 | import org.reactivestreams.Recorded; 8 | import org.reactivestreams.SubscriptionLog; 9 | 10 | import java.util.concurrent.TimeUnit; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | 15 | public class HotObservableTest { 16 | 17 | @Test 18 | public void should_send_notification_occurring_after_subscribe() { 19 | // given 20 | TestScheduler scheduler = new TestScheduler(); 21 | Recorded event = new Recorded<>(10, Notification.createOnNext("Hello world!")); 22 | HotObservable hotObservable = HotObservable.create(scheduler, event); 23 | // when 24 | TestObserver observer = new TestObserver<>(); 25 | hotObservable.subscribe(observer); 26 | // then 27 | scheduler.advanceTimeBy(10, TimeUnit.SECONDS); 28 | observer.assertValue("Hello world!"); 29 | } 30 | 31 | @Test 32 | public void should_not_send_notification_occurring_before_subscribe() { 33 | // given 34 | TestScheduler scheduler = new TestScheduler(); 35 | Recorded event = new Recorded<>(10, Notification.createOnNext("Hello world!")); 36 | final HotObservable hotObservable = HotObservable.create(scheduler, event); 37 | // when 38 | final TestObserver observer = new TestObserver<>(); 39 | scheduler.createWorker().schedule(new Runnable() { 40 | @Override 41 | public void run() { 42 | hotObservable.subscribe(observer); 43 | } 44 | }, 15, TimeUnit.MILLISECONDS); 45 | // then 46 | scheduler.advanceTimeBy(Long.MAX_VALUE, TimeUnit.MILLISECONDS); 47 | observer.assertNoValues(); 48 | } 49 | 50 | @Test 51 | public void should_not_send_notification_occurring_after_unsubscribe() { 52 | // given 53 | TestScheduler scheduler = new TestScheduler(); 54 | Recorded event = new Recorded<>(10, Notification.createOnNext("Hello world!")); 55 | final HotObservable hotObservable = HotObservable.create(scheduler, event); 56 | // when 57 | final TestObserver observer = new TestObserver<>(); 58 | hotObservable.subscribe(observer); 59 | 60 | scheduler.createWorker().schedule(new Runnable() { 61 | @Override 62 | public void run() { 63 | observer.dispose(); 64 | } 65 | }, 5, TimeUnit.MILLISECONDS); 66 | // then 67 | scheduler.advanceTimeBy(Long.MAX_VALUE, TimeUnit.MILLISECONDS); 68 | observer.assertNoValues(); 69 | } 70 | 71 | @Test 72 | public void should_keep_track_of_subscriptions() { 73 | // given 74 | TestScheduler scheduler = new TestScheduler(); 75 | final HotObservable hotObservable = HotObservable.create(scheduler); 76 | // when 77 | final TestObserver observer = new TestObserver<>(); 78 | 79 | scheduler.createWorker().schedule(new Runnable() { 80 | @Override 81 | public void run() { 82 | hotObservable.subscribe(observer); 83 | } 84 | }, 42, TimeUnit.MILLISECONDS); 85 | // then 86 | scheduler.advanceTimeBy(42, TimeUnit.MILLISECONDS); 87 | assertThat(hotObservable.getSubscriptions()) 88 | .containsExactly( 89 | new SubscriptionLog(42, Long.MAX_VALUE) 90 | ); 91 | } 92 | 93 | @Test 94 | public void should_keep_track_of_unsubscriptions() { 95 | // given 96 | TestScheduler scheduler = new TestScheduler(); 97 | final HotObservable hotObservable = HotObservable.create(scheduler); 98 | // when 99 | final TestObserver observer = new TestObserver<>(); 100 | hotObservable.subscribe(observer); 101 | scheduler.createWorker().schedule(new Runnable() { 102 | @Override 103 | public void run() { 104 | observer.dispose(); 105 | } 106 | }, 42, TimeUnit.MILLISECONDS); 107 | // then 108 | scheduler.advanceTimeBy(42, TimeUnit.MILLISECONDS); 109 | assertThat(hotObservable.getSubscriptions()) 110 | .containsExactly( 111 | new SubscriptionLog(0, 42) 112 | ); 113 | } 114 | 115 | @Test 116 | public void should_keep_track_of_several_subscriptions() { 117 | // given 118 | TestScheduler scheduler = new TestScheduler(); 119 | final HotObservable hotObservable = HotObservable.create(scheduler); 120 | // when 121 | final TestObserver observer1 = new TestObserver<>(); 122 | final TestObserver observer2 = new TestObserver<>(); 123 | hotObservable.subscribe(observer1); 124 | scheduler.createWorker().schedule(new Runnable() { 125 | @Override 126 | public void run() { 127 | hotObservable.subscribe(observer2); 128 | } 129 | }, 36, TimeUnit.MILLISECONDS); 130 | scheduler.createWorker().schedule(new Runnable() { 131 | @Override 132 | public void run() { 133 | observer1.dispose(); 134 | } 135 | }, 42, TimeUnit.MILLISECONDS); 136 | // then 137 | scheduler.advanceTimeBy(42, TimeUnit.MILLISECONDS); 138 | assertThat(hotObservable.getSubscriptions()) 139 | .containsExactly( 140 | new SubscriptionLog(0, 42), 141 | new SubscriptionLog(36, Long.MAX_VALUE) 142 | ); 143 | } 144 | 145 | } -------------------------------------------------------------------------------- /src/main/java/rx/marble/RecordedStreamComparator.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.Comparator; 6 | import java.util.List; 7 | 8 | /** 9 | * Created by Alexandre Victoor on 25/10/2016. 10 | */ 11 | public class RecordedStreamComparator { 12 | 13 | public StreamComparison compare( 14 | List> actualRecords, 15 | List> expectedRecords) { 16 | 17 | List unitComparisons = new ArrayList<>(); 18 | 19 | List> onlyOnExpected = new ArrayList<>(expectedRecords); 20 | onlyOnExpected.removeAll(actualRecords); 21 | for (Recorded record : onlyOnExpected) { 22 | unitComparisons.add(new EventComparison(record, EventComparisonResult.ONLY_ON_EXPECTED)); 23 | } 24 | 25 | List> onlyOnActual = new ArrayList<>(actualRecords); 26 | onlyOnActual.removeAll(expectedRecords); 27 | for (Recorded record : onlyOnActual) { 28 | unitComparisons.add(new EventComparison(record, EventComparisonResult.ONLY_ON_ACTUAL)); 29 | } 30 | 31 | boolean equalStreams = unitComparisons.isEmpty(); 32 | 33 | for (Recorded record : actualRecords){ 34 | if (!onlyOnActual.contains(record) && !onlyOnExpected.contains(record)) { 35 | unitComparisons.add(new EventComparison(record, EventComparisonResult.EQUALS)); 36 | } 37 | } 38 | Collections.sort(unitComparisons, new Comparator() { 39 | @Override 40 | public int compare(EventComparison first, EventComparison second) { 41 | 42 | int diff = new Long(first.record.time - second.record.time).intValue(); 43 | if (diff == 0) { 44 | // 45 | // if events are simultaneous 46 | // on complete and on error should be last 47 | // 48 | if (first.record.value.isOnCompleted() || first.record.value.isOnError()) { 49 | return Integer.MAX_VALUE; 50 | } 51 | if (second.record.value.isOnCompleted() || second.record.value.isOnError()) { 52 | return Integer.MIN_VALUE; 53 | } 54 | } 55 | return diff; 56 | } 57 | }); 58 | 59 | return new StreamComparison(equalStreams, unitComparisons); 60 | } 61 | 62 | 63 | public static class StreamComparison { 64 | public final boolean streamEquals; 65 | public final List unitComparisons; 66 | 67 | public StreamComparison(boolean equals, List comparisons) { 68 | this.streamEquals = equals; 69 | this.unitComparisons = comparisons; 70 | } 71 | 72 | public String toString() { 73 | StringBuilder builder = new StringBuilder(); 74 | if (streamEquals) { 75 | builder.append("Streams are equal"); 76 | } else { 77 | builder.append("Streams are not equal, details below:"); 78 | EventComparisonResult currentMode = null; 79 | for (EventComparison comparison: unitComparisons) { 80 | if (currentMode == comparison.result) { 81 | builder.append("\n"); 82 | } else { 83 | currentMode = comparison.result; 84 | switch (currentMode) { 85 | case EQUALS: 86 | builder.append("\n= On actual & expected streams\n"); 87 | break; 88 | case ONLY_ON_ACTUAL: 89 | builder.append("\n+ On actual stream\n"); 90 | break; 91 | case ONLY_ON_EXPECTED: 92 | builder.append("\n- On expected stream\n"); 93 | break; 94 | } 95 | } 96 | builder.append(comparison); 97 | } 98 | } 99 | return builder.toString(); 100 | } 101 | } 102 | 103 | public enum EventComparisonResult { EQUALS, ONLY_ON_ACTUAL, ONLY_ON_EXPECTED } 104 | 105 | public static class EventComparison { 106 | public final Recorded record; 107 | public final EventComparisonResult result; 108 | 109 | public EventComparison(Recorded record, EventComparisonResult result) { 110 | this.record = record; 111 | this.result = result; 112 | } 113 | 114 | @Override 115 | public String toString() { 116 | switch (result) { 117 | case EQUALS: 118 | return "= " + record.toString().replace("\n", "\n= "); 119 | case ONLY_ON_ACTUAL: 120 | return "+ " + record.toString().replace("\n", "\n+ "); 121 | case ONLY_ON_EXPECTED: 122 | return "- " + record.toString().replace("\n", "\n- "); 123 | default: throw new RuntimeException("should not happen"); 124 | } 125 | } 126 | 127 | @Override 128 | public boolean equals(Object o) { 129 | if (this == o) return true; 130 | if (o == null || getClass() != o.getClass()) return false; 131 | 132 | EventComparison that = (EventComparison) o; 133 | 134 | if (!record.equals(that.record)) return false; 135 | return result == that.result; 136 | 137 | } 138 | 139 | @Override 140 | public int hashCode() { 141 | int result1 = record.hashCode(); 142 | result1 = 31 * result1 + result.hashCode(); 143 | return result1; 144 | } 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/org/reactivestreams/RecordedStreamComparator.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.Comparator; 6 | import java.util.List; 7 | 8 | /** 9 | * Created by Alexandre Victoor on 25/10/2016. 10 | */ 11 | public class RecordedStreamComparator { 12 | 13 | public StreamComparison compare( 14 | List> actualRecords, 15 | List> expectedRecords) { 16 | 17 | List unitComparisons = new ArrayList<>(); 18 | 19 | List> onlyOnExpected = new ArrayList<>(expectedRecords); 20 | onlyOnExpected.removeAll(actualRecords); 21 | for (Recorded record : onlyOnExpected) { 22 | unitComparisons.add(new EventComparison(record, EventComparisonResult.ONLY_ON_EXPECTED)); 23 | } 24 | 25 | List> onlyOnActual = new ArrayList<>(actualRecords); 26 | onlyOnActual.removeAll(expectedRecords); 27 | for (Recorded record : onlyOnActual) { 28 | unitComparisons.add(new EventComparison(record, EventComparisonResult.ONLY_ON_ACTUAL)); 29 | } 30 | 31 | boolean equalStreams = unitComparisons.isEmpty(); 32 | 33 | for (Recorded record : actualRecords){ 34 | if (!onlyOnActual.contains(record) && !onlyOnExpected.contains(record)) { 35 | unitComparisons.add(new EventComparison(record, EventComparisonResult.EQUALS)); 36 | } 37 | } 38 | Collections.sort(unitComparisons, new Comparator() { 39 | @Override 40 | public int compare(EventComparison first, EventComparison second) { 41 | 42 | int diff = new Long(first.record.time - second.record.time).intValue(); 43 | if (diff == 0) { 44 | // 45 | // if events are simultaneous 46 | // on complete and on error should be last 47 | // 48 | if (first.record.value.isOnComplete() || first.record.value.isOnError()) { 49 | return Integer.MAX_VALUE; 50 | } 51 | if (second.record.value.isOnComplete() || second.record.value.isOnError()) { 52 | return Integer.MIN_VALUE; 53 | } 54 | } 55 | return diff; 56 | } 57 | }); 58 | 59 | return new StreamComparison(equalStreams, unitComparisons); 60 | } 61 | 62 | 63 | public static class StreamComparison { 64 | public final boolean streamEquals; 65 | public final List unitComparisons; 66 | 67 | public StreamComparison(boolean equals, List comparisons) { 68 | this.streamEquals = equals; 69 | this.unitComparisons = comparisons; 70 | } 71 | 72 | public String toString() { 73 | StringBuilder builder = new StringBuilder(); 74 | if (streamEquals) { 75 | builder.append("Streams are equal"); 76 | } else { 77 | builder.append("Streams are not equal, details below:"); 78 | EventComparisonResult currentMode = null; 79 | for (EventComparison comparison: unitComparisons) { 80 | if (currentMode == comparison.result) { 81 | builder.append("\n"); 82 | } else { 83 | currentMode = comparison.result; 84 | switch (currentMode) { 85 | case EQUALS: 86 | builder.append("\n= On actual & expected streams\n"); 87 | break; 88 | case ONLY_ON_ACTUAL: 89 | builder.append("\n+ On actual stream\n"); 90 | break; 91 | case ONLY_ON_EXPECTED: 92 | builder.append("\n- On expected stream\n"); 93 | break; 94 | } 95 | } 96 | builder.append(comparison); 97 | } 98 | } 99 | return builder.toString(); 100 | } 101 | } 102 | 103 | public enum EventComparisonResult { EQUALS, ONLY_ON_ACTUAL, ONLY_ON_EXPECTED } 104 | 105 | public static class EventComparison { 106 | public final Recorded record; 107 | public final EventComparisonResult result; 108 | 109 | public EventComparison(Recorded record, EventComparisonResult result) { 110 | this.record = record; 111 | this.result = result; 112 | } 113 | 114 | @Override 115 | public String toString() { 116 | switch (result) { 117 | case EQUALS: 118 | return "= " + record.toString().replace("\n", "\n= "); 119 | case ONLY_ON_ACTUAL: 120 | return "+ " + record.toString().replace("\n", "\n+ "); 121 | case ONLY_ON_EXPECTED: 122 | return "- " + record.toString().replace("\n", "\n- "); 123 | default: throw new RuntimeException("should not happen"); 124 | } 125 | } 126 | 127 | @Override 128 | public boolean equals(Object o) { 129 | if (this == o) return true; 130 | if (o == null || getClass() != o.getClass()) return false; 131 | 132 | EventComparison that = (EventComparison) o; 133 | 134 | if (!record.equals(that.record)) return false; 135 | return result == that.result; 136 | 137 | } 138 | 139 | @Override 140 | public int hashCode() { 141 | int result1 = record.hashCode(); 142 | result1 = 31 * result1 + result.hashCode(); 143 | return result1; 144 | } 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/test/java/rx/marble/HotObservableTest.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | import org.junit.Test; 4 | import rx.Notification; 5 | import rx.Subscription; 6 | import rx.functions.Action0; 7 | import rx.observers.TestSubscriber; 8 | import rx.schedulers.TestScheduler; 9 | 10 | import java.util.concurrent.TimeUnit; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | 15 | public class HotObservableTest { 16 | 17 | @Test 18 | public void should_send_notification_occurring_after_subscribe() { 19 | // given 20 | TestScheduler scheduler = new TestScheduler(); 21 | Recorded event = new Recorded<>(10, Notification.createOnNext("Hello world!")); 22 | HotObservable hotObservable = HotObservable.create(scheduler, event); 23 | // when 24 | TestSubscriber subscriber = new TestSubscriber<>(); 25 | hotObservable.subscribe(subscriber); 26 | // then 27 | scheduler.advanceTimeBy(10, TimeUnit.SECONDS); 28 | assertThat(subscriber.getOnNextEvents()).containsExactly("Hello world!"); 29 | } 30 | 31 | @Test 32 | public void should_not_send_notification_occurring_before_subscribe() { 33 | // given 34 | TestScheduler scheduler = new TestScheduler(); 35 | Recorded event = new Recorded<>(10, Notification.createOnNext("Hello world!")); 36 | final HotObservable hotObservable = HotObservable.create(scheduler, event); 37 | // when 38 | final TestSubscriber subscriber = new TestSubscriber<>(); 39 | scheduler.createWorker().schedule(new Action0() { 40 | @Override 41 | public void call() { 42 | hotObservable.subscribe(subscriber); 43 | } 44 | }, 15, TimeUnit.MILLISECONDS); 45 | // then 46 | scheduler.advanceTimeBy(Long.MAX_VALUE, TimeUnit.MILLISECONDS); 47 | assertThat(subscriber.getOnNextEvents()).isEmpty(); 48 | } 49 | 50 | @Test 51 | public void should_not_send_notification_occurring_after_unsubscribe() { 52 | // given 53 | TestScheduler scheduler = new TestScheduler(); 54 | Recorded event = new Recorded<>(10, Notification.createOnNext("Hello world!")); 55 | final HotObservable hotObservable = HotObservable.create(scheduler, event); 56 | // when 57 | final TestSubscriber subscriber = new TestSubscriber<>(); 58 | final Subscription subscription = hotObservable.subscribe(subscriber); 59 | scheduler.createWorker().schedule(new Action0() { 60 | @Override 61 | public void call() { 62 | subscription.unsubscribe(); 63 | } 64 | }, 5, TimeUnit.MILLISECONDS); 65 | // then 66 | scheduler.advanceTimeBy(Long.MAX_VALUE, TimeUnit.MILLISECONDS); 67 | assertThat(subscriber.getOnNextEvents()).isEmpty(); 68 | } 69 | 70 | @Test 71 | public void should_keep_track_of_subscriptions() { 72 | // given 73 | TestScheduler scheduler = new TestScheduler(); 74 | final HotObservable hotObservable = HotObservable.create(scheduler); 75 | // when 76 | final TestSubscriber subscriber = new TestSubscriber<>(); 77 | 78 | scheduler.createWorker().schedule(new Action0() { 79 | @Override 80 | public void call() { 81 | hotObservable.subscribe(subscriber); 82 | } 83 | }, 42, TimeUnit.MILLISECONDS); 84 | // then 85 | scheduler.advanceTimeBy(42, TimeUnit.MILLISECONDS); 86 | assertThat(hotObservable.subscriptions) 87 | .containsExactly( 88 | new SubscriptionLog(42, Long.MAX_VALUE) 89 | ); 90 | } 91 | 92 | @Test 93 | public void should_keep_track_of_unsubscriptions() { 94 | // given 95 | TestScheduler scheduler = new TestScheduler(); 96 | final HotObservable hotObservable = HotObservable.create(scheduler); 97 | // when 98 | final TestSubscriber subscriber = new TestSubscriber<>(); 99 | final Subscription subscription = hotObservable.subscribe(subscriber); 100 | scheduler.createWorker().schedule(new Action0() { 101 | @Override 102 | public void call() { 103 | subscription.unsubscribe(); 104 | } 105 | }, 42, TimeUnit.MILLISECONDS); 106 | // then 107 | scheduler.advanceTimeBy(42, TimeUnit.MILLISECONDS); 108 | assertThat(hotObservable.subscriptions) 109 | .containsExactly( 110 | new SubscriptionLog(0, 42) 111 | ); 112 | } 113 | 114 | @Test 115 | public void should_keep_track_of_several_subscriptions() { 116 | // given 117 | TestScheduler scheduler = new TestScheduler(); 118 | final HotObservable hotObservable = HotObservable.create(scheduler); 119 | // when 120 | final TestSubscriber subscriber1 = new TestSubscriber<>(); 121 | final TestSubscriber subscriber2 = new TestSubscriber<>(); 122 | final Subscription subscription = hotObservable.subscribe(subscriber1); 123 | scheduler.createWorker().schedule(new Action0() { 124 | @Override 125 | public void call() { 126 | hotObservable.subscribe(subscriber2); 127 | } 128 | }, 36, TimeUnit.MILLISECONDS); 129 | scheduler.createWorker().schedule(new Action0() { 130 | @Override 131 | public void call() { 132 | subscription.unsubscribe(); 133 | } 134 | }, 42, TimeUnit.MILLISECONDS); 135 | // then 136 | scheduler.advanceTimeBy(42, TimeUnit.MILLISECONDS); 137 | assertThat(hotObservable.subscriptions) 138 | .containsExactly( 139 | new SubscriptionLog(0, 42), 140 | new SubscriptionLog(36, Long.MAX_VALUE) 141 | ); 142 | } 143 | 144 | } -------------------------------------------------------------------------------- /src/test/java/rx/marble/RecordedStreamComparatorTest.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | import org.junit.Test; 4 | import rx.Notification; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | import static java.util.Arrays.asList; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static rx.Notification.createOnError; 12 | import static rx.Notification.createOnNext; 13 | import static rx.marble.RecordedStreamComparator.EventComparisonResult.*; 14 | 15 | /** 16 | * Created by Alexandre Victoor on 26/10/2016. 17 | */ 18 | public class RecordedStreamComparatorTest { 19 | 20 | 21 | @Test 22 | public void should_detect_missing_event_in_actual_records() { 23 | // given 24 | Recorded onCompletedEvent = new Recorded<>(10, Notification.createOnCompleted()); 25 | List> actualRecords = asList(); 26 | List> expectedRecords = new ArrayList<>(); 27 | expectedRecords.add(onCompletedEvent); 28 | 29 | // when 30 | RecordedStreamComparator.StreamComparison result 31 | = new RecordedStreamComparator().compare(actualRecords, expectedRecords); 32 | // then 33 | assertThat(result.streamEquals).isFalse(); 34 | assertThat(result.unitComparisons).hasSize(1); 35 | assertThat(result.unitComparisons.get(0)).isEqualToComparingFieldByField( 36 | new RecordedStreamComparator.EventComparison(onCompletedEvent, RecordedStreamComparator.EventComparisonResult.ONLY_ON_EXPECTED) 37 | ); 38 | } 39 | 40 | @Test 41 | public void should_detect_additional_event_in_actual_records() { 42 | // given 43 | Recorded onCompletedEvent = new Recorded<>(10, Notification.createOnCompleted()); 44 | List> actualRecords = new ArrayList<>(); 45 | actualRecords.add(onCompletedEvent); 46 | List> expectedRecords = asList(); 47 | 48 | // when 49 | RecordedStreamComparator.StreamComparison result 50 | = new RecordedStreamComparator().compare(actualRecords, expectedRecords); 51 | // then 52 | assertThat(result.streamEquals).isFalse(); 53 | assertThat(result.unitComparisons).hasSize(1); 54 | assertThat(result.unitComparisons.get(0)).isEqualToComparingFieldByField( 55 | new RecordedStreamComparator.EventComparison(onCompletedEvent, ONLY_ON_ACTUAL) 56 | ); 57 | } 58 | 59 | @Test 60 | public void should_detect_identical_streams() { 61 | // given 62 | List> actualRecords = new ArrayList<>(); 63 | actualRecords.add(new Recorded<>(10, createOnNext(12))); 64 | List> expectedRecords = new ArrayList<>(); 65 | expectedRecords.add(new Recorded<>(10, createOnNext(12))); 66 | // when 67 | RecordedStreamComparator.StreamComparison result 68 | = new RecordedStreamComparator().compare(actualRecords, expectedRecords); 69 | // then 70 | assertThat(result.streamEquals).isTrue(); 71 | } 72 | 73 | @Test 74 | public void should_detect_identical_streams_ending_on_error() { 75 | // given 76 | List> actualRecords = new ArrayList<>(); 77 | actualRecords.add(new Recorded<>(10, createOnError(new Exception("whatever")))); 78 | List> expectedRecords = new ArrayList<>(); 79 | expectedRecords.add(new Recorded<>(10, createOnError(new Exception("whatever")))); 80 | // when 81 | RecordedStreamComparator.StreamComparison result 82 | = new RecordedStreamComparator().compare(actualRecords, expectedRecords); 83 | // then 84 | assertThat(result.streamEquals).isTrue(); 85 | } 86 | 87 | @Test 88 | public void should_detect_equal_and_different_records() { 89 | // given 90 | Recorded onCompletedEvent = new Recorded<>(20, Notification.createOnCompleted()); 91 | List> actualRecords = asList( 92 | new Recorded<>(5, createOnNext(12)), 93 | onCompletedEvent 94 | ); 95 | List> expectedRecords = asList( 96 | new Recorded<>(15, createOnNext(36)), 97 | onCompletedEvent 98 | ); 99 | // when 100 | RecordedStreamComparator.StreamComparison result 101 | = new RecordedStreamComparator().compare(actualRecords, expectedRecords); 102 | // then 103 | assertThat(result.streamEquals).isFalse(); 104 | assertThat(result.unitComparisons).hasSize(3); 105 | assertThat(result.unitComparisons).containsExactly( 106 | new RecordedStreamComparator.EventComparison(new Recorded<>(5, createOnNext((Object)12)), ONLY_ON_ACTUAL), 107 | new RecordedStreamComparator.EventComparison(new Recorded<>(15, createOnNext((Object)36)), ONLY_ON_EXPECTED), 108 | new RecordedStreamComparator.EventComparison(onCompletedEvent, EQUALS) 109 | ); 110 | 111 | } 112 | 113 | @Test 114 | public void should_put_on_completed_events_after() { 115 | // given 116 | Recorded onCompletedEvent = new Recorded<>(20, Notification.createOnCompleted()); 117 | List> actualRecords = asList( 118 | new Recorded<>(20, createOnNext(12)), 119 | onCompletedEvent 120 | ); 121 | List> expectedRecords = new ArrayList<>(); 122 | expectedRecords.add(new Recorded<>(20, createOnNext(12))); 123 | // when 124 | RecordedStreamComparator.StreamComparison result 125 | = new RecordedStreamComparator().compare(actualRecords, expectedRecords); 126 | // then 127 | assertThat(result.streamEquals).isFalse(); 128 | assertThat(result.unitComparisons).hasSize(2); 129 | assertThat(result.unitComparisons).containsExactly( 130 | new RecordedStreamComparator.EventComparison(new Recorded<>(20, createOnNext(12)), EQUALS), 131 | new RecordedStreamComparator.EventComparison(onCompletedEvent, ONLY_ON_ACTUAL) 132 | ); 133 | 134 | } 135 | 136 | 137 | } -------------------------------------------------------------------------------- /src/test/java/io/reactivex/marble/RecordedStreamComparatorTest.java: -------------------------------------------------------------------------------- 1 | package io.reactivex.marble; 2 | 3 | import org.junit.Test; 4 | import rx.Notification; 5 | import rx.marble.Recorded; 6 | import rx.marble.RecordedStreamComparator; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | import static java.util.Arrays.asList; 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static rx.Notification.createOnError; 14 | import static rx.Notification.createOnNext; 15 | import static rx.marble.RecordedStreamComparator.EventComparisonResult.*; 16 | 17 | /** 18 | * Created by Alexandre Victoor on 26/10/2016. 19 | */ 20 | public class RecordedStreamComparatorTest { 21 | 22 | 23 | @Test 24 | public void should_detect_missing_event_in_actual_records() { 25 | // given 26 | Recorded onCompletedEvent = new Recorded<>(10, Notification.createOnCompleted()); 27 | List> actualRecords = asList(); 28 | List> expectedRecords = new ArrayList<>(); 29 | expectedRecords.add(onCompletedEvent); 30 | 31 | // when 32 | RecordedStreamComparator.StreamComparison result 33 | = new RecordedStreamComparator().compare(actualRecords, expectedRecords); 34 | // then 35 | assertThat(result.streamEquals).isFalse(); 36 | assertThat(result.unitComparisons).hasSize(1); 37 | assertThat(result.unitComparisons.get(0)).isEqualToComparingFieldByField( 38 | new RecordedStreamComparator.EventComparison(onCompletedEvent, RecordedStreamComparator.EventComparisonResult.ONLY_ON_EXPECTED) 39 | ); 40 | } 41 | 42 | @Test 43 | public void should_detect_additional_event_in_actual_records() { 44 | // given 45 | Recorded onCompletedEvent = new Recorded<>(10, Notification.createOnCompleted()); 46 | List> actualRecords = new ArrayList<>(); 47 | actualRecords.add(onCompletedEvent); 48 | List> expectedRecords = asList(); 49 | 50 | // when 51 | RecordedStreamComparator.StreamComparison result 52 | = new RecordedStreamComparator().compare(actualRecords, expectedRecords); 53 | // then 54 | assertThat(result.streamEquals).isFalse(); 55 | assertThat(result.unitComparisons).hasSize(1); 56 | assertThat(result.unitComparisons.get(0)).isEqualToComparingFieldByField( 57 | new RecordedStreamComparator.EventComparison(onCompletedEvent, ONLY_ON_ACTUAL) 58 | ); 59 | } 60 | 61 | @Test 62 | public void should_detect_identical_streams() { 63 | // given 64 | List> actualRecords = new ArrayList<>(); 65 | actualRecords.add(new Recorded<>(10, createOnNext(12))); 66 | List> expectedRecords = new ArrayList<>(); 67 | expectedRecords.add(new Recorded<>(10, createOnNext(12))); 68 | // when 69 | RecordedStreamComparator.StreamComparison result 70 | = new RecordedStreamComparator().compare(actualRecords, expectedRecords); 71 | // then 72 | assertThat(result.streamEquals).isTrue(); 73 | } 74 | 75 | @Test 76 | public void should_detect_identical_streams_ending_on_error() { 77 | // given 78 | List> actualRecords = new ArrayList<>(); 79 | actualRecords.add(new Recorded<>(10, createOnError(new Exception("whatever")))); 80 | List> expectedRecords = new ArrayList<>(); 81 | expectedRecords.add(new Recorded<>(10, createOnError(new Exception("whatever")))); 82 | // when 83 | RecordedStreamComparator.StreamComparison result 84 | = new RecordedStreamComparator().compare(actualRecords, expectedRecords); 85 | // then 86 | assertThat(result.streamEquals).isTrue(); 87 | } 88 | 89 | @Test 90 | public void should_detect_equal_and_different_records() { 91 | // given 92 | Recorded onCompletedEvent = new Recorded<>(20, Notification.createOnCompleted()); 93 | List> actualRecords = asList( 94 | new Recorded<>(5, createOnNext(12)), 95 | onCompletedEvent 96 | ); 97 | List> expectedRecords = asList( 98 | new Recorded<>(15, createOnNext(36)), 99 | onCompletedEvent 100 | ); 101 | // when 102 | RecordedStreamComparator.StreamComparison result 103 | = new RecordedStreamComparator().compare(actualRecords, expectedRecords); 104 | // then 105 | assertThat(result.streamEquals).isFalse(); 106 | assertThat(result.unitComparisons).hasSize(3); 107 | assertThat(result.unitComparisons).containsExactly( 108 | new RecordedStreamComparator.EventComparison(new Recorded<>(5, createOnNext((Object)12)), ONLY_ON_ACTUAL), 109 | new RecordedStreamComparator.EventComparison(new Recorded<>(15, createOnNext((Object)36)), ONLY_ON_EXPECTED), 110 | new RecordedStreamComparator.EventComparison(onCompletedEvent, EQUALS) 111 | ); 112 | 113 | } 114 | 115 | @Test 116 | public void should_put_on_completed_events_after() { 117 | // given 118 | Recorded onCompletedEvent = new Recorded<>(20, Notification.createOnCompleted()); 119 | List> actualRecords = asList( 120 | new Recorded<>(20, createOnNext(12)), 121 | onCompletedEvent 122 | ); 123 | List> expectedRecords = new ArrayList<>(); 124 | expectedRecords.add(new Recorded<>(20, createOnNext(12))); 125 | // when 126 | RecordedStreamComparator.StreamComparison result 127 | = new RecordedStreamComparator().compare(actualRecords, expectedRecords); 128 | // then 129 | assertThat(result.streamEquals).isFalse(); 130 | assertThat(result.unitComparisons).hasSize(2); 131 | assertThat(result.unitComparisons).containsExactly( 132 | new RecordedStreamComparator.EventComparison(new Recorded<>(20, createOnNext(12)), EQUALS), 133 | new RecordedStreamComparator.EventComparison(onCompletedEvent, ONLY_ON_ACTUAL) 134 | ); 135 | 136 | } 137 | 138 | 139 | } -------------------------------------------------------------------------------- /src/test/java/reactor/ColdFluxTest.java: -------------------------------------------------------------------------------- 1 | package reactor; 2 | 3 | import io.reactivex.subscribers.TestSubscriber; 4 | import org.junit.Test; 5 | import org.reactivestreams.Notification; 6 | import org.reactivestreams.Recorded; 7 | import org.reactivestreams.SubscriptionLog; 8 | import reactor.test.scheduler.VirtualTimeScheduler; 9 | 10 | import java.time.Duration; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | 15 | 16 | public class ColdFluxTest { 17 | 18 | 19 | @Test 20 | public void should_send_notification_on_subscribe() { 21 | // given 22 | VirtualTimeScheduler scheduler = VirtualTimeScheduler.create(); 23 | Recorded event = new Recorded<>(0, Notification.createOnNext("Hello world!")); 24 | ColdFlux coldObservable = ColdFlux.create(scheduler, event); 25 | // when 26 | TestSubscriber observer = new TestSubscriber<>(); 27 | coldObservable.subscribe(observer); 28 | // then 29 | scheduler.advanceTimeBy(Duration.ofSeconds(1)); 30 | observer.assertValue("Hello world!"); 31 | } 32 | 33 | @Test 34 | public void should_send_notification_on_subscribe_using_offset() { 35 | // given 36 | VirtualTimeScheduler scheduler = VirtualTimeScheduler.create(); 37 | long offset = 10; 38 | Recorded event = new Recorded<>(offset, Notification.createOnNext("Hello world!")); 39 | ColdFlux coldObservable = ColdFlux.create(scheduler, event); 40 | // when 41 | TestSubscriber observer = new TestSubscriber<>(); 42 | coldObservable.subscribe(observer); 43 | // then 44 | scheduler.advanceTimeBy(Duration.ofMillis(9)); 45 | observer.assertNoValues(); 46 | scheduler.advanceTimeBy(Duration.ofMillis(1)); 47 | observer.assertValue("Hello world!"); 48 | } 49 | 50 | @Test 51 | public void should_not_send_notification_after_unsubscribe() { 52 | // given 53 | VirtualTimeScheduler scheduler = VirtualTimeScheduler.create(); 54 | long offset = 10; 55 | Recorded event = new Recorded<>(offset, Notification.createOnNext("Hello world!")); 56 | ColdFlux coldObservable = ColdFlux.create(scheduler, event); 57 | final TestSubscriber observer = new TestSubscriber<>(); 58 | coldObservable.subscribe(observer); 59 | // when 60 | scheduler.schedule(new Runnable() { 61 | @Override 62 | public void run() { 63 | observer.dispose(); 64 | } 65 | }, 5, TimeUnit.MILLISECONDS); 66 | // then 67 | scheduler.advanceTimeBy(Duration.ofNanos(Long.MAX_VALUE)); 68 | observer.assertNoValues(); 69 | } 70 | 71 | @Test 72 | public void should_be_cold_and_send_notification_at_subscribe_time() { 73 | // given 74 | VirtualTimeScheduler scheduler = VirtualTimeScheduler.create(); 75 | Recorded event = new Recorded<>(0, Notification.createOnNext("Hello world!")); 76 | final ColdFlux coldObservable = ColdFlux.create(scheduler, event); 77 | // when 78 | final TestSubscriber observer = new TestSubscriber<>(); 79 | scheduler.schedule(new Runnable() { 80 | @Override 81 | public void run() { 82 | coldObservable.subscribe(observer); 83 | } 84 | }, 42, TimeUnit.SECONDS); 85 | // then 86 | scheduler.advanceTimeBy(Duration.ofSeconds(42)); 87 | observer.assertValue("Hello world!"); 88 | } 89 | 90 | @Test 91 | public void should_keep_track_of_subscriptions() { 92 | // given 93 | VirtualTimeScheduler scheduler = VirtualTimeScheduler.create(); 94 | final ColdFlux coldObservable = ColdFlux.create(scheduler); 95 | // when 96 | final TestSubscriber observer = new TestSubscriber<>(); 97 | 98 | scheduler.schedule(new Runnable() { 99 | @Override 100 | public void run() { 101 | coldObservable.subscribe(observer); 102 | } 103 | }, 42, TimeUnit.MILLISECONDS); 104 | // then 105 | scheduler.advanceTimeBy(Duration.ofMillis(42)); 106 | assertThat(coldObservable.getSubscriptions()) 107 | .containsExactly( 108 | new SubscriptionLog(42, Long.MAX_VALUE) 109 | ); 110 | } 111 | 112 | @Test 113 | public void should_keep_track_of_unsubscriptions() { 114 | // given 115 | VirtualTimeScheduler scheduler = VirtualTimeScheduler.create(); 116 | final ColdFlux coldObservable = ColdFlux.create(scheduler); 117 | // when 118 | final TestSubscriber observer = new TestSubscriber<>(); 119 | coldObservable.subscribe(observer); 120 | scheduler.schedule(new Runnable() { 121 | @Override 122 | public void run() { 123 | observer.dispose(); 124 | } 125 | }, 42, TimeUnit.MILLISECONDS); 126 | // then 127 | scheduler.advanceTimeBy(Duration.ofMillis(42)); 128 | assertThat(coldObservable.getSubscriptions()) 129 | .containsExactly( 130 | new SubscriptionLog(0, 42) 131 | ); 132 | } 133 | 134 | @Test 135 | public void should_keep_track_of_several_subscriptions() { 136 | // given 137 | VirtualTimeScheduler scheduler = VirtualTimeScheduler.create(); 138 | final ColdFlux coldObservable = ColdFlux.create(scheduler); 139 | // when 140 | final TestSubscriber observer1 = new TestSubscriber<>(); 141 | final TestSubscriber observer2 = new TestSubscriber<>(); 142 | coldObservable.subscribe(observer1); 143 | scheduler.schedule(new Runnable() { 144 | @Override 145 | public void run() { 146 | coldObservable.subscribe(observer2); 147 | } 148 | }, 36, TimeUnit.MILLISECONDS); 149 | scheduler.schedule(new Runnable() { 150 | @Override 151 | public void run() { 152 | observer1.dispose(); 153 | } 154 | }, 42, TimeUnit.MILLISECONDS); 155 | // then 156 | scheduler.advanceTimeBy(Duration.ofMillis(42)); 157 | assertThat(coldObservable.getSubscriptions()) 158 | .containsExactly( 159 | new SubscriptionLog(0, 42), 160 | new SubscriptionLog(36, Long.MAX_VALUE) 161 | ); 162 | } 163 | } -------------------------------------------------------------------------------- /src/test/java/io/reactivex/marble/ColdObservableTest.java: -------------------------------------------------------------------------------- 1 | package io.reactivex.marble; 2 | 3 | import io.reactivex.observers.TestObserver; 4 | import io.reactivex.schedulers.TestScheduler; 5 | import org.junit.Test; 6 | import org.reactivestreams.Notification; 7 | import org.reactivestreams.Recorded; 8 | import org.reactivestreams.SubscriptionLog; 9 | 10 | import java.util.concurrent.TimeUnit; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | 15 | public class ColdObservableTest { 16 | 17 | 18 | @Test 19 | public void should_send_notification_on_subscribe() { 20 | // given 21 | TestScheduler scheduler = new TestScheduler(); 22 | Recorded event = new Recorded<>(0, Notification.createOnNext("Hello world!")); 23 | ColdObservable coldObservable = ColdObservable.create(scheduler, event); 24 | // when 25 | TestObserver observer = new TestObserver<>(); 26 | coldObservable.subscribe(observer); 27 | // then 28 | scheduler.advanceTimeBy(1, TimeUnit.SECONDS); 29 | observer.assertValue("Hello world!"); 30 | } 31 | 32 | @Test 33 | public void should_send_notification_on_subscribe_using_offset() { 34 | // given 35 | TestScheduler scheduler = new TestScheduler(); 36 | long offset = 10; 37 | Recorded event = new Recorded<>(offset, Notification.createOnNext("Hello world!")); 38 | ColdObservable coldObservable = ColdObservable.create(scheduler, event); 39 | // when 40 | TestObserver observer = new TestObserver<>(); 41 | coldObservable.subscribe(observer); 42 | // then 43 | scheduler.advanceTimeBy(9, TimeUnit.MILLISECONDS); 44 | observer.assertNoValues(); 45 | scheduler.advanceTimeBy(1, TimeUnit.MILLISECONDS); 46 | observer.assertValue("Hello world!"); 47 | } 48 | 49 | @Test 50 | public void should_not_send_notification_after_unsubscribe() { 51 | // given 52 | TestScheduler scheduler = new TestScheduler(); 53 | long offset = 10; 54 | Recorded event = new Recorded<>(offset, Notification.createOnNext("Hello world!")); 55 | ColdObservable coldObservable = ColdObservable.create(scheduler, event); 56 | final TestObserver observer = new TestObserver<>(); 57 | coldObservable.subscribe(observer); 58 | // when 59 | scheduler.createWorker().schedule(new Runnable() { 60 | @Override 61 | public void run() { 62 | observer.dispose(); 63 | } 64 | }, 5, TimeUnit.MILLISECONDS); 65 | // then 66 | scheduler.advanceTimeBy(Long.MAX_VALUE, TimeUnit.MILLISECONDS); 67 | observer.assertNoValues(); 68 | } 69 | 70 | @Test 71 | public void should_be_cold_and_send_notification_at_subscribe_time() { 72 | // given 73 | TestScheduler scheduler = new TestScheduler(); 74 | Recorded event = new Recorded<>(0, Notification.createOnNext("Hello world!")); 75 | final ColdObservable coldObservable = ColdObservable.create(scheduler, event); 76 | // when 77 | final TestObserver observer = new TestObserver<>(); 78 | scheduler.createWorker().schedule(new Runnable() { 79 | @Override 80 | public void run() { 81 | coldObservable.subscribe(observer); 82 | } 83 | }, 42, TimeUnit.SECONDS); 84 | // then 85 | scheduler.advanceTimeBy(42, TimeUnit.SECONDS); 86 | observer.assertValue("Hello world!"); 87 | } 88 | 89 | @Test 90 | public void should_keep_track_of_subscriptions() { 91 | // given 92 | TestScheduler scheduler = new TestScheduler(); 93 | final ColdObservable coldObservable = ColdObservable.create(scheduler); 94 | // when 95 | final TestObserver observer = new TestObserver<>(); 96 | 97 | scheduler.createWorker().schedule(new Runnable() { 98 | @Override 99 | public void run() { 100 | coldObservable.subscribe(observer); 101 | } 102 | }, 42, TimeUnit.MILLISECONDS); 103 | // then 104 | scheduler.advanceTimeBy(42, TimeUnit.MILLISECONDS); 105 | assertThat(coldObservable.getSubscriptions()) 106 | .containsExactly( 107 | new SubscriptionLog(42, Long.MAX_VALUE) 108 | ); 109 | } 110 | 111 | @Test 112 | public void should_keep_track_of_unsubscriptions() { 113 | // given 114 | TestScheduler scheduler = new TestScheduler(); 115 | final ColdObservable coldObservable = ColdObservable.create(scheduler); 116 | // when 117 | final TestObserver observer = new TestObserver<>(); 118 | coldObservable.subscribe(observer); 119 | scheduler.createWorker().schedule(new Runnable() { 120 | @Override 121 | public void run() { 122 | observer.dispose(); 123 | } 124 | }, 42, TimeUnit.MILLISECONDS); 125 | // then 126 | scheduler.advanceTimeBy(42, TimeUnit.MILLISECONDS); 127 | assertThat(coldObservable.getSubscriptions()) 128 | .containsExactly( 129 | new SubscriptionLog(0, 42) 130 | ); 131 | } 132 | 133 | @Test 134 | public void should_keep_track_of_several_subscriptions() { 135 | // given 136 | TestScheduler scheduler = new TestScheduler(); 137 | final ColdObservable coldObservable = ColdObservable.create(scheduler); 138 | // when 139 | final TestObserver observer1 = new TestObserver<>(); 140 | final TestObserver observer2 = new TestObserver<>(); 141 | coldObservable.subscribe(observer1); 142 | scheduler.createWorker().schedule(new Runnable() { 143 | @Override 144 | public void run() { 145 | coldObservable.subscribe(observer2); 146 | } 147 | }, 36, TimeUnit.MILLISECONDS); 148 | scheduler.createWorker().schedule(new Runnable() { 149 | @Override 150 | public void run() { 151 | observer1.dispose(); 152 | } 153 | }, 42, TimeUnit.MILLISECONDS); 154 | // then 155 | scheduler.advanceTimeBy(42, TimeUnit.MILLISECONDS); 156 | assertThat(coldObservable.getSubscriptions()) 157 | .containsExactly( 158 | new SubscriptionLog(0, 42), 159 | new SubscriptionLog(36, Long.MAX_VALUE) 160 | ); 161 | } 162 | } -------------------------------------------------------------------------------- /src/test/java/rx/marble/ParserTest.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | 4 | import org.junit.Test; 5 | import rx.Notification; 6 | 7 | import java.util.Arrays; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | 15 | public class ParserTest { 16 | 17 | @Test 18 | public void should_parse_a_marble_string_into_a_series_of_notifications_and_types() { 19 | Map events = new HashMap<>(); 20 | events.put("a", "A"); 21 | events.put("b", "B"); 22 | List> result = Parser.parseMarbles("-------a---b---|", events, 10); 23 | 24 | assertThat(result).containsExactly( 25 | new Recorded<>(70, Notification.createOnNext((Object)"A")), 26 | new Recorded<>(110, Notification.createOnNext((Object)"B")), 27 | new Recorded<>(150, Notification.createOnCompleted()) 28 | ); 29 | } 30 | 31 | @Test 32 | public void should_parse_a_marble_string_allowing_spaces_too() { 33 | Map events = new HashMap<>(); 34 | events.put("a", "A"); 35 | events.put("b", "B"); 36 | List> result = Parser.parseMarbles("--a--b--| ", events, 10); 37 | 38 | assertThat(result).containsExactly( 39 | new Recorded<>(20, Notification.createOnNext((Object)"A")), 40 | new Recorded<>(50, Notification.createOnNext((Object)"B")), 41 | new Recorded<>(80, Notification.createOnCompleted()) 42 | ); 43 | } 44 | 45 | @Test 46 | public void should_parse_a_marble_string_with_a_subscription_point() { 47 | Map events = new HashMap<>(); 48 | events.put("a", "A"); 49 | events.put("b", "B"); 50 | List> result = Parser.parseMarbles("---^---a---b---|", events, 10); 51 | 52 | assertThat(result).containsExactly( 53 | new Recorded<>(40, Notification.createOnNext((Object)"A")), 54 | new Recorded<>(80, Notification.createOnNext((Object)"B")), 55 | new Recorded<>(120, Notification.createOnCompleted()) 56 | ); 57 | } 58 | 59 | @Test 60 | public void should_parse_a_marble_string_with_an_error() { 61 | Exception errorValue = new Exception("omg error!"); 62 | Map events = new HashMap<>(); 63 | events.put("a", "A"); 64 | events.put("b", "B"); 65 | List> result = Parser.parseMarbles("-------a---b---#", events, errorValue, 10); 66 | 67 | assertThat(result).containsExactly( 68 | new Recorded<>(70, Notification.createOnNext((Object)"A")), 69 | new Recorded<>(110, Notification.createOnNext((Object)"B")), 70 | new Recorded<>(150, Notification.createOnError(errorValue)) 71 | ); 72 | } 73 | 74 | @Test 75 | public void should_default_in_the_letter_for_the_value_if_no_value_hash_was_passed() { 76 | List> result = Parser.parseMarbles("--a--b--|", 10); 77 | 78 | assertThat(result).containsExactly( 79 | new Recorded<>(20, Notification.createOnNext("a")), 80 | new Recorded<>(50, Notification.createOnNext("b")), 81 | new Recorded<>(80, Notification.createOnCompleted()) 82 | ); 83 | } 84 | 85 | @Test 86 | public void should_handle_grouped_values() { 87 | List> result = Parser.parseMarbles("---(abc)---", 10); 88 | 89 | assertThat(result).containsExactly( 90 | new Recorded<>(30, Notification.createOnNext("a")), 91 | new Recorded<>(30, Notification.createOnNext("b")), 92 | new Recorded<>(30, Notification.createOnNext("c")) 93 | ); 94 | } 95 | 96 | @Test 97 | public void should_handle_grouped_values_at_zero_time() { 98 | List> result = Parser.parseMarbles("(abc)---", 10); 99 | 100 | assertThat(result).containsExactly( 101 | new Recorded<>(0, Notification.createOnNext("a")), 102 | new Recorded<>(0, Notification.createOnNext("b")), 103 | new Recorded<>(0, Notification.createOnNext("c")) 104 | ); 105 | } 106 | 107 | @Test 108 | public void should_handle_value_after_grouped_values() { 109 | List> result = Parser.parseMarbles("---(abc)d--", 10); 110 | 111 | assertThat(result).containsExactly( 112 | new Recorded<>(30, Notification.createOnNext("a")), 113 | new Recorded<>(30, Notification.createOnNext("b")), 114 | new Recorded<>(30, Notification.createOnNext("c")), 115 | new Recorded<>(80, Notification.createOnNext("d")) 116 | ); 117 | } 118 | 119 | @Test 120 | public void should_parse_a_subscription_marble_string_into_a_subscriptionLog() { 121 | SubscriptionLog result = Parser.parseMarblesAsSubscriptions("---^---!-", 10); 122 | 123 | assertThat(result.subscribe).isEqualTo(30); 124 | assertThat(result.unsubscribe).isEqualTo(70); 125 | } 126 | 127 | @Test 128 | public void should_parse_a_subscription_marble_without_an_unsubscription() { 129 | SubscriptionLog result = Parser.parseMarblesAsSubscriptions("---^---", 10); 130 | 131 | assertThat(result.subscribe).isEqualTo(30); 132 | assertThat(result.unsubscribe).isEqualTo(Long.MAX_VALUE); 133 | } 134 | 135 | @Test 136 | public void should_parse_a_subscription_marble_with_a_synchronous_unsubscription() { 137 | SubscriptionLog result = Parser.parseMarblesAsSubscriptions("---(^!)---", 10); 138 | 139 | assertThat(result.subscribe).isEqualTo(30); 140 | assertThat(result.unsubscribe).isEqualTo(30); 141 | } 142 | 143 | @Test 144 | public void should_parse_a_marble_string_with_observable_values() { 145 | 146 | ColdObservable aObservable 147 | = ColdObservable.create(null, new Recorded<>(20, Notification.createOnNext(123))); 148 | Map events = new HashMap<>(); 149 | events.put("a", aObservable); 150 | List> result = Parser.parseMarbles("-a-", events, null, 10, true); 151 | 152 | assertThat(result).containsExactly( 153 | new Recorded<>(10, 154 | Notification.createOnNext( 155 | (Object)Arrays.asList( 156 | new Recorded<>(20, Notification.createOnNext(123)) 157 | ) 158 | ) 159 | ) 160 | ); 161 | } 162 | } -------------------------------------------------------------------------------- /src/test/java/org/reactivestreams/ParserTest.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.Arrays; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | /** 13 | * Created by Alexandre Victoor on 20/04/2017. 14 | */ 15 | public class ParserTest { 16 | 17 | @Test 18 | public void should_parse_a_marble_string_into_a_series_of_notifications_and_types() { 19 | Map events = new HashMap<>(); 20 | events.put("a", "A"); 21 | events.put("b", "B"); 22 | List> result = Parser.parseMarbles("-------a---b---|", events, 10); 23 | 24 | assertThat(result).containsExactly( 25 | new Recorded<>(70, Notification.createOnNext((Object)"A")), 26 | new Recorded<>(110, Notification.createOnNext((Object)"B")), 27 | new Recorded<>(150, Notification.createOnComplete()) 28 | ); 29 | } 30 | 31 | @Test 32 | public void should_parse_a_marble_string_allowing_spaces_too() { 33 | Map events = new HashMap<>(); 34 | events.put("a", "A"); 35 | events.put("b", "B"); 36 | List> result = Parser.parseMarbles("--a--b--| ", events, 10); 37 | 38 | assertThat(result).containsExactly( 39 | new Recorded<>(20, Notification.createOnNext((Object)"A")), 40 | new Recorded<>(50, Notification.createOnNext((Object)"B")), 41 | new Recorded<>(80, Notification.createOnComplete()) 42 | ); 43 | } 44 | 45 | @Test 46 | public void should_parse_a_marble_string_with_a_subscription_point() { 47 | Map events = new HashMap<>(); 48 | events.put("a", "A"); 49 | events.put("b", "B"); 50 | List> result = Parser.parseMarbles("---^---a---b---|", events, 10); 51 | 52 | assertThat(result).containsExactly( 53 | new Recorded<>(40, Notification.createOnNext((Object)"A")), 54 | new Recorded<>(80, Notification.createOnNext((Object)"B")), 55 | new Recorded<>(120, Notification.createOnComplete()) 56 | ); 57 | } 58 | 59 | @Test 60 | public void should_parse_a_marble_string_with_an_error() { 61 | Exception errorValue = new Exception("omg error!"); 62 | Map events = new HashMap<>(); 63 | events.put("a", "A"); 64 | events.put("b", "B"); 65 | List> result = Parser.parseMarbles("-------a---b---#", events, errorValue, 10); 66 | 67 | assertThat(result).containsExactly( 68 | new Recorded<>(70, Notification.createOnNext((Object)"A")), 69 | new Recorded<>(110, Notification.createOnNext((Object)"B")), 70 | new Recorded<>(150, Notification.createOnError(errorValue)) 71 | ); 72 | } 73 | 74 | @Test 75 | public void should_default_in_the_letter_for_the_value_if_no_value_hash_was_passed() { 76 | List> result = Parser.parseMarbles("--a--b--|", 10); 77 | 78 | assertThat(result).containsExactly( 79 | new Recorded<>(20, Notification.createOnNext("a")), 80 | new Recorded<>(50, Notification.createOnNext("b")), 81 | new Recorded<>(80, Notification.createOnComplete()) 82 | ); 83 | } 84 | 85 | @Test 86 | public void should_handle_grouped_values() { 87 | List> result = Parser.parseMarbles("---(abc)---", 10); 88 | 89 | assertThat(result).containsExactly( 90 | new Recorded<>(30, Notification.createOnNext("a")), 91 | new Recorded<>(30, Notification.createOnNext("b")), 92 | new Recorded<>(30, Notification.createOnNext("c")) 93 | ); 94 | } 95 | 96 | @Test 97 | public void should_handle_grouped_values_at_zero_time() { 98 | List> result = Parser.parseMarbles("(abc)---", 10); 99 | 100 | assertThat(result).containsExactly( 101 | new Recorded<>(0, Notification.createOnNext("a")), 102 | new Recorded<>(0, Notification.createOnNext("b")), 103 | new Recorded<>(0, Notification.createOnNext("c")) 104 | ); 105 | } 106 | 107 | @Test 108 | public void should_handle_value_after_grouped_values() { 109 | List> result = Parser.parseMarbles("---(abc)d--", 10); 110 | 111 | assertThat(result).containsExactly( 112 | new Recorded<>(30, Notification.createOnNext("a")), 113 | new Recorded<>(30, Notification.createOnNext("b")), 114 | new Recorded<>(30, Notification.createOnNext("c")), 115 | new Recorded<>(80, Notification.createOnNext("d")) 116 | ); 117 | } 118 | 119 | @Test 120 | public void should_parse_a_subscription_marble_string_into_a_subscriptionLog() { 121 | SubscriptionLog result = Parser.parseMarblesAsSubscriptions("---^---!-", 10); 122 | 123 | assertThat(result.subscribe).isEqualTo(30); 124 | assertThat(result.unsubscribe).isEqualTo(70); 125 | } 126 | 127 | @Test 128 | public void should_parse_a_subscription_marble_without_an_unsubscription() { 129 | SubscriptionLog result = Parser.parseMarblesAsSubscriptions("---^---", 10); 130 | 131 | assertThat(result.subscribe).isEqualTo(30); 132 | assertThat(result.unsubscribe).isEqualTo(Long.MAX_VALUE); 133 | } 134 | 135 | @Test 136 | public void should_parse_a_subscription_marble_with_a_synchronous_unsubscription() { 137 | SubscriptionLog result = Parser.parseMarblesAsSubscriptions("---(^!)---", 10); 138 | 139 | assertThat(result.subscribe).isEqualTo(30); 140 | assertThat(result.unsubscribe).isEqualTo(30); 141 | } 142 | 143 | @Test 144 | public void should_parse_a_marble_string_with_observable_values() { 145 | 146 | ColdPublisher aObservable 147 | = new ColdPublisher<>(null, Arrays.asList(new Recorded<>(20, Notification.createOnNext(123)))); 148 | Map events = new HashMap<>(); 149 | events.put("a", aObservable); 150 | List> result = Parser.parseMarbles("-a-", events, null, 10, true); 151 | 152 | assertThat(result).containsExactly( 153 | new Recorded<>(10, 154 | Notification.createOnNext( 155 | (Object) Arrays.asList( 156 | new Recorded<>(20, Notification.createOnNext(123)) 157 | ) 158 | ) 159 | ) 160 | ); 161 | } 162 | } -------------------------------------------------------------------------------- /src/main/java/org/reactivestreams/Notification.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | /** 4 | * Copy/pasted from rxjava Notification class 5 | */ 6 | public final class Notification { 7 | 8 | private final Kind kind; 9 | private final Throwable throwable; 10 | private final T value; 11 | 12 | private static final Notification ON_COMPLETE = new Notification(Kind.OnComplete, null, null); 13 | 14 | /** 15 | * Creates and returns a {@code Notification} of variety {@code Kind.OnNext}, and assigns it a value. 16 | * 17 | * @param the actual value type held by the Notification 18 | * @param t 19 | * the item to assign to the notification as its value 20 | * @return an {@code OnNext} variety of {@code Notification} 21 | */ 22 | public static Notification createOnNext(T t) { 23 | return new Notification(Kind.OnNext, t, null); 24 | } 25 | 26 | /** 27 | * Creates and returns a {@code Notification} of variety {@code Kind.OnError}, and assigns it an exception. 28 | * 29 | * @param the actual value type held by the Notification 30 | * @param e 31 | * the exception to assign to the notification 32 | * @return an {@code OnError} variety of {@code Notification} 33 | */ 34 | public static Notification createOnError(Throwable e) { 35 | return new Notification(Kind.OnError, null, e); 36 | } 37 | 38 | /** 39 | * Creates and returns a {@code Notification} of variety {@code Kind.OnComplete}. 40 | * 41 | * @param the actual value type held by the Notification 42 | * @return an {@code OnComplete} variety of {@code Notification} 43 | */ 44 | @SuppressWarnings("unchecked") 45 | public static Notification createOnComplete() { 46 | return (Notification) ON_COMPLETE; 47 | } 48 | 49 | private Notification(Kind kind, T value, Throwable e) { 50 | this.value = value; 51 | this.throwable = e; 52 | this.kind = kind; 53 | } 54 | 55 | /** 56 | * Retrieves the exception associated with this (onError) notification. 57 | * 58 | * @return the Throwable associated with this (onError) notification 59 | */ 60 | public Throwable getThrowable() { 61 | return throwable; 62 | } 63 | 64 | /** 65 | * Retrieves the item associated with this (onNext) notification. 66 | * 67 | * @return the item associated with this (onNext) notification 68 | */ 69 | public T getValue() { 70 | return value; 71 | } 72 | 73 | /** 74 | * Indicates whether this notification has an item associated with it. 75 | * 76 | * @return a boolean indicating whether or not this notification has an item associated with it 77 | */ 78 | public boolean hasValue() { 79 | return isOnNext() && value != null; 80 | // isn't "null" a valid item? 81 | } 82 | 83 | /** 84 | * Indicates whether this notification has an exception associated with it. 85 | * 86 | * @return a boolean indicating whether this notification has an exception associated with it 87 | */ 88 | public boolean hasThrowable() { 89 | return isOnError() && throwable != null; 90 | } 91 | 92 | /** 93 | * Retrieves the kind of this notification: {@code OnNext}, {@code OnError}, or {@code OnComplete} 94 | * 95 | * @return the kind of the notification: {@code OnNext}, {@code OnError}, or {@code OnComplete} 96 | */ 97 | public Kind getKind() { 98 | return kind; 99 | } 100 | 101 | /** 102 | * Indicates whether this notification represents an {@code onError} event. 103 | * 104 | * @return a boolean indicating whether this notification represents an {@code onError} event 105 | */ 106 | public boolean isOnError() { 107 | return getKind() == Kind.OnError; 108 | } 109 | 110 | /** 111 | * Indicates whether this notification represents an {@code onCompleted} event. 112 | * 113 | * @return a boolean indicating whether this notification represents an {@code onCompleted} event 114 | */ 115 | public boolean isOnComplete() { 116 | return getKind() == Kind.OnComplete; 117 | } 118 | 119 | /** 120 | * Indicates whether this notification represents an {@code onNext} event. 121 | * 122 | * @return a boolean indicating whether this notification represents an {@code onNext} event 123 | */ 124 | public boolean isOnNext() { 125 | return getKind() == Kind.OnNext; 126 | } 127 | 128 | /** 129 | * Forwards this notification on to a specified {@link Subscriber}. 130 | * @param subscriber the target subscriber to call onXXX methods on based on the kind of this Notification instance 131 | */ 132 | public void accept(Subscriber subscriber) { 133 | if (kind == Kind.OnNext) { 134 | subscriber.onNext(getValue()); 135 | } else if (kind == Kind.OnComplete) { 136 | subscriber.onComplete(); 137 | } else { 138 | subscriber.onError(getThrowable()); 139 | } 140 | } 141 | 142 | /** 143 | * Specifies the kind of the notification: an element, an error or a completion notification. 144 | */ 145 | public enum Kind { 146 | OnNext, OnError, OnComplete 147 | } 148 | 149 | @Override 150 | public String toString() { 151 | StringBuilder str = new StringBuilder(64).append('[').append(super.toString()) 152 | .append(' ').append(getKind()); 153 | if (hasValue()) { 154 | str.append(' ').append(getValue()); 155 | } 156 | if (hasThrowable()) { 157 | str.append(' ').append(getThrowable().getMessage()); 158 | } 159 | str.append(']'); 160 | return str.toString(); 161 | } 162 | 163 | @Override 164 | public int hashCode() { 165 | int hash = getKind().hashCode(); 166 | if (hasValue()) { 167 | hash = hash * 31 + getValue().hashCode(); 168 | } 169 | if (hasThrowable()) { 170 | hash = hash * 31 + getThrowable().hashCode(); 171 | } 172 | return hash; 173 | } 174 | 175 | @Override 176 | public boolean equals(Object obj) { 177 | if (obj == null) { 178 | return false; 179 | } 180 | 181 | if (this == obj) { 182 | return true; 183 | } 184 | 185 | if (obj.getClass() != getClass()) { 186 | return false; 187 | } 188 | 189 | Notification notification = (Notification) obj; 190 | return notification.getKind() == getKind() && (value == notification.value || (value != null && value.equals(notification.value))) && (throwable == notification.throwable || (throwable != null && throwable.equals(notification.throwable))); 191 | 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/test/java/rx/marble/ColdObservableTest.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | import org.junit.Test; 4 | import rx.Notification; 5 | import rx.Subscription; 6 | import rx.functions.Action0; 7 | import rx.observers.TestSubscriber; 8 | import rx.schedulers.TestScheduler; 9 | 10 | import java.util.concurrent.TimeUnit; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | 15 | public class ColdObservableTest { 16 | 17 | 18 | @Test 19 | public void should_send_notification_on_subscribe() { 20 | // given 21 | TestScheduler scheduler = new TestScheduler(); 22 | Recorded event = new Recorded<>(0, Notification.createOnNext("Hello world!")); 23 | ColdObservable coldObservable = ColdObservable.create(scheduler, event); 24 | // when 25 | TestSubscriber subscriber = new TestSubscriber<>(); 26 | coldObservable.subscribe(subscriber); 27 | // then 28 | scheduler.advanceTimeBy(1, TimeUnit.SECONDS); 29 | assertThat(subscriber.getOnNextEvents()).containsExactly("Hello world!"); 30 | } 31 | 32 | @Test 33 | public void should_send_notification_on_subscribe_using_offset() { 34 | // given 35 | TestScheduler scheduler = new TestScheduler(); 36 | long offset = 10; 37 | Recorded event = new Recorded<>(offset, Notification.createOnNext("Hello world!")); 38 | ColdObservable coldObservable = ColdObservable.create(scheduler, event); 39 | // when 40 | TestSubscriber subscriber = new TestSubscriber<>(); 41 | coldObservable.subscribe(subscriber); 42 | // then 43 | scheduler.advanceTimeBy(9, TimeUnit.MILLISECONDS); 44 | assertThat(subscriber.getOnNextEvents()).isEmpty(); 45 | scheduler.advanceTimeBy(1, TimeUnit.MILLISECONDS); 46 | assertThat(subscriber.getOnNextEvents()).containsExactly("Hello world!"); 47 | } 48 | 49 | @Test 50 | public void should_not_send_notification_after_unsubscribe() { 51 | // given 52 | TestScheduler scheduler = new TestScheduler(); 53 | long offset = 10; 54 | Recorded event = new Recorded<>(offset, Notification.createOnNext("Hello world!")); 55 | ColdObservable coldObservable = ColdObservable.create(scheduler, event); 56 | TestSubscriber subscriber = new TestSubscriber<>(); 57 | final Subscription subscription = coldObservable.subscribe(subscriber); 58 | // when 59 | scheduler.createWorker().schedule(new Action0() { 60 | @Override 61 | public void call() { 62 | subscription.unsubscribe(); 63 | } 64 | }, 5, TimeUnit.MILLISECONDS); 65 | // then 66 | scheduler.advanceTimeBy(Long.MAX_VALUE, TimeUnit.MILLISECONDS); 67 | assertThat(subscriber.getOnNextEvents()).isEmpty(); 68 | } 69 | 70 | @Test 71 | public void should_be_cold_and_send_notification_at_subscribe_time() { 72 | // given 73 | TestScheduler scheduler = new TestScheduler(); 74 | Recorded event = new Recorded<>(0, Notification.createOnNext("Hello world!")); 75 | final ColdObservable coldObservable = ColdObservable.create(scheduler, event); 76 | // when 77 | final TestSubscriber subscriber = new TestSubscriber<>(); 78 | scheduler.createWorker().schedule(new Action0() { 79 | @Override 80 | public void call() { 81 | coldObservable.subscribe(subscriber); 82 | } 83 | }, 42, TimeUnit.SECONDS); 84 | // then 85 | scheduler.advanceTimeBy(42, TimeUnit.SECONDS); 86 | assertThat(subscriber.getOnNextEvents()).containsExactly("Hello world!"); 87 | } 88 | 89 | @Test 90 | public void should_keep_track_of_subscriptions() { 91 | // given 92 | TestScheduler scheduler = new TestScheduler(); 93 | final ColdObservable coldObservable = ColdObservable.create(scheduler); 94 | // when 95 | final TestSubscriber subscriber = new TestSubscriber<>(); 96 | 97 | scheduler.createWorker().schedule(new Action0() { 98 | @Override 99 | public void call() { 100 | coldObservable.subscribe(subscriber); 101 | } 102 | }, 42, TimeUnit.MILLISECONDS); 103 | // then 104 | scheduler.advanceTimeBy(42, TimeUnit.MILLISECONDS); 105 | assertThat(coldObservable.getSubscriptions()) 106 | .containsExactly( 107 | new SubscriptionLog(42, Long.MAX_VALUE) 108 | ); 109 | } 110 | 111 | @Test 112 | public void should_keep_track_of_unsubscriptions() { 113 | // given 114 | TestScheduler scheduler = new TestScheduler(); 115 | final ColdObservable coldObservable = ColdObservable.create(scheduler); 116 | // when 117 | final TestSubscriber subscriber = new TestSubscriber<>(); 118 | final Subscription subscription = coldObservable.subscribe(subscriber); 119 | scheduler.createWorker().schedule(new Action0() { 120 | @Override 121 | public void call() { 122 | subscription.unsubscribe(); 123 | } 124 | }, 42, TimeUnit.MILLISECONDS); 125 | // then 126 | scheduler.advanceTimeBy(42, TimeUnit.MILLISECONDS); 127 | assertThat(coldObservable.getSubscriptions()) 128 | .containsExactly( 129 | new SubscriptionLog(0, 42) 130 | ); 131 | } 132 | 133 | @Test 134 | public void should_keep_track_of_several_subscriptions() { 135 | // given 136 | TestScheduler scheduler = new TestScheduler(); 137 | final ColdObservable coldObservable = ColdObservable.create(scheduler); 138 | // when 139 | final TestSubscriber subscriber1 = new TestSubscriber<>(); 140 | final TestSubscriber subscriber2 = new TestSubscriber<>(); 141 | final Subscription subscription = coldObservable.subscribe(subscriber1); 142 | scheduler.createWorker().schedule(new Action0() { 143 | @Override 144 | public void call() { 145 | coldObservable.subscribe(subscriber2); 146 | } 147 | }, 36, TimeUnit.MILLISECONDS); 148 | scheduler.createWorker().schedule(new Action0() { 149 | @Override 150 | public void call() { 151 | subscription.unsubscribe(); 152 | } 153 | }, 42, TimeUnit.MILLISECONDS); 154 | // then 155 | scheduler.advanceTimeBy(42, TimeUnit.MILLISECONDS); 156 | assertThat(coldObservable.getSubscriptions()) 157 | .containsExactly( 158 | new SubscriptionLog(0, 42), 159 | new SubscriptionLog(36, Long.MAX_VALUE) 160 | ); 161 | } 162 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | com.github.alexvictoor 6 | marbletest4j 7 | marbletest4j 8 | 1.4-SNAPSHOT 9 | https://github.com/alexvictoor/marbletest4j 10 | Java port of RxJS marble tests 11 | 12 | 13 | https://github.com/alexvictoor/marbletest4j 14 | scm:git:git@github.com:alexvictoor/marbletest4j.git 15 | scm:git:git@github.com:alexvictoor/marbletest4j.git 16 | HEAD 17 | 18 | 19 | 20 | 21 | The Apache Software License, Version 2.0 22 | http://www.apache.org/licenses/LICENSE-2.0.txt 23 | repo 24 | 25 | 26 | 27 | 28 | https://github.com/alexvictoor/marbletest4j/issues 29 | GitHub 30 | 31 | 32 | 33 | 34 | ossrh 35 | https://oss.sonatype.org/content/repositories/snapshots 36 | 37 | 38 | ossrh 39 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 40 | 41 | 42 | 43 | 44 | 45 | alexvictoor 46 | Alexandre Victoor 47 | alexvictoor@gmail.com 48 | 49 | 50 | 51 | 52 | 4.11 53 | 2.16 54 | 3.0.6.RELEASE 55 | 1.7 56 | 1.7 57 | UTF-8 58 | 59 | 60 | 61 | 62 | io.reactivex 63 | rxjava 64 | 1.2.1 65 | true 66 | 67 | 68 | io.reactivex.rxjava2 69 | rxjava 70 | 2.0.8 71 | true 72 | 73 | 74 | io.projectreactor 75 | reactor-core 76 | ${reactor.version} 77 | true 78 | 79 | 80 | io.projectreactor.addons 81 | reactor-test 82 | ${reactor.version} 83 | true 84 | 85 | 86 | junit 87 | junit 88 | ${junit.version} 89 | true 90 | 91 | 92 | org.assertj 93 | assertj-core 94 | 1.6.1 95 | test 96 | 97 | 98 | 99 | 100 | 101 | 102 | org.apache.maven.plugins 103 | maven-surefire-plugin 104 | ${surefire.version} 105 | 106 | 107 | org.apache.maven.surefire 108 | surefire-junit47 109 | ${surefire.version} 110 | 111 | 112 | 113 | none:none 114 | 115 | 116 | 117 | org.apache.maven.plugins 118 | maven-jar-plugin 119 | 2.4 120 | 121 | 122 | 123 | true 124 | true 125 | 126 | 127 | com.github.alexvictoor 128 | 129 | 130 | 131 | 132 | 133 | org.pitest 134 | pitest-maven 135 | 1.1.10 136 | 137 | 138 | rx.marble* 139 | 140 | 141 | rx.marble* 142 | 143 | 144 | XML 145 | HTML 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | release 158 | 159 | 160 | 161 | org.apache.maven.plugins 162 | maven-source-plugin 163 | 2.2.1 164 | 165 | 166 | attach-sources 167 | 168 | jar 169 | 170 | 171 | 172 | 173 | 174 | org.apache.maven.plugins 175 | maven-javadoc-plugin 176 | 2.9.1 177 | 178 | 179 | javadoc 180 | 181 | jar 182 | 183 | 184 | 185 | 186 | 187 | org.apache.maven.plugins 188 | maven-gpg-plugin 189 | 1.5 190 | 191 | 192 | sign-artifacts 193 | verify 194 | 195 | sign 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/alexvictoor/MarbleTest4J.svg?branch=master)](https://travis-ci.org/alexvictoor/MarbleTest4J) 2 | [![Quality Gate](https://sonarqube.com/api/badges/gate?key=com.github.alexvictoor%3Amarbletest4j)](https://sonarqube.com/dashboard/index/com.github.alexvictoor%3Amarbletest4j) 3 | # MarbleTest4j 4 | Java port of RxJS marble tests, works with RxJava, RxJava2 and Reactor3 5 | 6 | MarbleTest4j is a tiny library that allows to write tests using marble diagrams in ASCII form. 7 | This is a Java port of the [marble test features](https://github.com/ReactiveX/rxjs/blob/master/doc/writing-marble-tests.md) of amazing RxJS v5. 8 | The purpose of the library is to help you write as concise an readable tests when dealing with reactive code, 9 | bringing a developer experience as close as possible as the one of RxJS. 10 | Check out this nice [7 minutes introduction on egghead.io](https://egghead.io/lessons/rxjs-introduction-to-rxjs-marble-testing) to get up to speed with RxJS marble testing. 11 | 12 | ## Quickstart 13 | 14 | To get the lib just use add a maven dependency as below: 15 | ```xml 16 | 17 | com.github.alexvictoor 18 | marbletest4j 19 | 1.3 20 | 21 | ``` 22 | 23 | You will need also to import the reactive library used in your project (RxJava, RxJava2 or Reactor3). 24 | When using Reactor3, you also need to import the *reactor-test* module as follow: 25 | ```xml 26 | 27 | io.projectreactor 28 | reactor-core 29 | ${reactor.version} 30 | 31 | 32 | io.projectreactor.addons 33 | reactor-test 34 | ${reactor.version} 35 | 36 | ``` 37 | 38 | ## Usage (the concise way) 39 | A jUnit integration is provided in order to let you write concise tests as you would have done with RxJS. 40 | This integration is made of a jUnit rule **MarbleRule** and a bunch of static methods providing aliases to MarbleScheduler's methods. 41 | **MarbleScheduler** is very similar to RxJS TestScheduler: it is like RxJava's TestScheduler plus marble related methods to create hot & cold 42 | test observables and then perform assertions. Obviously everything is done using marble schemas in ASCII form. 43 | **MarbleRule** keeps in a threadlocal reference a **MarbleScheduler** instance that will be used by static aliases methods. 44 | Though, for most cases you will not need to manipulate directly any scheduler. 45 | *marbletest4j* is compatible with RxJava and RxJava2. Depending on the flavor of RxJava in use in your project, you need 46 | to use a different package prefix when importing *marbletest4j* classes: 47 | 48 | - *rx.marble* & *rx.marble.junit* for RxJava1 projects 49 | - *io.reactivex.marble* & *io.reactivex.marble.junit* for RxJava2 projects 50 | - *reactor* & *reactor.junit* for Reactor3 projects 51 | 52 | Below a complete RxJava2 example: 53 | ``` 54 | import static io.reactivex.marble.junit.MarbleRule.*; 55 | // import static rx.marble.junit.MarbleRule.*; if RxJava1 is used 56 | 57 | @Rule 58 | public MarbleRule marble = new MarbleRule(); 59 | 60 | @Test 61 | public void should_map() { 62 | // given 63 | Observable input = hot("a-b--c---d"); 64 | // when 65 | Observable output = input.map(s -> s.toUpperCase()); 66 | // then 67 | expectObservable(output).toBe( "A-B--C---D"); 68 | } 69 | ``` 70 | In the example above, we create first a hot observable trigering events 'a', 'b', 'c', 'd' (at 0, 20, 50 and 90) 71 | Then we perform some transformations, using rx map operator, and last we perform an assertion on generated Observable. 72 | 73 | If you are into Reactor3, the API is quite similar as you can see in the simple example below: 74 | ``` 75 | import static reactor.MapHelper.of; 76 | import static reactor.junit.MarbleRule.*; 77 | 78 | @Rule 79 | public MarbleRule marbleRule = new MarbleRule(); 80 | 81 | @Test 82 | public void should_concat_with_scan() { 83 | // given 84 | HotFlux flux = hot( "--a--b--c"); 85 | // when 86 | Flux concatFlux = flux.scan((x, y) -> x + " " + y); 87 | // then 88 | expectFlux(concatFlux).toBe("--A--B--C", of("A", "a", "B", "a b", "C", "a b c")); 89 | } 90 | ``` 91 | 92 | 93 | In previous examples, event values were strings, other types are also supported. Below an RxJava2 example but obviously 94 | reactor API is once again quite similar: 95 | ``` 96 | import static io.reactivex.marble.junit.MarbleRule.*; 97 | import static io.reactivex.marble.MapHelper.of; 98 | 99 | // for RxJava1 replace by the following imports 100 | // import static rx.marble.junit.MarbleRule.*; 101 | // import static rx.marble.MapHelper.of; 102 | 103 | @Rule 104 | public MarbleRule marble = new MarbleRule(); 105 | 106 | @Test 107 | public void should_subscribe_during_the_test() { 108 | Map values = of("a", 1, "b", 2); // shortcut from class MapHelper to create a Map 109 | 110 | ColdObservable myObservable 111 | = cold( "---a---b--|", values); 112 | String subscription = "^---------!"; 113 | 114 | expectObservable(myObservable).toBe("---a---b--|", values); 115 | expectSubscriptions(myObservable.getSubscriptions()).toBe(subscription); 116 | } 117 | ``` 118 | As shown above, you can check events timing and values, but also when subscriptions start and end. 119 | Everything in a visual way using marble diagrams in ASCII forms :-) 120 | 121 | ## Usage (the verbose way) 122 | 123 | As said before, the API sticks to the RxJS one. The cornerstone of this API is the **MarbleScheduler** class. Below an example showing how to initiate a scheduler: 124 | ``` 125 | MarbleScheduler scheduler = new MarbleScheduler(); 126 | ``` 127 | This scheduler can then be used to configure source observables: 128 | ``` 129 | Observable sourceEvents = scheduler.createColdObservable("a-b-c-|"); 130 | ``` 131 | Then you can use the **MarbleScheduler.expectObservable()** to verify that everything went as expected during the test. 132 | Below a really simple all-in-one example: 133 | ``` 134 | MarbleScheduler scheduler = new MarbleScheduler(); 135 | Observable sourceEvents = scheduler.createColdObservable("a-b-c-|"); // create an IObservable emiting 3 "next" events 136 | Observable upperEvents = sourceEvents.select(s -> s.toUpperCase()); // transform the events - this is what we often call the SUT ;) 137 | scheduler.expectObservable(upperEvents).toBe("A-B-C-|"); // check that the output events have the timing and values as expected 138 | scheduler.flush(); // let the virtual clock goes... otherwise nothing happens 139 | ``` 140 | **Important:** as shown above, do not forget to **flush** the scheduler at the end of your test case, otherwise no event will be emitted. 141 | 142 | In the above examples, event values are not specified and string streams are produced (i.e. Observable). 143 | As with the RxJS api, you can use a parameter map/hash containing event values: 144 | ``` 145 | Map values = new HashMap<>(); 146 | values.put("a", 1); 147 | values.put("b", 2); 148 | values.put("c", 3); 149 | Observable events = scheduler.CreateHotObservable("a-b-c-|", values); 150 | ``` 151 | In order to reduce the amount of boilerplate code needed to create an iniate the map, you can use guava's ImmutableMap or **MapHelper** static methods: 152 | ``` 153 | import static rx.marble.MapHelper.of; 154 | ... 155 | Observable events 156 | = scheduler.CreateHotObservable("a-b-c-|", of("a", 1, "b", 2, "c", 3)); 157 | ``` 158 | 159 | 160 | ## Marble ASCII syntax 161 | 162 | The syntax remains exactly the same as the one of RxJS. 163 | Each ASCII character represents what happens during a time interval, by default 10 ticks. 164 | **'-'** means that nothing happens 165 | **'a'** or any letter means that an event occurs 166 | **'|'** means the stream end successfully 167 | **'#'** means an error occurs 168 | 169 | So "a-b-|" means: 170 | 171 | - At 0, an event 'a' occurs 172 | - Nothing till 20 where an event 'b' occurs 173 | - Then the stream ends at 40 174 | 175 | If some events occurs simultanously, you can group them using paranthesis. 176 | So "--(abc)--" means events a, b and c occur at time 20. 177 | 178 | For an exhaustive description of the syntax you can checkout 179 | the [official RxJS documentation](https://github.com/ReactiveX/rxjs/blob/master/doc/writing-marble-tests.md) 180 | 181 | ## Advanced features 182 | 183 | For a complete listof supported features you can checkout 184 | the [tests of the MarbleScheduler class](https://github.com/alexvictoor/MarbleTest4J/blob/master/src/test/java/io/reactivex/marble/MarbleSchedulerTest.java). 185 | -------------------------------------------------------------------------------- /src/main/java/org/reactivestreams/MarbleSchedulerState.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | 4 | import java.util.ArrayList; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | /** 10 | * Created by Alexandre Victoor on 23/04/2017. 11 | */ 12 | public class MarbleSchedulerState { 13 | 14 | private final List flushTests = new ArrayList<>(); 15 | private final long frameTimeFactor; 16 | protected final ISchedule scheduler; 17 | private final Class schedulerClass; 18 | 19 | public MarbleSchedulerState(long frameTimeFactor, ISchedule scheduler, Class schedulerClass) { 20 | 21 | this.frameTimeFactor = frameTimeFactor; 22 | this.scheduler = scheduler; 23 | this.schedulerClass = schedulerClass; 24 | } 25 | 26 | 27 | public void flush() { 28 | for (ITestOnFlush test: flushTests) { 29 | if (test.isReady()) { 30 | test.run(); 31 | } 32 | } 33 | } 34 | 35 | public ISetupTest expectPublisher(Publisher publisher, String unsubscriptionMarbles) { 36 | String caller = ExceptionHelper.findCallerInStackTrace(schedulerClass, MarbleSchedulerState.class); 37 | FlushableTest flushTest = new FlushableTest(caller); 38 | final List> actual = new ArrayList<>(); 39 | flushTest.actual = actual; 40 | long unsubscriptionFrame = Long.MAX_VALUE; 41 | 42 | if (unsubscriptionMarbles != null) { 43 | unsubscriptionFrame 44 | = Parser.parseMarblesAsSubscriptions(unsubscriptionMarbles, frameTimeFactor).unsubscribe; 45 | } 46 | final SubscriberForExpect subscriber = new SubscriberForExpect<>(actual, scheduler); 47 | publisher.subscribe(subscriber); 48 | 49 | if (unsubscriptionFrame != Long.MAX_VALUE) { 50 | scheduler.schedule(new Runnable() { 51 | @Override 52 | public void run() { 53 | subscriber.subscription.cancel(); 54 | } 55 | }, unsubscriptionFrame); 56 | } 57 | 58 | flushTests.add(flushTest); 59 | 60 | return new SetupTest(flushTest, frameTimeFactor); 61 | } 62 | 63 | protected List> materializeInnerPublisher(final Publisher publisher, final ISchedule clock) { 64 | final List> messages = new ArrayList<>(); 65 | final long outerFrame = clock.now(); 66 | publisher.subscribe(new Subscriber() { 67 | @Override 68 | public void onSubscribe(Subscription s) { 69 | s.request(Long.MAX_VALUE); 70 | } 71 | 72 | @Override 73 | public void onNext(Object x) { 74 | messages.add(new Recorded<>(clock.now() - outerFrame, Notification.createOnNext(x))); 75 | } 76 | 77 | @Override 78 | public void onError(Throwable throwable) { 79 | messages.add(new Recorded<>(clock.now() - outerFrame, Notification.createOnError(throwable))); 80 | } 81 | 82 | @Override 83 | public void onComplete() { 84 | messages.add(new Recorded<>(clock.now() - outerFrame, Notification.createOnComplete())); 85 | } 86 | }); 87 | 88 | return messages; 89 | } 90 | 91 | // Hack to be able to handle rxjava's observables in flowables 92 | protected Object materializeInnerStreamWhenNeeded(Object value) { 93 | if (value instanceof Publisher) { 94 | return materializeInnerPublisher((Publisher) value, scheduler); 95 | } 96 | return value; 97 | } 98 | 99 | private class SubscriberForExpect implements Subscriber { 100 | 101 | public Subscription subscription; 102 | private final List> actual; 103 | private final ISchedule clock; 104 | 105 | public SubscriberForExpect(List> actual, ISchedule clock) { 106 | this.actual = actual; 107 | this.clock = clock; 108 | } 109 | 110 | @Override 111 | public void onSubscribe(Subscription s) { 112 | subscription = s; 113 | subscription.request(Long.MAX_VALUE); 114 | } 115 | 116 | @Override 117 | public void onNext(T x) { 118 | // Support Publisher-of-Publishers & Publisher-of-Observables 119 | Object value = materializeInnerStreamWhenNeeded(x); 120 | actual.add(new Recorded<>(clock.now(), (Notification)Notification.createOnNext(value))); 121 | } 122 | 123 | @Override 124 | public void onError(Throwable throwable) { 125 | actual.add(new Recorded<>(clock.now(), Notification.createOnError(throwable))); 126 | } 127 | 128 | @Override 129 | public void onComplete() { 130 | actual.add(new Recorded<>(clock.now(), Notification.createOnComplete())); 131 | } 132 | } 133 | 134 | 135 | public ISetupSubscriptionsTest expectSubscriptions(List subscriptions) { 136 | String caller = ExceptionHelper.findCallerInStackTrace(schedulerClass, MarbleSchedulerState.class); 137 | FlushableSubscriptionTest flushTest = new FlushableSubscriptionTest(caller); 138 | flushTest.actual = subscriptions; 139 | flushTests.add(flushTest); 140 | return new SetupSubscriptionsTest(flushTest, frameTimeFactor); 141 | } 142 | 143 | public interface ISchedule { 144 | long now(); 145 | void schedule(Runnable runnable, long time); 146 | } 147 | 148 | class SetupTest extends SetupTestSupport { 149 | private final FlushableTest flushTest; 150 | private final long frameTimeFactor; 151 | 152 | public SetupTest(FlushableTest flushTest, long frameTimeFactor) { 153 | this.flushTest = flushTest; 154 | this.frameTimeFactor = frameTimeFactor; 155 | } 156 | 157 | public void toBe(String marble, Map values, Exception errorValue) { 158 | flushTest.ready = true; 159 | if (values == null) { 160 | flushTest.expected = Parser.parseMarbles(marble, null, errorValue, frameTimeFactor, true); 161 | } else { 162 | flushTest.expected = Parser.parseMarbles(marble, new HashMap<>(values), errorValue, frameTimeFactor, true); 163 | } 164 | } 165 | } 166 | 167 | interface ITestOnFlush { 168 | void run(); 169 | boolean isReady(); 170 | } 171 | 172 | class FlushableTest implements ITestOnFlush { 173 | private final String caller; 174 | private boolean ready; 175 | public List> actual; 176 | public List expected; 177 | 178 | public FlushableTest(String caller) { 179 | this.caller = caller; 180 | } 181 | 182 | public void run() { 183 | 184 | RecordedStreamComparator.StreamComparison result 185 | = new RecordedStreamComparator().compare(actual, expected); 186 | 187 | if (!result.streamEquals) { 188 | throw new ExpectPublisherException(result.toString(), caller); 189 | } 190 | } 191 | 192 | @Override 193 | public boolean isReady() { 194 | return ready; 195 | } 196 | 197 | } 198 | 199 | class SetupSubscriptionsTest implements ISetupSubscriptionsTest { 200 | private final FlushableSubscriptionTest flushTest; 201 | private final long frameTimeFactor; 202 | 203 | public SetupSubscriptionsTest(FlushableSubscriptionTest flushTest, long frameTimeFactor) { 204 | this.flushTest = flushTest; 205 | this.frameTimeFactor = frameTimeFactor; 206 | } 207 | 208 | public void toBe(String... marbles) { 209 | flushTest.ready = true; 210 | flushTest.expected = new ArrayList<>(); 211 | for (String marble : marbles) { 212 | SubscriptionLog subscriptionLog = Parser.parseMarblesAsSubscriptions(marble, frameTimeFactor); 213 | flushTest.expected.add(subscriptionLog); 214 | } 215 | } 216 | } 217 | 218 | class FlushableSubscriptionTest implements ITestOnFlush { 219 | private final String caller; 220 | private boolean ready; 221 | public List actual; 222 | public List expected; 223 | 224 | public FlushableSubscriptionTest(String caller) { 225 | this.caller = caller; 226 | } 227 | 228 | public void run() { 229 | if (actual.size() != expected.size()) { 230 | throw new ExpectSubscriptionsException( 231 | expected.size() + " subscription(s) expected, only " + actual.size() + " observed", 232 | caller 233 | ); 234 | } 235 | for (int i = 0; i < actual.size(); i++) { 236 | if ((actual.get(i) != null && !actual.get(i).equals(expected.get(i))) 237 | || (actual.get(i) == null && expected.get(i) != null)) { 238 | throw new ExpectSubscriptionsException( 239 | "Expected subscription was " + expected.get(i) + ", instead received " + actual.get(i), 240 | caller 241 | ); 242 | } 243 | } 244 | } 245 | 246 | @Override 247 | public boolean isReady() { 248 | return ready; 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/main/java/rx/marble/MarbleScheduler.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | import rx.Notification; 4 | import rx.Observable; 5 | import rx.Subscription; 6 | import rx.functions.Action0; 7 | import rx.functions.Action1; 8 | import rx.schedulers.TestScheduler; 9 | 10 | import java.util.ArrayList; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.concurrent.TimeUnit; 15 | 16 | 17 | public class MarbleScheduler extends TestScheduler { 18 | 19 | private final List flushTests = new ArrayList<>(); 20 | private final long frameTimeFactor; 21 | 22 | public MarbleScheduler(long frameTimeFactor) { 23 | 24 | this.frameTimeFactor = frameTimeFactor; 25 | } 26 | 27 | public MarbleScheduler() { 28 | frameTimeFactor = 10; 29 | } 30 | 31 | public ColdObservable createColdObservable(String marbles, Map values) { 32 | List> notifications = Parser.parseMarbles(marbles, values, null, frameTimeFactor); 33 | return ColdObservable.create(this, notifications); 34 | } 35 | 36 | public ColdObservable createColdObservable(String marbles) { 37 | return createColdObservable(marbles, null); 38 | } 39 | 40 | public HotObservable createHotObservable(String marbles, Map values) { 41 | List> notifications = Parser.parseMarbles(marbles, values, null, frameTimeFactor); 42 | return HotObservable.create(this, notifications); 43 | } 44 | 45 | public HotObservable createHotObservable(String marbles) { 46 | return createHotObservable(marbles, null); 47 | } 48 | 49 | 50 | public long createTime(String marbles) { 51 | int endIndex = marbles.indexOf("|"); 52 | if (endIndex == -1) { 53 | throw new RuntimeException("Marble diagram for time should have a completion marker '|'"); 54 | } 55 | 56 | return endIndex * frameTimeFactor; 57 | } 58 | 59 | public void flush() { 60 | advanceTimeTo(Long.MAX_VALUE, TimeUnit.MILLISECONDS); 61 | for (ITestOnFlush test: flushTests) { 62 | if (test.isReady()) { 63 | test.run(); 64 | } 65 | } 66 | } 67 | 68 | public ISetupTest expectObservable(Observable observable) { 69 | return expectObservable(observable, null); 70 | } 71 | 72 | public ISetupTest expectObservable(Observable observable, String unsubscriptionMarbles) { 73 | String caller = ExceptionHelper.findCallerInStackTrace(getClass()); 74 | FlushableTest flushTest = new FlushableTest(caller); 75 | final List> actual = new ArrayList<>(); 76 | flushTest.actual = actual; 77 | long unsubscriptionFrame = Long.MAX_VALUE; 78 | 79 | if (unsubscriptionMarbles != null) { 80 | unsubscriptionFrame 81 | = Parser.parseMarblesAsSubscriptions(unsubscriptionMarbles, frameTimeFactor).unsubscribe; 82 | } 83 | final Subscription subscription 84 | = observable.subscribe( 85 | new Action1() { 86 | @Override 87 | public void call(T x) { 88 | Object value = x; 89 | // Support Observable-of-Observables 90 | if (value instanceof Observable) { 91 | value = materializeInnerObservable((Observable)value, now()); 92 | } 93 | actual.add(new Recorded<>(now(), (Notification)Notification.createOnNext(value))); 94 | } 95 | }, 96 | new Action1() { 97 | @Override 98 | public void call(Throwable throwable) { 99 | actual.add(new Recorded<>(now(), Notification.createOnError(throwable))); 100 | } 101 | }, new Action0() { 102 | @Override 103 | public void call() { 104 | actual.add(new Recorded<>(now(), Notification.createOnCompleted())); 105 | } 106 | }); 107 | 108 | if (unsubscriptionFrame != Long.MAX_VALUE) { 109 | createWorker().schedule(new Action0() { 110 | @Override 111 | public void call() { 112 | subscription.unsubscribe(); 113 | } 114 | }, unsubscriptionFrame, TimeUnit.MILLISECONDS); 115 | } 116 | 117 | flushTests.add(flushTest); 118 | 119 | return new SetupTest(flushTest, frameTimeFactor); 120 | } 121 | 122 | private List> materializeInnerObservable(final Observable observable, final long outerFrame) { 123 | final List> messages = new ArrayList<>(); 124 | observable.subscribe( 125 | new Action1() { 126 | @Override 127 | public void call(Object x) { 128 | messages.add(new Recorded<>(now() - outerFrame, Notification.createOnNext(x))); 129 | } 130 | }, new Action1() { 131 | @Override 132 | public void call(Throwable throwable) { 133 | messages.add(new Recorded<>(now() - outerFrame, Notification.createOnError(throwable))); 134 | } 135 | }, new Action0() { 136 | @Override 137 | public void call() { 138 | messages.add(new Recorded<>(now() - outerFrame, Notification.createOnCompleted())); 139 | } 140 | }); 141 | 142 | return messages; 143 | } 144 | 145 | public ISetupSubscriptionsTest expectSubscriptions(List subscriptions) { 146 | String caller = ExceptionHelper.findCallerInStackTrace(getClass()); 147 | FlushableSubscriptionTest flushTest = new FlushableSubscriptionTest(caller); 148 | flushTest.actual = subscriptions; 149 | flushTests.add(flushTest); 150 | return new SetupSubscriptionsTest(flushTest, frameTimeFactor); 151 | } 152 | 153 | class SetupTest extends SetupTestSupport { 154 | private final FlushableTest flushTest; 155 | private final long frameTimeFactor; 156 | 157 | public SetupTest(FlushableTest flushTest, long frameTimeFactor) { 158 | this.flushTest = flushTest; 159 | this.frameTimeFactor = frameTimeFactor; 160 | } 161 | 162 | public void toBe(String marble, Map values, Exception errorValue) { 163 | flushTest.ready = true; 164 | if (values == null) { 165 | flushTest.expected = Parser.parseMarbles(marble, null, errorValue, frameTimeFactor, true); 166 | } else { 167 | flushTest.expected = Parser.parseMarbles(marble, new HashMap<>(values), errorValue, frameTimeFactor, true); 168 | } 169 | } 170 | } 171 | 172 | interface ITestOnFlush { 173 | void run(); 174 | boolean isReady(); 175 | } 176 | 177 | class FlushableTest implements ITestOnFlush { 178 | private final String caller; 179 | private boolean ready; 180 | public List> actual; 181 | public List expected; 182 | 183 | public FlushableTest(String caller) { 184 | this.caller = caller; 185 | } 186 | 187 | public void run() { 188 | 189 | RecordedStreamComparator.StreamComparison result 190 | = new RecordedStreamComparator().compare(actual, expected); 191 | 192 | if (!result.streamEquals) { 193 | throw new ExpectObservableException(result.toString(), caller); 194 | } 195 | } 196 | 197 | @Override 198 | public boolean isReady() { 199 | return ready; 200 | } 201 | 202 | } 203 | 204 | class SetupSubscriptionsTest implements ISetupSubscriptionsTest { 205 | private final FlushableSubscriptionTest flushTest; 206 | private final long frameTimeFactor; 207 | 208 | public SetupSubscriptionsTest(FlushableSubscriptionTest flushTest, long frameTimeFactor) { 209 | this.flushTest = flushTest; 210 | this.frameTimeFactor = frameTimeFactor; 211 | } 212 | 213 | public void toBe(String... marbles) { 214 | flushTest.ready = true; 215 | flushTest.expected = new ArrayList<>(); 216 | for (String marble : marbles) { 217 | SubscriptionLog subscriptionLog = Parser.parseMarblesAsSubscriptions(marble, frameTimeFactor); 218 | flushTest.expected.add(subscriptionLog); 219 | } 220 | } 221 | } 222 | 223 | class FlushableSubscriptionTest implements ITestOnFlush { 224 | private final String caller; 225 | private boolean ready; 226 | public List actual; 227 | public List expected; 228 | 229 | public FlushableSubscriptionTest(String caller) { 230 | this.caller = caller; 231 | } 232 | 233 | public void run() { 234 | if (actual.size() != expected.size()) { 235 | throw new ExpectSubscriptionsException( 236 | expected.size() + " subscription(s) expected, only " + actual.size() + " observed", 237 | caller 238 | ); 239 | } 240 | for (int i = 0; i < actual.size(); i++) { 241 | if ((actual.get(i) != null && !actual.get(i).equals(expected.get(i))) 242 | || (actual.get(i) == null && expected.get(i) != null)) { 243 | throw new ExpectSubscriptionsException( 244 | "Expected subscription was " + expected.get(i) + ", instead received " + actual.get(i), 245 | caller 246 | ); 247 | } 248 | } 249 | } 250 | 251 | @Override 252 | public boolean isReady() { 253 | return ready; 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/test/java/org/reactivestreams/MarbleSchedulerTest.java: -------------------------------------------------------------------------------- 1 | package org.reactivestreams; 2 | 3 | import io.reactivex.subscribers.TestSubscriber; 4 | import org.junit.After; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import reactor.ColdFlux; 8 | import reactor.HotFlux; 9 | import reactor.MarbleScheduler; 10 | import reactor.core.publisher.Flux; 11 | 12 | import java.time.Duration; 13 | import java.util.Collections; 14 | import java.util.Map; 15 | import java.util.concurrent.TimeUnit; 16 | import java.util.function.Function; 17 | 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static io.reactivex.marble.MapHelper.of; 20 | 21 | public class MarbleSchedulerTest { 22 | 23 | private MarbleScheduler scheduler; 24 | 25 | @Before 26 | public void setupScheduler() { 27 | scheduler = new MarbleScheduler(); 28 | } 29 | 30 | @After 31 | public void flushScheduler() { 32 | if (scheduler != null) { 33 | scheduler.flush(); 34 | } 35 | } 36 | 37 | @Test 38 | public void should_create_a_cold_observable() { 39 | ColdFlux source = scheduler.createColdFlux("--a---b--|", of("a", "A", "b", "B")); 40 | TestSubscriber observer = new TestSubscriber<>(); 41 | source.subscribe(observer); 42 | scheduler.advanceTimeBy(Duration.ofNanos(Long.MAX_VALUE)); 43 | observer.assertValues("A", "B"); 44 | } 45 | 46 | @Test 47 | public void should_create_a_cold_observable_taking_in_account_frame_time_factor() { 48 | scheduler = new MarbleScheduler(100); 49 | ColdFlux source = scheduler.createColdFlux("--a---b--|", of("a", "A", "b", "B")); 50 | TestSubscriber observer = new TestSubscriber<>(); 51 | source.subscribe(observer); 52 | scheduler.advanceTimeBy(Duration.ofMillis(100)); 53 | 54 | observer.assertNoValues(); 55 | } 56 | 57 | @Test 58 | public void should_create_a_hot_observable() { 59 | HotFlux source = scheduler.createHotFlux("--a---b--|", of("a", "A", "b", "B")); 60 | TestSubscriber observer = new TestSubscriber<>(); 61 | source.subscribe(observer); 62 | scheduler.advanceTimeBy(Duration.ofNanos(Long.MAX_VALUE)); 63 | observer.assertValues("A", "B"); 64 | } 65 | 66 | @Test 67 | public void should_create_a_hot_observable_taking_in_account_frame_time_factor() { 68 | scheduler = new MarbleScheduler(100); 69 | HotFlux source = scheduler.createHotFlux("--a---b--|", of("a", "A", "b", "B")); 70 | TestSubscriber observer = new TestSubscriber<>(); 71 | source.subscribe(observer); 72 | scheduler.advanceTimeBy(Duration.ofMillis(100)); 73 | 74 | observer.assertNoValues(); 75 | } 76 | 77 | @Test 78 | public void should_create_a_hot_observable_sending_events_occurring_after_subscribe() { 79 | final HotFlux source = scheduler.createHotFlux("--a---b--|", of("a", "A", "b", "B")); 80 | final TestSubscriber observer = new TestSubscriber<>(); 81 | scheduler.schedule(new Runnable() { 82 | @Override 83 | public void run() { 84 | source.subscribe(observer); 85 | } 86 | }, 50, TimeUnit.MILLISECONDS); 87 | scheduler.advanceTimeBy(Duration.ofNanos(Long.MAX_VALUE)); 88 | observer.assertValue("B"); 89 | } 90 | 91 | @Test 92 | public void should_parse_a_simple_time_marble_string_to_a_number() { 93 | long time = scheduler.createTime("-----|"); 94 | assertThat(time).isEqualTo(50l); 95 | } 96 | 97 | @Test(expected = RuntimeException.class) 98 | public void should_throw_if_not_given_good_marble_input() { 99 | scheduler.createTime("-a-b-c-#"); 100 | } 101 | 102 | @Test 103 | public void should_expect_empty_observable() { 104 | scheduler.expectFlux(Flux.empty()).toBe("|", Collections.emptyMap()); 105 | } 106 | 107 | @Test 108 | public void should_expect_never_observable() { 109 | scheduler.expectFlux(Flux.never()).toBe("-", Collections.emptyMap()); 110 | scheduler.expectFlux(Flux.never()).toBe("---", Collections.emptyMap()); 111 | } 112 | 113 | @Test 114 | public void should_expect_one_value_observable() { 115 | scheduler.expectFlux(Flux.just("hello")).toBe("(h|)", of("h", (Object)"hello")); 116 | } 117 | 118 | @Test(expected = RuntimeException.class) 119 | public void should_fail_when_event_values_differ() { 120 | MarbleScheduler scheduler = new MarbleScheduler(); 121 | scheduler.expectFlux(Flux.just("hello")).toBe("(h|)", of("h", (Object)"bye")); 122 | scheduler.flush(); 123 | } 124 | 125 | @Test(expected = RuntimeException.class) 126 | public void should_fail_when_event_timing_differs() { 127 | MarbleScheduler scheduler = new MarbleScheduler(); 128 | scheduler.expectFlux(Flux.just("hello")).toBe("--h|", of("h", (Object)"hello")); 129 | scheduler.flush(); 130 | } 131 | 132 | @Test 133 | public void should_compare_streams_with_multiple_events() { 134 | Flux sourceEvents = scheduler.createColdFlux("a-b-c-|"); 135 | Flux upperEvents = sourceEvents.map(new Function() { 136 | @Override 137 | public String apply(String s) { 138 | return s.toUpperCase(); 139 | } 140 | }); 141 | scheduler.expectFlux(upperEvents).toBe("A-B-C-|"); 142 | } 143 | 144 | @Test 145 | public void should_use_unsubscription_diagram() { 146 | Flux source = scheduler.createHotFlux("---^-a-b-|"); 147 | String unsubscribe = "---!"; 148 | String expected = "--a"; 149 | scheduler.expectFlux(source, unsubscribe).toBe(expected); 150 | } 151 | 152 | @Test 153 | public void should_assert_subscriptions_of_a_cold_observable() { 154 | ColdFlux source = scheduler.createColdFlux("---a---b-|"); 155 | String subs = "^--------!"; 156 | scheduler.expectSubscriptions(source.getSubscriptions()).toBe(subs); 157 | source.subscribe(); 158 | } 159 | 160 | @Test 161 | public void should_assert_subscriptions_of_a_hot_observable() { 162 | HotFlux source = scheduler.createHotFlux("---a---b-|"); 163 | String subs = "^--------!"; 164 | scheduler.expectSubscriptions(source.getSubscriptions()).toBe(subs); 165 | source.subscribe(); 166 | } 167 | 168 | @Test 169 | public void should_be_awesome() { 170 | Map values = of("a", 1, "b", 2); 171 | ColdFlux myFlux 172 | = scheduler.createColdFlux( "---a---b--|", values); 173 | String subs = "^---------!"; 174 | scheduler.expectFlux(myFlux).toBe("---a---b--|", values); 175 | scheduler.expectSubscriptions(myFlux.getSubscriptions()).toBe(subs); 176 | } 177 | 178 | @Test 179 | public void should_support_testing_metastreams() 180 | { 181 | ColdFlux x = scheduler.createColdFlux("-a-b|"); 182 | ColdFlux y = scheduler.createColdFlux("-c-d|"); 183 | Flux> myFlux 184 | = scheduler.createHotFlux("---x---y----|", of("x", x, "y", y)); 185 | String expected = "---x---y----|"; 186 | Object expectedx = scheduler.createColdFlux("-a-b|"); 187 | Object expectedy = scheduler.createColdFlux("-c-d|"); 188 | scheduler.expectFlux(myFlux).toBe(expected, of("x", expectedx, "y", expectedy)); 189 | } 190 | 191 | @Test 192 | public void should_demo_metastreams_with_windows() { 193 | String input = "a---b---c---d-|"; 194 | Flux myFlux = scheduler.createColdFlux(input); 195 | 196 | Flux result = myFlux.window(2, 1); 197 | 198 | Object aWindow = scheduler.createColdFlux("a---(b|)"); 199 | Object bWindow = scheduler.createColdFlux( "b---(c|)"); 200 | Object cWindow = scheduler.createColdFlux( "c---(d|)"); 201 | Object dWindow = scheduler.createColdFlux( "d-|"); 202 | 203 | String expected = "a---b---c---d-|"; 204 | scheduler 205 | .expectFlux(result) 206 | .toBe(expected, of("a", aWindow, "b", bWindow, "c", cWindow, "d", dWindow)); 207 | } 208 | 209 | @Test 210 | public void should_indicate_failed_assertion_with_unexpected_observable() { 211 | MarbleScheduler scheduler = new MarbleScheduler(); 212 | scheduler.expectFlux(Flux.just("hello")).toBe("--h|"); 213 | try { 214 | scheduler.flush(); 215 | } catch(ExpectPublisherException ex) { 216 | assertThat(ex.getMessage()).contains("from assertion at " + getClass().getCanonicalName()); 217 | } 218 | } 219 | 220 | @Test 221 | public void should_indicate_events_values_when_assertions_fails() { 222 | MarbleScheduler scheduler = new MarbleScheduler(); 223 | scheduler.expectFlux(Flux.just("hello")).toBe("--h#", of("h", "hola"), new Exception()); 224 | try { 225 | scheduler.flush(); 226 | } catch(ExpectPublisherException ex) { 227 | assertThat(ex.getMessage()).contains("hello"); 228 | assertThat(ex.getMessage()).contains("On Error"); 229 | } 230 | } 231 | 232 | @Test 233 | public void should_indicate_failed_assertion_when_no_expected_subscription() { 234 | MarbleScheduler scheduler = new MarbleScheduler(); 235 | ColdFlux myFlux 236 | = scheduler.createColdFlux( "---a---b--|"); 237 | String subs = "^---------!"; 238 | scheduler.expectSubscriptions(myFlux.getSubscriptions()).toBe(subs); 239 | try { 240 | scheduler.flush(); 241 | } catch(ExpectSubscriptionsException ex) { 242 | assertThat(ex.getMessage()).contains("from assertion at " + getClass().getCanonicalName()); 243 | } 244 | } 245 | 246 | @Test 247 | public void should_indicate_failed_assertion_with_unexpected_subscription() { 248 | MarbleScheduler scheduler = new MarbleScheduler(); 249 | ColdFlux myFlux 250 | = scheduler.createColdFlux( "---a---b--|"); 251 | String subs = "^-----------!"; 252 | scheduler.expectFlux(myFlux).toBe("---a---b--|"); 253 | scheduler.expectSubscriptions(myFlux.getSubscriptions()).toBe(subs); 254 | try { 255 | scheduler.flush(); 256 | } catch(ExpectSubscriptionsException ex) { 257 | assertThat(ex.getMessage()).contains("from assertion at " + getClass().getCanonicalName()); 258 | } 259 | } 260 | 261 | } -------------------------------------------------------------------------------- /src/test/java/rx/marble/MarbleSchedulerTest.java: -------------------------------------------------------------------------------- 1 | package rx.marble; 2 | 3 | 4 | import org.junit.After; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import rx.Observable; 8 | import rx.Subscription; 9 | import rx.functions.Action0; 10 | import rx.functions.Func1; 11 | import rx.observers.TestSubscriber; 12 | import rx.subjects.BehaviorSubject; 13 | 14 | import java.io.PrintWriter; 15 | import java.io.StringWriter; 16 | import java.util.Collections; 17 | import java.util.Map; 18 | import java.util.concurrent.TimeUnit; 19 | 20 | import static org.assertj.core.api.Assertions.assertThat; 21 | import static rx.marble.MapHelper.of; 22 | 23 | public class MarbleSchedulerTest { 24 | 25 | private MarbleScheduler scheduler; 26 | 27 | @Before 28 | public void setupScheduler() { 29 | scheduler = new MarbleScheduler(); 30 | } 31 | 32 | @After 33 | public void flushScheduler() { 34 | if (scheduler != null) { 35 | scheduler.flush(); 36 | } 37 | } 38 | 39 | @Test 40 | public void should_create_a_cold_observable() { 41 | ColdObservable source = scheduler.createColdObservable("--a---b--|", of("a", "A", "b", "B")); 42 | TestSubscriber subscriber = new TestSubscriber<>(); 43 | source.subscribe(subscriber); 44 | scheduler.advanceTimeBy(Long.MAX_VALUE, TimeUnit.MILLISECONDS); 45 | assertThat(subscriber.getOnNextEvents()) 46 | .containsExactly("A", "B"); 47 | } 48 | 49 | @Test 50 | public void should_create_a_cold_observable_taking_in_account_frame_time_factor() { 51 | scheduler = new MarbleScheduler(100); 52 | ColdObservable source = scheduler.createColdObservable("--a---b--|", of("a", "A", "b", "B")); 53 | TestSubscriber subscriber = new TestSubscriber<>(); 54 | source.subscribe(subscriber); 55 | scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); 56 | 57 | assertThat(subscriber.getOnNextEvents()).isEmpty(); 58 | } 59 | 60 | @Test 61 | public void should_create_a_hot_observable() { 62 | HotObservable source = scheduler.createHotObservable("--a---b--|", of("a", "A", "b", "B")); 63 | TestSubscriber subscriber = new TestSubscriber<>(); 64 | source.subscribe(subscriber); 65 | scheduler.advanceTimeBy(Long.MAX_VALUE, TimeUnit.MILLISECONDS); 66 | assertThat(subscriber.getOnNextEvents()) 67 | .containsExactly("A", "B"); 68 | } 69 | 70 | @Test 71 | public void should_create_a_hot_observable_taking_in_account_frame_time_factor() { 72 | scheduler = new MarbleScheduler(100); 73 | HotObservable source = scheduler.createHotObservable("--a---b--|", of("a", "A", "b", "B")); 74 | TestSubscriber subscriber = new TestSubscriber<>(); 75 | source.subscribe(subscriber); 76 | scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); 77 | 78 | assertThat(subscriber.getOnNextEvents()).isEmpty(); 79 | } 80 | 81 | @Test 82 | public void should_create_a_hot_observable_sending_events_occurring_after_subscribe() { 83 | final HotObservable source = scheduler.createHotObservable("--a---b--|", of("a", "A", "b", "B")); 84 | final TestSubscriber subscriber = new TestSubscriber<>(); 85 | scheduler.createWorker().schedule(new Action0() { 86 | @Override 87 | public void call() { 88 | source.subscribe(subscriber); 89 | } 90 | }, 50, TimeUnit.MILLISECONDS); 91 | scheduler.advanceTimeBy(Long.MAX_VALUE, TimeUnit.MILLISECONDS); 92 | assertThat(subscriber.getOnNextEvents()) 93 | .containsExactly("B"); 94 | } 95 | 96 | @Test 97 | public void should_parse_a_simple_time_marble_string_to_a_number() { 98 | long time = scheduler.createTime("-----|"); 99 | assertThat(time).isEqualTo(50l); 100 | } 101 | 102 | @Test(expected = RuntimeException.class) 103 | public void should_throw_if_not_given_good_marble_input() { 104 | scheduler.createTime("-a-b-c-#"); 105 | } 106 | 107 | @Test 108 | public void should_expect_empty_observable() { 109 | scheduler.expectObservable(Observable.empty()).toBe("|", Collections.emptyMap()); 110 | } 111 | 112 | @Test 113 | public void should_expect_never_observable() { 114 | scheduler.expectObservable(Observable.never()).toBe("-", Collections.emptyMap()); 115 | scheduler.expectObservable(Observable.never()).toBe("---", Collections.emptyMap()); 116 | } 117 | 118 | @Test 119 | public void should_expect_one_value_observable() { 120 | scheduler.expectObservable(Observable.just("hello")).toBe("(h|)", of("h", (Object)"hello")); 121 | } 122 | 123 | @Test(expected = RuntimeException.class) 124 | public void should_fail_when_event_values_differ() { 125 | MarbleScheduler scheduler = new MarbleScheduler(); 126 | scheduler.expectObservable(Observable.just("hello")).toBe("(h|)", of("h", (Object)"bye")); 127 | scheduler.flush(); 128 | } 129 | 130 | @Test(expected = RuntimeException.class) 131 | public void should_fail_when_event_timing_differs() { 132 | MarbleScheduler scheduler = new MarbleScheduler(); 133 | scheduler.expectObservable(Observable.just("hello")).toBe("--h|", of("h", (Object)"hello")); 134 | scheduler.flush(); 135 | } 136 | 137 | @Test 138 | public void should_compare_streams_with_multiple_events() { 139 | Observable sourceEvents = scheduler.createColdObservable("a-b-c-|"); 140 | Observable upperEvents = sourceEvents.map(new Func1() { 141 | @Override 142 | public String call(String s) { 143 | return s.toUpperCase(); 144 | } 145 | }); 146 | scheduler.expectObservable(upperEvents).toBe("A-B-C-|"); 147 | } 148 | 149 | @Test 150 | public void should_use_unsubscription_diagram() { 151 | Observable source = scheduler.createHotObservable("---^-a-b-|"); 152 | String unsubscribe = "---!"; 153 | String expected = "--a"; 154 | scheduler.expectObservable(source, unsubscribe).toBe(expected); 155 | } 156 | 157 | @Test 158 | public void should_assert_subscriptions_of_a_cold_observable() { 159 | ColdObservable source = scheduler.createColdObservable("---a---b-|"); 160 | String subs = "^--------!"; 161 | scheduler.expectSubscriptions(source.getSubscriptions()).toBe(subs); 162 | source.subscribe(); 163 | } 164 | 165 | @Test 166 | public void should_be_awesome() { 167 | Map values = of("a", 1, "b", 2); 168 | ColdObservable myObservable 169 | = scheduler.createColdObservable( "---a---b--|", values); 170 | String subs = "^---------!"; 171 | scheduler.expectObservable(myObservable).toBe("---a---b--|", values); 172 | scheduler.expectSubscriptions(myObservable.getSubscriptions()).toBe(subs); 173 | } 174 | 175 | @Test 176 | public void should_support_testing_metastreams() 177 | { 178 | ColdObservable x = scheduler.createColdObservable("-a-b|"); 179 | ColdObservable y = scheduler.createColdObservable("-c-d|"); 180 | Observable> myObservable 181 | = scheduler.createHotObservable("---x---y----|", of("x", x, "y", y)); 182 | String expected = "---x---y----|"; 183 | Object expectedx = scheduler.createColdObservable("-a-b|"); 184 | Object expectedy = scheduler.createColdObservable("-c-d|"); 185 | scheduler.expectObservable(myObservable).toBe(expected, of("x", expectedx, "y", expectedy)); 186 | } 187 | 188 | @Test 189 | public void should_demo_metastreams_with_windows() { 190 | String input = "a---b---c---d-|"; 191 | Observable myObservable = scheduler.createColdObservable(input); 192 | 193 | Observable result = myObservable.window(2, 1); 194 | 195 | Object aWindow = scheduler.createColdObservable("a---(b|)"); 196 | Object bWindow = scheduler.createColdObservable( "b---(c|)"); 197 | Object cWindow = scheduler.createColdObservable( "c---(d|)"); 198 | Object dWindow = scheduler.createColdObservable( "d-|"); 199 | 200 | String expected = "a---b---c---d-|"; 201 | scheduler 202 | .expectObservable(result) 203 | .toBe(expected, of("a", aWindow, "b", bWindow, "c", cWindow, "d", dWindow)); 204 | } 205 | 206 | @Test 207 | public void should_indicate_failed_assertion_with_unexpected_observable() { 208 | MarbleScheduler scheduler = new MarbleScheduler(); 209 | scheduler.expectObservable(Observable.just("hello")).toBe("--h|"); 210 | try { 211 | scheduler.flush(); 212 | } catch(ExpectObservableException ex) { 213 | assertThat(ex.getMessage()).contains("from assertion at " + getClass().getCanonicalName()); 214 | } 215 | } 216 | 217 | @Test 218 | public void should_indicate_events_values_when_assertions_fails() { 219 | MarbleScheduler scheduler = new MarbleScheduler(); 220 | scheduler.expectObservable(Observable.just("hello")).toBe("--h#", of("h", "hola"), new Exception()); 221 | try { 222 | scheduler.flush(); 223 | } catch(ExpectObservableException ex) { 224 | assertThat(ex.getMessage()).contains("hello"); 225 | assertThat(ex.getMessage()).contains("On Error"); 226 | } 227 | } 228 | 229 | @Test 230 | public void should_indicate_failed_assertion_when_no_expected_subscription() { 231 | MarbleScheduler scheduler = new MarbleScheduler(); 232 | ColdObservable myObservable 233 | = scheduler.createColdObservable( "---a---b--|"); 234 | String subs = "^---------!"; 235 | scheduler.expectSubscriptions(myObservable.getSubscriptions()).toBe(subs); 236 | try { 237 | scheduler.flush(); 238 | } catch(ExpectSubscriptionsException ex) { 239 | assertThat(ex.getMessage()).contains("from assertion at " + getClass().getCanonicalName()); 240 | } 241 | } 242 | 243 | @Test 244 | public void should_indicate_failed_assertion_with_unexpected_subscription() { 245 | MarbleScheduler scheduler = new MarbleScheduler(); 246 | ColdObservable myObservable 247 | = scheduler.createColdObservable( "---a---b--|"); 248 | String subs = "^-----------!"; 249 | scheduler.expectObservable(myObservable).toBe("---a---b--|"); 250 | scheduler.expectSubscriptions(myObservable.getSubscriptions()).toBe(subs); 251 | try { 252 | scheduler.flush(); 253 | } catch(ExpectSubscriptionsException ex) { 254 | assertThat(ex.getMessage()).contains("from assertion at " + getClass().getCanonicalName()); 255 | } 256 | } 257 | 258 | } --------------------------------------------------------------------------------