// Copyright 2013 Square, Inc.
package retrofit;
import retrofit.client.Request;
import retrofit.client.Response;
import rx.Observable;
import rx.Observer;
import rx.Subscription;
import rx.concurrency.Schedulers;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import static retrofit.RestAdapter.LogLevel;
/**
* Wraps mocks implementations of API interfaces so that they exhibit the delay and error
* characteristics of a real network.
* <p/>
* Because APIs are defined as interfaces, versions of the API that use mock data can be created by
* simply implementing the API interface on a class. These mock implementations execute
* synchronously which is a large deviation from the behavior of those backed by an API call over
* the network. By wrapping the mock instances using this class, the interface will still use mock
* data but exhibit the delays and errors that a real network would face.
* <p/>
* Create an API interface and a mock implementation of it.
* <pre>
* public interface UserService {
* @GET("/user/{id}")
* User getUser(@PathParam("id") String userId);
* }
* public class MockUserService implements UserService {
* @Override public User getUser(String userId) {
* return new User("Jake");
* }
* }
* </pre>
* Given a {@link RestAdapter} an instance of this class can be created by calling {@link #from}.
* <pre>
* MockRestAdapter mockRestAdapter = MockRestAdapter.from(restAdapter);
* </pre>
* Instances of this class should be used as a singleton so that the behavior of every mock service
* is consistent.
* <p/>
* Rather than using the {@code MockUserService} directly, pass it through
* {@link #create(Class, Object) the create method}.
* <pre>
* UserService service = mockRestAdapter.create(UserService.class, new MockUserService());
* </pre>
* The returned {@code UserService} instance will now behave like it is happening over the network
* while allowing the mock implementation to be written synchronously.
* <p/>
* HTTP errors can be simulated in your mock services by throwing an instance of
* {@link MockHttpException}. This should be done for both synchronous and asynchronous methods.
* Do not call the {@link Callback#failure(RetrofitError) failure()} method of a callback.
*/
public final class MockRestAdapter
{
private static final int DEFAULT_DELAY_MS = 2000; // Network calls will take 2 seconds.
private static final int DEFAULT_VARIANCE_PCT = 40; // Network delay varies by ±40%.
private static final int DEFAULT_ERROR_PCT = 3; // 3% of network calls will fail.
private static final int ERROR_DELAY_FACTOR = 3; // Network errors will be scaled by this value.
/**
* Create a new {@link MockRestAdapter} which will act as a factory for mock services. Some of
* the configuration of the supplied {@link RestAdapter} will be used generating mock behavior.
*/
public static MockRestAdapter from(RestAdapter restAdapter)
{
return new MockRestAdapter(restAdapter);
}
/**
* A listener invoked when the network behavior values for a {@link MockRestAdapter} change.
*/
public interface ValueChangeListener
{
void onMockValuesChanged(long delayMs, int variancePct, int errorPct);
ValueChangeListener EMPTY = new ValueChangeListener()
{
@Override
public void onMockValuesChanged(long delayMs, int variancePct, int errorPct)
{
}
};
}
private final RestAdapter restAdapter;
private final MockRxSupport mockRxSupport;
final Random random = new Random();
private ValueChangeListener listener = ValueChangeListener.EMPTY;
private int delayMs = DEFAULT_DELAY_MS;
private int variancePct = DEFAULT_VARIANCE_PCT;
private int errorPct = DEFAULT_ERROR_PCT;
private MockRestAdapter(RestAdapter restAdapter)
{
this.restAdapter = restAdapter;
if (Platform.HAS_RX_JAVA)
{
mockRxSupport = new MockRxSupport(restAdapter);
}
else
{
mockRxSupport = null;
}
}
/**
* Set a listener to be notified when any mock value changes.
*/
public void setValueChangeListener(ValueChangeListener listener)
{
this.listener = listener;
}
private void notifyValueChangeListener()
{
listener.onMockValuesChanged(delayMs, variancePct, errorPct);
}
/**
* Set the network round trip delay, in milliseconds.
*/
public void setDelay(long delayMs)
{
if (delayMs < 0)
{
throw new IllegalArgumentException("Delay must be positive value.");
}
if (delayMs > Integer.MAX_VALUE)
{
throw new IllegalArgumentException("Delay value too large. Max: " + Integer.MAX_VALUE);
}
if (this.delayMs != delayMs)
{
this.delayMs = (int) delayMs;
notifyValueChangeListener();
}
}
/**
* The network round trip delay, in milliseconds
*/
public long getDelay()
{
return delayMs;
}
/**
* Set the plus-or-minus variance percentage of the network round trip delay.
*/
public void setVariancePercentage(int variancePct)
{
if (variancePct < 0 || variancePct > 100)
{
throw new IllegalArgumentException("Variance percentage must be between 0 and 100.");
}
if (this.variancePct != variancePct)
{
this.variancePct = variancePct;
notifyValueChangeListener();
}
}
/**
* The plus-or-minus variance percentage of the network round trip delay.
*/
public int getVariancePercentage()
{
return variancePct;
}
/**
* Set the percentage of calls to {@link #calculateIsFailure()} that return {@code true}.
*/
public void setErrorPercentage(int errorPct)
{
if (errorPct < 0 || errorPct > 100)
{
throw new IllegalArgumentException("Error percentage must be between 0 and 100.");
}
if (this.errorPct != errorPct)
{
this.errorPct = errorPct;
notifyValueChangeListener();
}
}
/**
* The percentage of calls to {@link #calculateIsFailure()} that return {@code true}.
*/
public int getErrorPercentage()
{
return errorPct;
}
/**
* Randomly determine whether this call should result in a network failure.
* <p/>
* This method is exposed for implementing other, non-Retrofit services which exhibit similar
* network behavior. Retrofit services automatically will exhibit network behavior when wrapped
* using {@link #create(Class, Object)}.
*/
public boolean calculateIsFailure()
{
int randomValue = random.nextInt(100) + 1;
return randomValue <= errorPct;
}
/**
* Get the delay (in milliseconds) that should be used for triggering a network error.
* <p/>
* Because we are triggering an error, use a random delay between 0 and three times the normal
* network delay to simulate a flaky connection failing anywhere from quickly to slowly.
* <p/>
* This method is exposed for implementing other, non-Retrofit services which exhibit similar
* network behavior. Retrofit services automatically will exhibit network behavior when wrapped
* using {@link #create(Class, Object)}.
*/
public int calculateDelayForError()
{
return random.nextInt(delayMs * ERROR_DELAY_FACTOR);
}
/**
* Get the delay (in milliseconds) that should be used for delaying a network call response.
* <p/>
* This method is exposed for implementing other, non-Retrofit services which exhibit similar
* network behavior. Retrofit services automatically will exhibit network behavior when wrapped
* using {@link #create(Class, Object)}.
*/
public int calculateDelayForCall()
{
float errorPercent = variancePct / 100f; // e.g., 20 / 100f == 0.2f
float lowerBound = 1f - errorPercent; // 0.2f --> 0.8f
float upperBound = 1f + errorPercent; // 0.2f --> 1.2f
float bound = upperBound - lowerBound; // 1.2f - 0.8f == 0.4f
float delayPercent = (random.nextFloat() * bound) + lowerBound; // 0.8 + (rnd * 0.4)
return (int) (delayMs * delayPercent);
}
/**
* Wrap the supplied mock implementation of a service so that it exhibits the delay and error
* characteristics of a real network.
*
* @see #setDelay(long)
* @see #setVariancePercentage(int)
* @see #setErrorPercentage(int)
*/
@SuppressWarnings("unchecked")
public <T> T create(Class<T> service, T mockService)
{
Utils.validateServiceClass(service);
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service},
new MockHandler(mockService, restAdapter.getMethodInfoCache(service)));
}
private class MockHandler implements InvocationHandler
{
private final Object mockService;
private final Map<Method, RestMethodInfo> methodInfoCache;
public MockHandler(Object mockService, Map<Method, RestMethodInfo> methodInfoCache)
{
this.mockService = mockService;
this.methodInfoCache = methodInfoCache;
}
@Override
public Object invoke(Object proxy, Method method, final Object[] args)
throws Throwable
{
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class)
{
return method.invoke(this, args);
}
// Load or create the details cache for the current method.
final RestMethodInfo methodInfo = RestAdapter.getMethodInfo(methodInfoCache, method);
if (methodInfo.isSynchronous)
{
return invokeSync(methodInfo, restAdapter.requestInterceptor, args);
}
if (restAdapter.httpExecutor == null || restAdapter.callbackExecutor == null)
{
throw new IllegalStateException("Asynchronous invocation requires calling setExecutors.");
}
// Apply the interceptor synchronously, recording the interception so we can replay it later.
// This way we still defer argument serialization to the background thread.
final RequestInterceptorTape interceptorTape = new RequestInterceptorTape();
restAdapter.requestInterceptor.intercept(interceptorTape);
if (methodInfo.isObservable)
{
return mockRxSupport.createMockObservable(this, methodInfo, interceptorTape, args);
}
restAdapter.httpExecutor.execute(new Runnable()
{
@Override
public void run()
{
invokeAsync(methodInfo, interceptorTape, args);
}
});
return null; // Asynchronous methods should have return type of void.
}
private Request buildRequest(RestMethodInfo methodInfo, RequestInterceptor interceptor,
Object[] args) throws Throwable
{
methodInfo.init();
// Begin building a normal request.
RequestBuilder requestBuilder = new RequestBuilder(restAdapter.converter, methodInfo);
requestBuilder.setApiUrl(restAdapter.server.getUrl());
requestBuilder.setArguments(args);
// Run it through the interceptor.
interceptor.intercept(requestBuilder);
Request request = requestBuilder.build();
if (restAdapter.logLevel.log())
{
request = restAdapter.logAndReplaceRequest("MOCK", request);
}
return request;
}
private Object invokeSync(RestMethodInfo methodInfo, RequestInterceptor interceptor,
Object[] args) throws Throwable
{
Request request = buildRequest(methodInfo, interceptor, args);
String url = request.getUrl();
if (calculateIsFailure())
{
sleep(calculateDelayForError());
IOException exception = new IOException("Mock network error!");
if (restAdapter.logLevel.log())
{
restAdapter.logException(exception, url);
}
throw RetrofitError.networkError(url, exception);
}
LogLevel logLevel = restAdapter.logLevel;
RestAdapter.Log log = restAdapter.log;
int callDelay = calculateDelayForCall();
long beforeNanos = System.nanoTime();
try
{
Object returnValue = methodInfo.method.invoke(mockService, args);
// Sleep for whatever amount of time is left to satisfy the network delay, if any.
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - beforeNanos);
sleep(callDelay - tookMs);
if (logLevel.log())
{
log.log(String.format("<--- MOCK 200 %s (%sms)", url, callDelay));
if (logLevel.ordinal() >= LogLevel.FULL.ordinal())
{
log.log(returnValue + ""); // Hack to convert toString while supporting null.
log.log("<--- END MOCK");
}
}
return returnValue;
}
catch (InvocationTargetException e)
{
Throwable innerEx = e.getCause();
if (!(innerEx instanceof MockHttpException))
{
throw innerEx;
}
MockHttpException httpEx = (MockHttpException) innerEx;
Response response = httpEx.toResponse(restAdapter.converter);
// Sleep for whatever amount of time is left to satisfy the network delay, if any.
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - beforeNanos);
sleep(callDelay - tookMs);
if (logLevel.log())
{
log.log(String.format("<---- MOCK %s %s (%sms)", httpEx.code, url, callDelay));
if (logLevel.ordinal() >= LogLevel.FULL.ordinal())
{
log.log(httpEx.responseBody + ""); // Hack to convert toString while supporting null.
log.log("<--- END MOCK");
}
}
throw new MockHttpRetrofitError(url, response, httpEx.responseBody);
}
}
private void invokeAsync(RestMethodInfo methodInfo, RequestInterceptor interceptorTape,
Object[] args)
{
Request request;
try
{
request = buildRequest(methodInfo, interceptorTape, args);
}
catch (final Throwable throwable)
{
restAdapter.callbackExecutor.execute(new Runnable()
{
@Override
public void run()
{
throw new RuntimeException(throwable);
}
});
return;
}
LogLevel logLevel = restAdapter.logLevel;
RestAdapter.Log log = restAdapter.log;
long beforeNanos = System.nanoTime();
int callDelay = calculateDelayForCall();
final String url = request.getUrl();
final Callback realCallback = (Callback) args[args.length - 1];
if (calculateIsFailure())
{
sleep(calculateDelayForError());
final IOException exception = new IOException("Mock network error!");
if (restAdapter.logLevel.log())
{
restAdapter.logException(exception, url);
}
restAdapter.callbackExecutor.execute(new Runnable()
{
@Override
public void run()
{
realCallback.failure(RetrofitError.networkError(url, exception));
}
});
return;
}
// Replace the normal callback with one which supports the delay.
Object[] newArgs = new Object[args.length];
System.arraycopy(args, 0, newArgs, 0, args.length - 1);
newArgs[args.length - 1] = new DelayingCallback(beforeNanos, callDelay, url, realCallback);
try
{
methodInfo.method.invoke(mockService, newArgs);
}
catch (Throwable throwable)
{
final Throwable innerEx = throwable.getCause();
if (!(innerEx instanceof MockHttpException))
{
restAdapter.callbackExecutor.execute(new Runnable()
{
@Override
public void run()
{
if (innerEx instanceof RuntimeException)
{
throw (RuntimeException) innerEx;
}
throw new RuntimeException(innerEx);
}
});
return;
}
MockHttpException httpEx = (MockHttpException) innerEx;
Response response = httpEx.toResponse(restAdapter.converter);
// Sleep for whatever amount of time is left to satisfy the network delay, if any.
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - beforeNanos);
sleep(callDelay - tookMs);
if (logLevel.log())
{
log.log(String.format("<---- MOCK %s %s (%sms)", httpEx.code, url, callDelay));
if (logLevel.ordinal() >= LogLevel.FULL.ordinal())
{
log.log(httpEx.responseBody + ""); // Hack to convert toString while supporting null.
log.log("<--- END MOCK");
}
}
final RetrofitError error = new MockHttpRetrofitError(url, response, httpEx.responseBody);
restAdapter.callbackExecutor.execute(new Runnable()
{
@Override
public void run()
{
realCallback.failure(error);
}
});
}
}
private class DelayingCallback implements Callback
{
private final long beforeNanos;
private final String url;
private final Callback realCallback;
private final long callDelay;
private DelayingCallback(long beforeNanos, int callDelay, String url, Callback realCallback)
{
this.beforeNanos = beforeNanos;
this.callDelay = callDelay;
this.url = url;
this.realCallback = realCallback;
}
@Override
public void success(final Object object, final Response response)
{
LogLevel logLevel = restAdapter.logLevel;
RestAdapter.Log log = restAdapter.log;
// Sleep for whatever amount of time is left to satisfy the network delay, if any.
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - beforeNanos);
sleep(callDelay - tookMs);
if (logLevel.log())
{
log.log(String.format("<--- MOCK 200 %s (%sms)", url, callDelay));
if (logLevel.ordinal() >= LogLevel.FULL.ordinal())
{
log.log(object + ""); // Hack to convert toString while supporting null.
log.log("<--- END MOCK");
}
}
restAdapter.callbackExecutor.execute(new Runnable()
{
@SuppressWarnings("unchecked") //
@Override
public void run()
{
realCallback.success(object, response);
}
});
}
@Override
public void failure(final RetrofitError error)
{
restAdapter.callbackExecutor.execute(new Runnable()
{
@Override
public void run()
{
throw new IllegalStateException(
"Calling failure directly is not supported. Throw MockHttpException instead.");
}
});
}
}
}
/**
* Waits a given number of milliseconds (of uptimeMillis) before returning. Similar to {@link
* Thread#sleep(long)}, but does not throw {@link InterruptedException}; {@link
* Thread#interrupt()} events are deferred until the next interruptible operation. Does not
* return until at least the specified number of milliseconds has elapsed.
*
* @param ms to sleep before returning, in milliseconds of uptime.
*/
private static void sleep(long ms)
{
// This implementation is modified from Android's SystemClock#sleep.
long start = uptimeMillis();
long duration = ms;
boolean interrupted = false;
while (duration > 0)
{
try
{
Thread.sleep(duration);
}
catch (InterruptedException e)
{
interrupted = true;
}
duration = start + ms - uptimeMillis();
}
if (interrupted)
{
// Important: we don't want to quietly eat an interrupt() event,
// so we make sure to re-interrupt the thread so that the next
// call to Thread.sleep() or Object.wait() will be interrupted.
Thread.currentThread().interrupt();
}
}
private static long uptimeMillis()
{
return System.nanoTime() / 1000000L;
}
/**
* Indirection to avoid VerifyError if RxJava isn't present.
*/
private static class MockRxSupport
{
private final RestAdapter restAdapter;
MockRxSupport(RestAdapter restAdapter)
{
this.restAdapter = restAdapter;
}
Observable createMockObservable(final MockHandler mockHandler, final RestMethodInfo methodInfo,
final RequestInterceptor interceptor, final Object[] args)
{
return Observable.create(new Observable.OnSubscribeFunc<Object>()
{
@Override
public Subscription onSubscribe(Observer<? super Object> observer)
{
try
{
Observable observable =
(Observable) mockHandler.invokeSync(methodInfo, interceptor, args);
//noinspection unchecked
return observable.subscribe(observer);
}
catch (Throwable throwable)
{
return Observable.error(throwable).subscribe(observer);
}
}
}).subscribeOn(Schedulers.executor(restAdapter.httpExecutor));
}
}
}