diff --git a/okhttp-testing-support/src/main/kotlin/okhttp3/CallEvent.kt b/okhttp-testing-support/src/main/kotlin/okhttp3/CallEvent.kt index bc91e6f22..35eaddcd4 100644 --- a/okhttp-testing-support/src/main/kotlin/okhttp3/CallEvent.kt +++ b/okhttp-testing-support/src/main/kotlin/okhttp3/CallEvent.kt @@ -30,8 +30,8 @@ sealed class CallEvent { val name: String get() = javaClass.simpleName - /** Returns the open event that this close event closes, or null if this is not a close event. */ - open fun closes(timestampNs: Long): CallEvent? = null + /** Returns if the event closes this event, or null if this is no open event. */ + open fun closes(event: CallEvent): Boolean? = null data class ProxySelectStart( override val timestampNs: Long, @@ -44,7 +44,10 @@ sealed class CallEvent { override val call: Call, val url: HttpUrl, val proxies: List? - ) : CallEvent() + ) : CallEvent() { + override fun closes(event: CallEvent): Boolean = + event is ProxySelectStart && call == event.call && url == event.url + } data class DnsStart( override val timestampNs: Long, @@ -58,7 +61,8 @@ sealed class CallEvent { val domainName: String, val inetAddressList: List ) : CallEvent() { - override fun closes(timestampNs: Long): CallEvent = DnsStart(timestampNs, call, domainName) + override fun closes(event: CallEvent): Boolean = + event is DnsStart && call == event.call && domainName == event.domainName } data class ConnectStart( @@ -75,8 +79,8 @@ sealed class CallEvent { val proxy: Proxy?, val protocol: Protocol? ) : CallEvent() { - override fun closes(timestampNs: Long): CallEvent = - ConnectStart(timestampNs, call, inetSocketAddress, proxy) + override fun closes(event: CallEvent): Boolean = + event is ConnectStart && call == event.call && inetSocketAddress == event.inetSocketAddress && proxy == event.proxy } data class ConnectFailed( @@ -87,8 +91,8 @@ sealed class CallEvent { val protocol: Protocol?, val ioe: IOException ) : CallEvent() { - override fun closes(timestampNs: Long): CallEvent = - ConnectStart(timestampNs, call, inetSocketAddress, proxy) + override fun closes(event: CallEvent): Boolean = + event is ConnectStart && call == event.call && inetSocketAddress == event.inetSocketAddress && proxy == event.proxy } data class SecureConnectStart( @@ -101,7 +105,8 @@ sealed class CallEvent { override val call: Call, val handshake: Handshake? ) : CallEvent() { - override fun closes(timestampNs: Long): CallEvent = SecureConnectStart(timestampNs, call) + override fun closes(event: CallEvent): Boolean = + event is SecureConnectStart && call == event.call } data class ConnectionAcquired( @@ -115,7 +120,8 @@ sealed class CallEvent { override val call: Call, val connection: Connection ) : CallEvent() { - override fun closes(timestampNs: Long): CallEvent = ConnectionAcquired(timestampNs, call, connection) + override fun closes(event: CallEvent): Boolean = + event is ConnectionAcquired && call == event.call && connection == event.connection } data class CallStart( @@ -127,14 +133,18 @@ sealed class CallEvent { override val timestampNs: Long, override val call: Call ) : CallEvent() { - override fun closes(timestampNs: Long): CallEvent = CallStart(timestampNs, call) + override fun closes(event: CallEvent): Boolean = + event is CallStart && call == event.call } data class CallFailed( override val timestampNs: Long, override val call: Call, val ioe: IOException - ) : CallEvent() + ) : CallEvent() { + override fun closes(event: CallEvent): Boolean = + event is CallStart && call == event.call + } data class Canceled( override val timestampNs: Long, @@ -151,7 +161,8 @@ sealed class CallEvent { override val call: Call, val headerLength: Long ) : CallEvent() { - override fun closes(timestampNs: Long): CallEvent = RequestHeadersStart(timestampNs, call) + override fun closes(event: CallEvent): Boolean = + event is RequestHeadersStart && call == event.call } data class RequestBodyStart( @@ -164,14 +175,19 @@ sealed class CallEvent { override val call: Call, val bytesWritten: Long ) : CallEvent() { - override fun closes(timestampNs: Long): CallEvent = RequestBodyStart(timestampNs, call) + override fun closes(event: CallEvent): Boolean = + event is RequestBodyStart && call == event.call } data class RequestFailed( override val timestampNs: Long, override val call: Call, val ioe: IOException - ) : CallEvent() + ) : CallEvent() { + + override fun closes(event: CallEvent): Boolean = + event is RequestHeadersStart && call == event.call + } data class ResponseHeadersStart( override val timestampNs: Long, @@ -183,7 +199,8 @@ sealed class CallEvent { override val call: Call, val headerLength: Long ) : CallEvent() { - override fun closes(timestampNs: Long): CallEvent = RequestHeadersStart(timestampNs, call) + override fun closes(event: CallEvent): Boolean = + event is ResponseHeadersStart && call == event.call } data class ResponseBodyStart( @@ -196,7 +213,8 @@ sealed class CallEvent { override val call: Call, val bytesRead: Long ) : CallEvent() { - override fun closes(timestampNs: Long): CallEvent = ResponseBodyStart(timestampNs, call) + override fun closes(event: CallEvent): Boolean = + event is ResponseBodyStart && call == event.call } data class ResponseFailed( diff --git a/okhttp-testing-support/src/main/kotlin/okhttp3/ConnectionEvent.kt b/okhttp-testing-support/src/main/kotlin/okhttp3/ConnectionEvent.kt index c322ae940..07827a702 100644 --- a/okhttp-testing-support/src/main/kotlin/okhttp3/ConnectionEvent.kt +++ b/okhttp-testing-support/src/main/kotlin/okhttp3/ConnectionEvent.kt @@ -16,16 +16,17 @@ package okhttp3 import java.io.IOException -import java.net.InetAddress -import java.net.InetSocketAddress -import java.net.Proxy import okhttp3.internal.SuppressSignatureCheck /** Data classes that correspond to each of the methods of [ConnectionListener]. */ @SuppressSignatureCheck sealed class ConnectionEvent { abstract val timestampNs: Long - open val connection: Connection? = null + open val connection: Connection? + get() = null + + /** Returns if the event closes this event, or null if this is no open event. */ + open fun closes(event: ConnectionEvent): Boolean? = null val name: String get() = javaClass.simpleName @@ -41,12 +42,20 @@ sealed class ConnectionEvent { val route: Route, val call: Call, val exception: IOException - ) : ConnectionEvent() + ) : ConnectionEvent() { + override fun closes(event: ConnectionEvent): Boolean = + event is ConnectStart && call == event.call && route == event.route + } data class ConnectEnd( override val timestampNs: Long, override val connection: Connection, - ) : ConnectionEvent() + val route: Route, + val call: Call, + ) : ConnectionEvent() { + override fun closes(event: ConnectionEvent): Boolean = + event is ConnectStart && call == event.call && route == event.route + } data class ConnectionClosed( override val timestampNs: Long, @@ -63,7 +72,11 @@ sealed class ConnectionEvent { override val timestampNs: Long, override val connection: Connection, val call: Call - ) : ConnectionEvent() + ) : ConnectionEvent() { + + override fun closes(event: ConnectionEvent): Boolean = + event is ConnectionAcquired && connection == event.connection && call == event.call + } data class NoNewExchanges( override val timestampNs: Long, diff --git a/okhttp-testing-support/src/main/kotlin/okhttp3/OkHttpClientTestRule.kt b/okhttp-testing-support/src/main/kotlin/okhttp3/OkHttpClientTestRule.kt index 8aa978df2..39da28dcf 100644 --- a/okhttp-testing-support/src/main/kotlin/okhttp3/OkHttpClientTestRule.kt +++ b/okhttp-testing-support/src/main/kotlin/okhttp3/OkHttpClientTestRule.kt @@ -173,7 +173,9 @@ class OkHttpClientTestRule : BeforeEachCallback, AfterEachCallback { // a test timeout failure. val waitTime = (entryTime + 1_000_000_000L - System.nanoTime()) if (!queue.idleLatch().await(waitTime, TimeUnit.NANOSECONDS)) { - TaskRunner.INSTANCE.cancelAll() + synchronized (TaskRunner.INSTANCE) { + TaskRunner.INSTANCE.cancelAll() + } fail("Queue still active after 1000 ms") } } diff --git a/okhttp-testing-support/src/main/kotlin/okhttp3/RecordingConnectionListener.kt b/okhttp-testing-support/src/main/kotlin/okhttp3/RecordingConnectionListener.kt index ca91440b6..d4b39adaf 100644 --- a/okhttp-testing-support/src/main/kotlin/okhttp3/RecordingConnectionListener.kt +++ b/okhttp-testing-support/src/main/kotlin/okhttp3/RecordingConnectionListener.kt @@ -19,12 +19,20 @@ import java.util.Deque import java.util.concurrent.ConcurrentLinkedDeque import java.util.concurrent.TimeUnit import okhttp3.ConnectionEvent.NoNewExchanges +import okhttp3.internal.connection.RealCall import okhttp3.internal.connection.RealConnection import okio.IOException import org.assertj.core.api.Assertions.assertThat import org.assertj.core.data.Offset +import org.junit.jupiter.api.Assertions -open class RecordingConnectionListener : ConnectionListener() { +open class RecordingConnectionListener( + /** + * An override to ignore the normal order that is enforced. + * EventListeners added by Interceptors will not see all events. + */ + private val enforceOrder: Boolean = true +) : ConnectionListener() { val eventSequence: Deque = ConcurrentLinkedDeque() private val forbiddenLocks = mutableSetOf() @@ -41,7 +49,7 @@ open class RecordingConnectionListener : ConnectionListener() { * Removes recorded events up to (and including) an event is found whose class equals [eventClass] * and returns it. */ - fun removeUpToEvent(eventClass: Class): T { + fun removeUpToEvent(eventClass: Class): T { val fullEventSequence = eventSequence.toList() try { while (true) { @@ -106,15 +114,34 @@ open class RecordingConnectionListener : ConnectionListener() { .isFalse() } + if (enforceOrder) { + checkForStartEvent(e) + } + eventSequence.offer(e) } + private fun checkForStartEvent(e: ConnectionEvent) { + if (eventSequence.isEmpty()) { + assertThat(e).isInstanceOf(ConnectionEvent.ConnectStart::class.java) + } else { + eventSequence.forEach loop@ { + when (e.closes(it)) { + null -> return // no open event + true -> return // found open event + false -> return@loop // this is not the open event so continue + } + } + Assertions.fail("event $e without matching start event") + } + } + override fun connectStart(route: Route, call: Call) = logEvent(ConnectionEvent.ConnectStart(System.nanoTime(), route, call)) override fun connectFailed(route: Route, call: Call, failure: IOException) = logEvent(ConnectionEvent.ConnectFailed(System.nanoTime(), route, call, failure)) - override fun connectEnd(connection: Connection) { - logEvent(ConnectionEvent.ConnectEnd(System.nanoTime(), connection)) + override fun connectEnd(connection: Connection, route: Route, call: RealCall) { + logEvent(ConnectionEvent.ConnectEnd(System.nanoTime(), connection, route, call)) } override fun connectionClosed(connection: Connection) = logEvent(ConnectionEvent.ConnectionClosed(System.nanoTime(), connection)) diff --git a/okhttp-testing-support/src/main/kotlin/okhttp3/RecordingEventListener.kt b/okhttp-testing-support/src/main/kotlin/okhttp3/RecordingEventListener.kt index 63f2a44eb..f16e0e1c6 100644 --- a/okhttp-testing-support/src/main/kotlin/okhttp3/RecordingEventListener.kt +++ b/okhttp-testing-support/src/main/kotlin/okhttp3/RecordingEventListener.kt @@ -54,8 +54,16 @@ import okhttp3.CallEvent.SecureConnectStart import org.assertj.core.api.Assertions.assertThat import org.assertj.core.data.Offset import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.fail -open class RecordingEventListener : EventListener() { +open class RecordingEventListener( + /** + * An override to ignore the normal order that is enforced. + * EventListeners added by Interceptors will not see all events. + */ + private val enforceOrder: Boolean = true +) : EventListener() { val eventSequence: Deque = ConcurrentLinkedDeque() private val forbiddenLocks = mutableListOf() @@ -72,7 +80,7 @@ open class RecordingEventListener : EventListener() { * Removes recorded events up to (and including) an event is found whose class equals [eventClass] * and returns it. */ - fun removeUpToEvent(eventClass: Class): T { + fun removeUpToEvent(eventClass: Class): T { val fullEventSequence = eventSequence.toList() try { while (true) { @@ -132,14 +140,28 @@ open class RecordingEventListener : EventListener() { .isFalse() } - val startEvent = e.closes(-1L) - if (startEvent != null) { - assertTrue(eventSequence.any { it == e.closes(it.timestampNs) }) + if (enforceOrder) { + checkForStartEvent(e) } eventSequence.offer(e) } + private fun checkForStartEvent(e: CallEvent) { + if (eventSequence.isEmpty()) { + assertThat(e).isInstanceOfAny(CallStart::class.java, Canceled::class.java) + } else { + eventSequence.forEach loop@ { + when (e.closes(it)) { + null -> return // no open event + true -> return // found open event + false -> return@loop // this is not the open event so continue + } + } + fail("event $e without matching start event") + } + } + override fun proxySelectStart( call: Call, url: HttpUrl diff --git a/okhttp/api/okhttp.api b/okhttp/api/okhttp.api index 020723cb0..f5d00d98b 100644 --- a/okhttp/api/okhttp.api +++ b/okhttp/api/okhttp.api @@ -362,7 +362,7 @@ public abstract interface class okhttp3/Connection { public abstract class okhttp3/ConnectionListener { public static final field Companion Lokhttp3/ConnectionListener$Companion; public fun ()V - public fun connectEnd (Lokhttp3/Connection;)V + public fun connectEnd (Lokhttp3/Connection;Lokhttp3/Route;Lokhttp3/internal/connection/RealCall;)V public fun connectFailed (Lokhttp3/Route;Lokhttp3/Call;Ljava/io/IOException;)V public fun connectStart (Lokhttp3/Route;Lokhttp3/Call;)V public fun connectionAcquired (Lokhttp3/Connection;Lokhttp3/Call;)V diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/ConnectionListener.kt b/okhttp/src/jvmMain/kotlin/okhttp3/ConnectionListener.kt index e3a334d8c..7f95c902b 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/ConnectionListener.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/ConnectionListener.kt @@ -15,6 +15,7 @@ */ package okhttp3 +import okhttp3.internal.connection.RealCall import okio.IOException /** @@ -38,7 +39,7 @@ abstract class ConnectionListener { /** * Invoked as soon as a connection is successfully established. */ - open fun connectEnd(connection: Connection) {} + open fun connectEnd(connection: Connection, route: Route, call: RealCall) {} /** * Invoked when a connection is released as no longer required. diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/internal/connection/ConnectPlan.kt b/okhttp/src/jvmMain/kotlin/okhttp3/internal/connection/ConnectPlan.kt index 5274e29c6..8941b0241 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/internal/connection/ConnectPlan.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/internal/connection/ConnectPlan.kt @@ -480,7 +480,7 @@ class ConnectPlan( call.client.routeDatabase.connected(route) val connection = this.connection!! - connectionListener.connectEnd(connection) + connectionListener.connectEnd(connection, route, call) // If we raced another call connecting to this host, coalesce the connections. This makes for // 3 different lookups in the connection pool! diff --git a/okhttp/src/jvmTest/java/okhttp3/ConnectionListenerTest.kt b/okhttp/src/jvmTest/java/okhttp3/ConnectionListenerTest.kt index 38cece206..6e17629a6 100644 --- a/okhttp/src/jvmTest/java/okhttp3/ConnectionListenerTest.kt +++ b/okhttp/src/jvmTest/java/okhttp3/ConnectionListenerTest.kt @@ -135,20 +135,22 @@ open class ConnectionListenerTest { @Test @Throws(IOException::class) fun secondCallEventSequence() { - enableTlsWithTunnel() - server!!.protocols = Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1) + enableTls() + server!!.protocols = listOf(Protocol.HTTP_2, Protocol.HTTP_1_1) server!!.enqueue(MockResponse()) server!!.enqueue(MockResponse()) - client.newCall(Request.Builder() - .url(server!!.url("/")) - .build()).execute().close() - listener.removeUpToEvent(ConnectionEvent.ConnectionReleased::class.java) - val call = client.newCall(Request.Builder() - .url(server!!.url("/")) - .build()) - val response = call.execute() - response.close() + + client.newCall(Request(server!!.url("/"))) + .execute().close() + + client.newCall(Request(server!!.url("/"))) + .execute().close() + assertThat(listener.recordedEventTypes()).containsExactly( + "ConnectStart", + "ConnectEnd", + "ConnectionAcquired", + "ConnectionReleased", "ConnectionAcquired", "ConnectionReleased" ) @@ -157,7 +159,7 @@ open class ConnectionListenerTest { @Test @Throws(IOException::class) fun successfulEmptyH2CallEventSequence() { - enableTlsWithTunnel() + enableTls() server!!.protocols = Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1) server!!.enqueue(MockResponse()) assertSuccessfulEventOrder() @@ -208,7 +210,7 @@ open class ConnectionListenerTest { @Test @Throws(UnknownHostException::class) fun failedConnect() { - enableTlsWithTunnel() + enableTls() server!!.enqueue(MockResponse(socketPolicy = SocketPolicy.FAIL_HANDSHAKE)) val call = client.newCall(Request.Builder() .url(server!!.url("/")) @@ -232,7 +234,7 @@ open class ConnectionListenerTest { @Test @Throws(IOException::class) fun multipleConnectsForSingleCall() { - enableTlsWithTunnel() + enableTls() server!!.enqueue(MockResponse(socketPolicy = SocketPolicy.FAIL_HANDSHAKE)) server!!.enqueue(MockResponse()) client = client.newBuilder() @@ -277,7 +279,7 @@ open class ConnectionListenerTest { assertThat(event.connection.route().proxy).isEqualTo(proxy) } - private fun enableTlsWithTunnel() { + private fun enableTls() { client = client.newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager)