1
0
mirror of https://github.com/square/okhttp.git synced 2026-01-12 10:23:16 +03:00

Call.addEventListener (#9181)

* Call.addEventListener

Use this to install listeners after-the-fact, such as
in Interceptors or other EventListeners.

* apiDump

* Make eventListener updates atomic

* Fix
This commit is contained in:
Jesse Wilson
2025-11-05 13:28:35 -05:00
committed by GitHub
parent c3f052fd94
commit e6b7eeee4b
11 changed files with 274 additions and 93 deletions

View File

@@ -0,0 +1,49 @@
/*
* Copyright (C) 2025 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 okhttp3
/**
* A special [EventListener] for testing the mechanics of event listeners.
*
* Each instance processes a single event on [call], and then adds a successor [EventListenerRelay]
* on the same [call] to process the next event.
*
* By forcing the list of listeners to change after every event, we can detect if buggy code caches
* a stale [EventListener] in a field or local variable.
*/
class EventListenerRelay(
val call: Call,
val eventRecorder: EventRecorder,
) {
private val eventListenerAdapter =
EventListenerAdapter()
.apply {
listeners += ::onEvent
}
val eventListener: EventListener
get() = eventListenerAdapter
var eventCount = 0
private fun onEvent(callEvent: CallEvent) {
if (eventCount++ == 0) {
eventRecorder.logEvent(callEvent)
val next = EventListenerRelay(call, eventRecorder)
call.addEventListener(next.eventListener)
}
}
}

View File

@@ -120,7 +120,7 @@ open class EventRecorder(
}
}
private fun logEvent(e: CallEvent) {
internal fun logEvent(e: CallEvent) {
for (lock in forbiddenLocks) {
assertThat(Thread.holdsLock(lock), lock.toString()).isFalse()
}

View File

@@ -33,6 +33,8 @@ open class FailingCall : Call {
override fun timeout(): Timeout = error("unexpected")
override fun addEventListener(eventListener: EventListener) = error("unexpected")
override fun <T : Any> tag(type: KClass<T>): T? = error("unexpected")
override fun <T> tag(type: Class<out T>): T? = error("unexpected")

View File

@@ -119,6 +119,7 @@ public final class okhttp3/CacheControl$Companion {
}
public abstract interface class okhttp3/Call : java/lang/Cloneable {
public abstract fun addEventListener (Lokhttp3/EventListener;)V
public abstract fun cancel ()V
public abstract fun clone ()Lokhttp3/Call;
public abstract fun enqueue (Lokhttp3/Callback;)V

View File

@@ -119,6 +119,7 @@ public final class okhttp3/CacheControl$Companion {
}
public abstract interface class okhttp3/Call : java/lang/Cloneable {
public abstract fun addEventListener (Lokhttp3/EventListener;)V
public abstract fun cancel ()V
public abstract fun clone ()Lokhttp3/Call;
public abstract fun enqueue (Lokhttp3/Callback;)V

View File

@@ -88,6 +88,19 @@ interface Call : Cloneable {
*/
fun timeout(): Timeout
/**
* Configure this call to publish all future events to [eventListener], in addition to the
* listeners configured by [OkHttpClient.Builder.eventListener] and other calls to this function.
*
* If this call is later [cloned][clone], [eventListener] will not be notified of its events.
*
* There is no mechanism to remove an event listener. Implementations should instead ignore events
* that they are not interested in.
*
* @see EventListener for semantics and restrictions on listener implementations.
*/
fun addEventListener(eventListener: EventListener)
/**
* Returns the tag attached with [type] as a key, or null if no tag is attached with that key.
*
@@ -161,6 +174,9 @@ interface Call : Cloneable {
* copy.tag(MyTag.class, () -> myTag);
* }
* ```
*
* If any event listeners were installed on this call with [addEventListener], they will not be
* installed on this copy.
*/
public override fun clone(): Call

View File

@@ -21,7 +21,6 @@ import java.net.HttpURLConnection.HTTP_GATEWAY_TIMEOUT
import java.net.HttpURLConnection.HTTP_NOT_MODIFIED
import java.util.concurrent.TimeUnit.MILLISECONDS
import okhttp3.Cache
import okhttp3.EventListener
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Protocol
@@ -42,11 +41,11 @@ import okio.buffer
/** Serves requests from the cache and writes responses to the cache. */
class CacheInterceptor(
internal val call: RealCall,
internal val cache: Cache?,
) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val call = chain.call()
val cacheCandidate = cache?.get(chain.request().requestForCache())
val now = System.currentTimeMillis()
@@ -56,7 +55,6 @@ class CacheInterceptor(
val cacheResponse = strategy.cacheResponse
cache?.trackResponse(strategy)
val listener = (call as? RealCall)?.eventListener ?: EventListener.NONE
if (cacheCandidate != null && cacheResponse == null) {
// The cache candidate wasn't applicable. Close it.
@@ -75,7 +73,7 @@ class CacheInterceptor(
.receivedResponseAtMillis(System.currentTimeMillis())
.build()
.also {
listener.satisfactionFailure(call, it)
call.eventListener.satisfactionFailure(call, it)
}
}
@@ -86,14 +84,14 @@ class CacheInterceptor(
.cacheResponse(cacheResponse.stripBody())
.build()
.also {
listener.cacheHit(call, it)
call.eventListener.cacheHit(call, it)
}
}
if (cacheResponse != null) {
listener.cacheConditionalHit(call, cacheResponse)
call.eventListener.cacheConditionalHit(call, cacheResponse)
} else if (cache != null) {
listener.cacheMiss(call)
call.eventListener.cacheMiss(call)
}
var networkResponse: Response? = null
@@ -126,7 +124,7 @@ class CacheInterceptor(
cache!!.trackConditionalCacheHit()
cache.update(cacheResponse, response)
return response.also {
listener.cacheHit(call, it)
call.eventListener.cacheHit(call, it)
}
} else {
cacheResponse.body.closeQuietly()
@@ -149,7 +147,7 @@ class CacheInterceptor(
return cacheWritingResponse(cacheRequest, response).also {
if (cacheResponse != null) {
// This will log a conditional cache miss only.
listener.cacheMiss(call)
call.eventListener.cacheMiss(call)
}
}
}

View File

@@ -38,7 +38,6 @@ import okio.buffer
*/
class Exchange(
internal val call: RealCall,
internal val eventListener: EventListener,
internal val finder: ExchangeFinder,
private val codec: ExchangeCodec,
) {
@@ -59,11 +58,11 @@ class Exchange(
@Throws(IOException::class)
fun writeRequestHeaders(request: Request) {
try {
eventListener.requestHeadersStart(call)
call.eventListener.requestHeadersStart(call)
codec.writeRequestHeaders(request)
eventListener.requestHeadersEnd(call, request)
call.eventListener.requestHeadersEnd(call, request)
} catch (e: IOException) {
eventListener.requestFailed(call, e)
call.eventListener.requestFailed(call, e)
trackFailure(e)
throw e
}
@@ -76,7 +75,7 @@ class Exchange(
): Sink {
this.isDuplex = duplex
val contentLength = request.body!!.contentLength()
eventListener.requestBodyStart(call)
call.eventListener.requestBodyStart(call)
val rawRequestBody = codec.createRequestBody(request, contentLength)
return RequestBodySink(
delegate = rawRequestBody,
@@ -90,7 +89,7 @@ class Exchange(
try {
codec.flushRequest()
} catch (e: IOException) {
eventListener.requestFailed(call, e)
call.eventListener.requestFailed(call, e)
trackFailure(e)
throw e
}
@@ -101,14 +100,14 @@ class Exchange(
try {
codec.finishRequest()
} catch (e: IOException) {
eventListener.requestFailed(call, e)
call.eventListener.requestFailed(call, e)
trackFailure(e)
throw e
}
}
fun responseHeadersStart() {
eventListener.responseHeadersStart(call)
call.eventListener.responseHeadersStart(call)
}
@Throws(IOException::class)
@@ -118,14 +117,14 @@ class Exchange(
result?.initExchange(this)
return result
} catch (e: IOException) {
eventListener.responseFailed(call, e)
call.eventListener.responseFailed(call, e)
trackFailure(e)
throw e
}
}
fun responseHeadersEnd(response: Response) {
eventListener.responseHeadersEnd(call, response)
call.eventListener.responseHeadersEnd(call, response)
}
@Throws(IOException::class)
@@ -142,7 +141,7 @@ class Exchange(
)
return RealResponseBody(contentType, contentLength, source.buffer())
} catch (e: IOException) {
eventListener.responseFailed(call, e)
call.eventListener.responseFailed(call, e)
trackFailure(e)
throw e
}
@@ -217,16 +216,16 @@ class Exchange(
}
if (requestDone) {
if (e != null) {
eventListener.requestFailed(call, e)
call.eventListener.requestFailed(call, e)
} else {
eventListener.requestBodyEnd(call, bytesRead)
call.eventListener.requestBodyEnd(call, bytesRead)
}
}
if (responseDone) {
if (e != null) {
eventListener.responseFailed(call, e)
call.eventListener.responseFailed(call, e)
} else {
eventListener.responseBodyEnd(call, bytesRead)
call.eventListener.responseBodyEnd(call, bytesRead)
}
}
return call.messageDone(
@@ -273,7 +272,7 @@ class Exchange(
try {
if (invokeStartEvent) {
invokeStartEvent = false
eventListener.requestBodyStart(call)
call.eventListener.requestBodyStart(call)
}
super.write(source, byteCount)
this.bytesReceived += byteCount
@@ -347,7 +346,7 @@ class Exchange(
if (invokeStartEvent) {
invokeStartEvent = false
eventListener.responseBodyStart(call)
call.eventListener.responseBodyStart(call)
}
if (read == -1L) {
@@ -390,7 +389,7 @@ class Exchange(
// If the body is closed without reading any bytes send a responseBodyStart() now.
if (e == null && invokeStartEvent) {
invokeStartEvent = false
eventListener.responseBodyStart(call)
call.eventListener.responseBodyStart(call)
}
return bodyComplete(
bytesRead = bytesReceived,

View File

@@ -26,6 +26,7 @@ import java.util.concurrent.TimeUnit.MILLISECONDS
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater
import kotlin.reflect.KClass
import okhttp3.Call
import okhttp3.Callback
@@ -70,7 +71,8 @@ class RealCall(
Lockable {
private val connectionPool: RealConnectionPool = client.connectionPool.delegate
internal val eventListener: EventListener = client.eventListenerFactory.create(this)
@Volatile
internal var eventListener: EventListener = client.eventListenerFactory.create(this)
private val timeout =
object : AsyncTimeout() {
@@ -126,6 +128,13 @@ class RealCall(
override fun timeout(): Timeout = timeout
override fun addEventListener(eventListener: EventListener) {
// Atomically replace the current eventListener with a composite one.
do {
val previous = this.eventListener
} while (!eventListenerUpdater.compareAndSet(this, previous, previous + eventListener))
}
override fun <T : Any> tag(type: KClass<T>): T? = type.java.cast(tags.get()[type])
override fun <T> tag(type: Class<out T>): T? = tag(type.kotlin)
@@ -202,7 +211,7 @@ class RealCall(
interceptors += client.interceptors
interceptors += RetryAndFollowUpInterceptor(client)
interceptors += BridgeInterceptor(client.cookieJar)
interceptors += CacheInterceptor(client.cache)
interceptors += CacheInterceptor(this, client.cache)
interceptors += ConnectInterceptor
if (!forWebSocket) {
interceptors += client.networkInterceptors
@@ -297,7 +306,7 @@ class RealCall(
val exchangeFinder = this.exchangeFinder!!
val connection = exchangeFinder.find()
val codec = connection.newCodec(client, chain)
val result = Exchange(this, eventListener, exchangeFinder, codec)
val result = Exchange(this, exchangeFinder, codec)
this.interceptorScopedExchange = result
this.exchange = result
withLock {
@@ -608,4 +617,13 @@ class RealCall(
*/
val callStackTrace: Any?,
) : WeakReference<RealCall>(referent)
private companion object {
val eventListenerUpdater: AtomicReferenceFieldUpdater<RealCall, EventListener> =
AtomicReferenceFieldUpdater.newUpdater(
RealCall::class.java,
EventListener::class.java,
"eventListener",
)
}
}

View File

@@ -15,6 +15,7 @@
*/
package okhttp3
import app.cash.burst.Burst
import assertk.all
import assertk.assertThat
import assertk.assertions.contains
@@ -25,6 +26,7 @@ import assertk.assertions.isEqualTo
import assertk.assertions.isFalse
import assertk.assertions.isIn
import assertk.assertions.isInstanceOf
import assertk.assertions.isNotEmpty
import assertk.assertions.isNotNull
import assertk.assertions.isNull
import assertk.assertions.isSameAs
@@ -95,6 +97,7 @@ import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.MatcherAssert
import org.junit.Assume.assumeThat
import org.junit.Assume.assumeTrue
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
@@ -105,7 +108,10 @@ import org.junit.jupiter.api.extension.RegisterExtension
@Flaky // STDOUT logging enabled for test
@Timeout(30)
@Tag("Slow")
class EventListenerTest {
@Burst
class EventListenerTest(
val listenerInstalledOn: ListenerInstalledOn = ListenerInstalledOn.Client,
) {
@RegisterExtension
val platform = PlatformRule()
@@ -120,8 +126,11 @@ class EventListenerTest {
private var client =
clientTestRule
.newClientBuilder()
.eventListenerFactory(clientTestRule.wrap(eventRecorder))
.build()
.apply {
if (listenerInstalledOn == ListenerInstalledOn.Client) {
eventListenerFactory(clientTestRule.wrap(eventRecorder))
}
}.build()
private var socksProxy: SocksProxy? = null
private var cache: Cache? = null
@@ -151,7 +160,7 @@ class EventListenerTest {
.build(),
)
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -192,7 +201,7 @@ class EventListenerTest {
)
val ipAddress = InetAddress.getLoopbackAddress().hostAddress
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(
@@ -235,7 +244,7 @@ class EventListenerTest {
.build(),
)
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -296,7 +305,7 @@ class EventListenerTest {
.readTimeout(Duration.ofMillis(250))
.build()
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -345,7 +354,7 @@ class EventListenerTest {
.readTimeout(Duration.ofMillis(250))
.build()
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -383,7 +392,7 @@ class EventListenerTest {
@Test
fun canceledCallEventSequence() {
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -411,7 +420,7 @@ class EventListenerTest {
.build(),
)
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -446,7 +455,7 @@ class EventListenerTest {
.build(),
)
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -462,7 +471,7 @@ class EventListenerTest {
emptyBody: Boolean = false,
) {
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -520,7 +529,7 @@ class EventListenerTest {
server.enqueue(MockResponse())
server.enqueue(MockResponse())
client
.newCall(
.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -529,7 +538,7 @@ class EventListenerTest {
.close()
eventRecorder.removeUpToEvent<CallEnd>()
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -703,7 +712,7 @@ class EventListenerTest {
fun successfulDnsLookup() {
server.enqueue(MockResponse())
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -728,7 +737,7 @@ class EventListenerTest {
// Seed the pool.
val call1 =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -739,7 +748,7 @@ class EventListenerTest {
response1.body.close()
eventRecorder.clearAllEvents()
val call2 =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -772,7 +781,7 @@ class EventListenerTest {
.dns(dns)
.build()
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url("http://fakeurl:" + server.port)
@@ -795,7 +804,7 @@ class EventListenerTest {
.dns(FakeDns())
.build()
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url("http://fakeurl/")
@@ -819,7 +828,7 @@ class EventListenerTest {
.dns(emptyDns)
.build()
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url("http://fakeurl/")
@@ -840,7 +849,7 @@ class EventListenerTest {
fun successfulConnect() {
server.enqueue(MockResponse())
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -855,7 +864,7 @@ class EventListenerTest {
assertThat(connectStart.call).isSameAs(call)
assertThat(connectStart.inetSocketAddress).isEqualTo(expectedAddress)
assertThat(connectStart.proxy).isEqualTo(Proxy.NO_PROXY)
val connectEnd = eventRecorder.removeUpToEvent<ConnectEnd>()
val connectEnd = eventRecorder.removeUpToEvent<CallEvent.ConnectEnd>()
assertThat(connectEnd.call).isSameAs(call)
assertThat(connectEnd.inetSocketAddress).isEqualTo(expectedAddress)
assertThat(connectEnd.protocol).isEqualTo(Protocol.HTTP_1_1)
@@ -871,7 +880,7 @@ class EventListenerTest {
.build(),
)
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -909,7 +918,7 @@ class EventListenerTest {
.dns(DoubleInetAddressDns())
.build()
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -933,7 +942,7 @@ class EventListenerTest {
.proxy(server.proxyAddress)
.build()
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url("http://www.fakeurl")
@@ -969,7 +978,7 @@ class EventListenerTest {
.proxy(proxy)
.build()
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url("http://" + SocksProxy.HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS + ":" + server.port)
@@ -1019,7 +1028,7 @@ class EventListenerTest {
.proxyAuthenticator(RecordingOkAuthenticator("password", "Basic"))
.build()
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1040,7 +1049,7 @@ class EventListenerTest {
enableTlsWithTunnel()
server.enqueue(MockResponse())
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1066,7 +1075,7 @@ class EventListenerTest {
.build(),
)
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1098,7 +1107,7 @@ class EventListenerTest {
.proxy(server.proxyAddress)
.build()
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1130,7 +1139,7 @@ class EventListenerTest {
.dns(DoubleInetAddressDns())
.build()
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1158,7 +1167,7 @@ class EventListenerTest {
// Seed the pool.
val call1 =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1169,7 +1178,7 @@ class EventListenerTest {
response1.body.close()
eventRecorder.clearAllEvents()
val call2 =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1187,7 +1196,7 @@ class EventListenerTest {
fun successfulConnectionFound() {
server.enqueue(MockResponse())
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1217,7 +1226,7 @@ class EventListenerTest {
.build(),
)
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1237,7 +1246,7 @@ class EventListenerTest {
// Seed the pool.
val call1 =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1249,7 +1258,7 @@ class EventListenerTest {
val connectionAcquired1 = eventRecorder.removeUpToEvent<ConnectionAcquired>()
eventRecorder.clearAllEvents()
val call2 =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1281,7 +1290,7 @@ class EventListenerTest {
.build(),
)
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1324,7 +1333,7 @@ class EventListenerTest {
.build(),
)
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1354,7 +1363,7 @@ class EventListenerTest {
.build(),
)
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1393,7 +1402,7 @@ class EventListenerTest {
.build(),
)
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1433,7 +1442,7 @@ class EventListenerTest {
.build(),
)
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1491,7 +1500,7 @@ class EventListenerTest {
)
val request = NonCompletingRequestBody()
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1564,7 +1573,7 @@ class EventListenerTest {
.build(),
)
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1665,7 +1674,7 @@ class EventListenerTest {
.setLevel(HttpLoggingInterceptor.Level.BODY),
).build()
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1709,7 +1718,7 @@ class EventListenerTest {
.build(),
)
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1761,7 +1770,7 @@ class EventListenerTest {
// Warm up the client so the timing part of the test gets a pooled connection.
server.enqueue(MockResponse())
val warmUpCall =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1796,7 +1805,7 @@ class EventListenerTest {
// Create a request body with artificial delays.
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -1868,7 +1877,7 @@ class EventListenerTest {
.build(),
)
server.enqueue(MockResponse())
val call = client.newCall(Request.Builder().url(server.url("/")).build())
val call = client.newCallWithListener(Request.Builder().url(server.url("/")).build())
call.execute()
assertThat(eventRecorder.recordedEventTypes()).containsExactly(
CallStart::class,
@@ -1913,7 +1922,7 @@ class EventListenerTest {
.build(),
)
otherServer.enqueue(MockResponse())
val call = client.newCall(Request.Builder().url(server.url("/")).build())
val call = client.newCallWithListener(Request.Builder().url(server.url("/")).build())
call.execute()
assertThat(eventRecorder.recordedEventTypes()).containsExactly(
CallStart::class,
@@ -1970,7 +1979,7 @@ class EventListenerTest {
chain.proceed(chain.request())
},
).build()
val call = client.newCall(Request.Builder().url(server.url("/")).build())
val call = client.newCallWithListener(Request.Builder().url(server.url("/")).build())
val response = call.execute()
assertThat(response.body.string()).isEqualTo("b")
assertThat(eventRecorder.recordedEventTypes()).containsExactly(
@@ -2020,7 +2029,7 @@ class EventListenerTest {
.build()
},
).build()
val call = client.newCall(Request.Builder().url(server.url("/")).build())
val call = client.newCallWithListener(Request.Builder().url(server.url("/")).build())
val response = call.execute()
assertThat(response.body.string()).isEqualTo("a")
assertThat(eventRecorder.recordedEventTypes())
@@ -2043,7 +2052,7 @@ class EventListenerTest {
.header("Expect", "100-continue")
.post("abc".toRequestBody("text/plain".toMediaType()))
.build()
val call = client.newCall(request)
val call = client.newCallWithListener(request)
call.execute()
assertThat(eventRecorder.recordedEventTypes()).containsExactly(
CallStart::class,
@@ -2085,7 +2094,7 @@ class EventListenerTest {
.header("Expect", "100-continue")
.post("abc".toRequestBody("text/plain".toMediaType()))
.build()
val call = client.newCall(request)
val call = client.newCallWithListener(request)
call
.execute()
.use { response -> assertThat(response.body.string()).isEqualTo("") }
@@ -2105,7 +2114,7 @@ class EventListenerTest {
.build(),
)
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -2154,7 +2163,7 @@ class EventListenerTest {
.build(),
)
var call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -2164,7 +2173,7 @@ class EventListenerTest {
assertThat(response.code).isEqualTo(200)
response.close()
eventRecorder.clearAllEvents()
call = call.clone()
call = call.cloneWithListener()
response = call.execute()
assertThat(response.code).isEqualTo(200)
assertThat(response.body.string()).isEqualTo("abc")
@@ -2205,7 +2214,7 @@ class EventListenerTest {
.build(),
)
var call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -2215,7 +2224,7 @@ class EventListenerTest {
assertThat(response.code).isEqualTo(200)
response.close()
eventRecorder.clearAllEvents()
call = call.clone()
call = call.cloneWithListener()
response = call.execute()
assertThat(response.code).isEqualTo(200)
assertThat(response.body.string()).isEqualTo("abd")
@@ -2241,7 +2250,7 @@ class EventListenerTest {
fun satisfactionFailure() {
enableCache()
val call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -2274,7 +2283,7 @@ class EventListenerTest {
.build(),
)
var call =
client.newCall(
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
@@ -2285,7 +2294,7 @@ class EventListenerTest {
assertThat(response.body.string()).isEqualTo("abc")
response.close()
eventRecorder.clearAllEvents()
call = call.clone()
call = call.cloneWithListener()
response = call.execute()
assertThat(response.code).isEqualTo(200)
assertThat(response.body.string()).isEqualTo("abc")
@@ -2374,6 +2383,60 @@ class EventListenerTest {
}
}
/** Listeners added with [Call.addEventListener] don't exist on clones of that call. */
@Test
fun clonedCallDoesNotHaveAddedEventListeners() {
assumeTrue(listenerInstalledOn != ListenerInstalledOn.Client)
server.enqueue(
MockResponse
.Builder()
.body("abc")
.build(),
)
val call =
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
.build(),
)
val clone = call.clone() // Not cloneWithListener.
val response = clone.execute()
assertThat(response.code).isEqualTo(200)
assertThat(response.body.string()).isEqualTo("abc")
response.body.close()
assertThat(eventRecorder.recordedEventTypes()).isEmpty()
}
/** Listeners added with [OkHttpClient.Builder.eventListener] are also added to clones. */
@Test
fun clonedCallHasClientEventListeners() {
assumeTrue(listenerInstalledOn == ListenerInstalledOn.Client)
server.enqueue(
MockResponse
.Builder()
.body("abc")
.build(),
)
val call =
client.newCallWithListener(
Request
.Builder()
.url(server.url("/"))
.build(),
)
val clone = call.clone() // Not cloneWithListener().
val response = clone.execute()
assertThat(response.code).isEqualTo(200)
assertThat(response.body.string()).isEqualTo("abc")
response.body.close()
assertThat(eventRecorder.recordedEventTypes()).isNotEmpty()
}
/**
* Returns a map with sample values for each possible parameter of an [EventListener] function
* parameter.
@@ -2433,6 +2496,38 @@ class EventListenerTest {
return Cache(cacheDir, (1024 * 1024).toLong())
}
private fun OkHttpClient.newCallWithListener(request: Request): Call =
newCall(request)
.apply {
addEventRecorder(eventRecorder)
}
private fun Call.cloneWithListener(): Call =
clone()
.apply {
addEventRecorder(eventRecorder)
}
private fun Call.addEventRecorder(eventRecorder: EventRecorder) {
when (listenerInstalledOn) {
ListenerInstalledOn.Call -> {
addEventListener(eventRecorder.eventListener)
}
ListenerInstalledOn.Relay -> {
addEventListener(EventListenerRelay(this, eventRecorder).eventListener)
}
ListenerInstalledOn.Client -> {} // listener is added elsewhere.
}
}
enum class ListenerInstalledOn {
Client,
Call,
Relay,
}
companion object {
val anyResponse = CoreMatchers.any(Response::class.java)
}

View File

@@ -202,6 +202,8 @@ class KotlinSourceModernTest {
override fun timeout(): Timeout = TODO()
override fun addEventListener(eventListener: EventListener) = TODO()
override fun <T : Any> tag(type: KClass<T>): T? = TODO()
override fun <T> tag(type: Class<out T>): T? = TODO()