提交 4c3c17b2 编写于 作者: J Jake Wharton

Add coroutine support to BehaviorDelegate

(cherry picked from commit 1ec98c23)
上级 62ab32f3
......@@ -24,6 +24,11 @@
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>junit</groupId>
......@@ -35,10 +40,35 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
......
......@@ -18,12 +18,17 @@ package retrofit2.mock;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.concurrent.ExecutorService;
import javax.annotation.Nullable;
import kotlin.coroutines.Continuation;
import retrofit2.Call;
import retrofit2.CallAdapter;
import retrofit2.KotlinExtensions;
import retrofit2.Response;
import retrofit2.Retrofit;
/**
......@@ -58,12 +63,103 @@ public final class BehaviorDelegate<T> {
new InvocationHandler() {
@Override
public T invoke(Object proxy, Method method, Object[] args) throws Throwable {
Type returnType = method.getGenericReturnType();
ServiceMethodAdapterInfo adapterInfo = parseServiceMethodAdapterInfo(method);
Annotation[] methodAnnotations = method.getAnnotations();
CallAdapter<R, T> callAdapter =
(CallAdapter<R, T>) retrofit.callAdapter(returnType, methodAnnotations);
return callAdapter.adapt(behaviorCall);
(CallAdapter<R, T>) retrofit.callAdapter(adapterInfo.responseType,
methodAnnotations);
T adapted = callAdapter.adapt(behaviorCall);
if (!adapterInfo.isSuspend) {
return adapted;
}
Call<Object> adaptedCall = (Call<Object>) adapted;
Continuation<Object> continuation = (Continuation<Object>) args[args.length - 1];
try {
return adapterInfo.wantsResponse
? (T) KotlinExtensions.awaitResponse(adaptedCall, continuation)
: (T) KotlinExtensions.await(adaptedCall, continuation);
} catch (Exception e) {
return (T) KotlinExtensions.suspendAndThrow(e, continuation);
}
}
});
}
/**
* Computes the adapter type of the method for lookup via {@link Retrofit#callAdapter} as well as
* information on whether the method is a {@code suspend fun}.
* <p>
* In the case of a Kotlin {@code suspend fun}, the last parameter type is a {@code Continuation}
* whose parameter carries the actual response type. In this case, we return {@code Call<T>} where
* {@code T} is the body type.
*/
private static ServiceMethodAdapterInfo parseServiceMethodAdapterInfo(Method method) {
Type[] genericParameterTypes = method.getGenericParameterTypes();
if (genericParameterTypes.length != 0) {
Type lastParameterType = genericParameterTypes[genericParameterTypes.length - 1];
if (lastParameterType instanceof ParameterizedType) {
ParameterizedType parameterizedLastParameterType = (ParameterizedType) lastParameterType;
try {
if (parameterizedLastParameterType.getRawType() == Continuation.class) {
Type resultType = parameterizedLastParameterType.getActualTypeArguments()[0];
if (resultType instanceof WildcardType) {
resultType = ((WildcardType) resultType).getLowerBounds()[0];
}
if (resultType instanceof ParameterizedType) {
ParameterizedType parameterizedResultType = (ParameterizedType) resultType;
if (parameterizedResultType.getRawType() == Response.class) {
Type bodyType = parameterizedResultType.getActualTypeArguments()[0];
Type callType = new CallParameterizedTypeImpl(bodyType);
return new ServiceMethodAdapterInfo(true, true, callType);
}
}
Type callType = new CallParameterizedTypeImpl(resultType);
return new ServiceMethodAdapterInfo(true, false, callType);
}
} catch (NoClassDefFoundError ignored) {
// Not using coroutines.
}
}
}
return new ServiceMethodAdapterInfo(false, false, method.getGenericReturnType());
}
static final class CallParameterizedTypeImpl implements ParameterizedType {
private final Type bodyType;
CallParameterizedTypeImpl(Type bodyType) {
this.bodyType = bodyType;
}
@Override public Type[] getActualTypeArguments() {
return new Type[] { bodyType };
}
@Override public Type getRawType() {
return Call.class;
}
@Override public @Nullable Type getOwnerType() {
return null;
}
}
static class ServiceMethodAdapterInfo {
final boolean isSuspend;
/**
* Whether the suspend function return type was {@code Response<T>}.
* Only meaningful if {@link #isSuspend} is true.
*/
final boolean wantsResponse;
final Type responseType;
ServiceMethodAdapterInfo(boolean isSuspend, boolean wantsResponse, Type responseType) {
this.isSuspend = isSuspend;
this.wantsResponse = wantsResponse;
this.responseType = responseType;
}
}
}
/*
* Copyright (C) 2019 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package retrofit2.mock
inline fun <reified T> MockRetrofit.create(): BehaviorDelegate<T> = create(T::class.java)
/*
* Copyright (C) 2019 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package retrofit2.mock
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import retrofit2.Response
import retrofit2.Retrofit
import java.io.IOException
import java.util.Random
import java.util.concurrent.TimeUnit.MILLISECONDS
import java.util.concurrent.TimeUnit.NANOSECONDS
class BehaviorDelegateKotlinTest {
internal interface DoWorkService {
suspend fun body(): String
suspend fun failure(): String
suspend fun response(): Response<String>
suspend fun responseWildcard(): Response<out String>
}
private val mockFailure = IOException("Timeout!")
private val behavior = NetworkBehavior.create(Random(2847))
private lateinit var service: DoWorkService
@Before fun before() {
val retrofit = Retrofit.Builder()
.baseUrl("http://example.com")
.build()
val mockRetrofit = MockRetrofit.Builder(retrofit)
.networkBehavior(behavior)
.build()
val delegate = mockRetrofit.create<DoWorkService>()
service = object : DoWorkService {
override suspend fun body(): String {
return delegate.returning(Calls.response("Response!")).body()
}
override suspend fun failure(): String {
val failure = Calls.failure<String>(mockFailure)
return delegate.returning(failure).failure()
}
override suspend fun response(): Response<String> {
val response = Calls.response("Response!")
return delegate.returning(response).response()
}
override suspend fun responseWildcard() = response()
}
}
@Test fun body() {
behavior.setDelay(100, MILLISECONDS)
behavior.setVariancePercent(0)
behavior.setFailurePercent(0)
val startNanos = System.nanoTime()
val result = runBlocking { service.body() }
val tookMs = NANOSECONDS.toMillis(System.nanoTime() - startNanos)
assertThat(tookMs).isGreaterThanOrEqualTo(100)
assertThat(result).isEqualTo("Response!")
}
@Test fun bodyFailure() {
behavior.setDelay(100, MILLISECONDS)
behavior.setVariancePercent(0)
behavior.setFailurePercent(100)
val startNanos = System.nanoTime()
val exception = runBlocking {
try {
throw AssertionError(service.body())
} catch (e: Exception) {
e
}
}
val tookMs = NANOSECONDS.toMillis(System.nanoTime() - startNanos)
assertThat(tookMs).isGreaterThanOrEqualTo(100)
assertThat(exception).isSameAs(behavior.failureException())
}
@Test fun failure() {
behavior.setDelay(100, MILLISECONDS)
behavior.setVariancePercent(0)
behavior.setFailurePercent(0)
val startNanos = System.nanoTime()
val exception = runBlocking {
try {
throw AssertionError(service.failure())
} catch (e: Exception) {
e
}
}
val tookMs = NANOSECONDS.toMillis(System.nanoTime() - startNanos)
assertThat(tookMs).isGreaterThanOrEqualTo(100)
// Coroutines break referential transparency on exceptions so compare type and message.
assertThat(exception).isExactlyInstanceOf(mockFailure.javaClass)
assertThat(exception).hasMessage(mockFailure.message)
}
@Test fun response() {
behavior.setDelay(100, MILLISECONDS)
behavior.setVariancePercent(0)
behavior.setFailurePercent(0)
val startNanos = System.nanoTime()
val result = runBlocking { service.response() }
val tookMs = NANOSECONDS.toMillis(System.nanoTime() - startNanos)
assertThat(tookMs).isGreaterThanOrEqualTo(100)
assertThat(result.body()).isEqualTo("Response!")
}
@Test fun responseFailure() {
behavior.setDelay(100, MILLISECONDS)
behavior.setVariancePercent(0)
behavior.setFailurePercent(100)
val startNanos = System.nanoTime()
val exception = runBlocking {
try {
throw AssertionError(service.response())
} catch (e: Exception) {
e
}
}
val tookMs = NANOSECONDS.toMillis(System.nanoTime() - startNanos)
assertThat(tookMs).isGreaterThanOrEqualTo(100)
assertThat(exception).isSameAs(behavior.failureException())
}
@Test fun responseWildcard() {
behavior.setDelay(100, MILLISECONDS)
behavior.setVariancePercent(0)
behavior.setFailurePercent(0)
val startNanos = System.nanoTime()
val result = runBlocking { service.responseWildcard() }
val tookMs = NANOSECONDS.toMillis(System.nanoTime() - startNanos)
assertThat(tookMs).isGreaterThanOrEqualTo(100)
assertThat(result.body()).isEqualTo("Response!")
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册