From 4c01b38d086c4a7ca4ee23861f133cf7e9c5b01d Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Wed, 22 Jul 2026 12:37:05 -0400 Subject: [PATCH 1/2] Add a tester just for the DnsCallStateMachine A very nice perk of extracting it out so it could be reused for Android, is that it's also a lot easier to test. --- .../src/main/kotlin/okhttp3/FakeDns.kt | 35 ++- .../internal/dns/DnsCallStateMachineTest.kt | 206 +++++++++++++ .../internal/dns/DnsCallStateMachineTester.kt | 270 ++++++++++++++++++ 3 files changed, 497 insertions(+), 14 deletions(-) create mode 100644 okhttp/src/commonTest/kotlin/okhttp3/internal/dns/DnsCallStateMachineTest.kt create mode 100644 okhttp/src/commonTest/kotlin/okhttp3/internal/dns/DnsCallStateMachineTester.kt diff --git a/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt b/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt index 7e778d071493..b632f84608e0 100644 --- a/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt +++ b/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt @@ -195,20 +195,7 @@ class FakeDns( } } - // QR = 1 (Response) - // OPCODE = 0 (standard query) - // RD = 1 (Recursion Desired) - // RA = 1 (Recursion Available) - // RCODE = 0 (success) - // QR OPCODE AA TC RD RA Z RCODE - val flags = 0b1___0000__0__0__1__1_000__0000 - - return DnsMessage( - id = request.id, - flags = flags, - questions = request.questions, - answers = answers, - ) + return dnsResponse(request, answers) } private fun ResourceRecord.matches(question: Question): Boolean { @@ -336,3 +323,23 @@ class FakeDns( ) : Request } } + +fun dnsResponse( + request: DnsMessage, + answers: List +): DnsMessage { + // QR = 1 (Response) + // OPCODE = 0 (standard query) + // RD = 1 (Recursion Desired) + // RA = 1 (Recursion Available) + // RCODE = 0 (success) + // QR OPCODE AA TC RD RA Z RCODE + val flags = 0b1___0000__0__0__1__1_000__0000 + + return DnsMessage( + id = request.id, + flags = flags, + questions = request.questions, + answers = answers, + ) +} diff --git a/okhttp/src/commonTest/kotlin/okhttp3/internal/dns/DnsCallStateMachineTest.kt b/okhttp/src/commonTest/kotlin/okhttp3/internal/dns/DnsCallStateMachineTest.kt new file mode 100644 index 000000000000..77a7d791340f --- /dev/null +++ b/okhttp/src/commonTest/kotlin/okhttp3/internal/dns/DnsCallStateMachineTest.kt @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * 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. + */ +@file:OptIn(OkHttpInternalApi::class) + +package okhttp3.internal.dns + +import java.io.IOException +import java.net.InetAddress +import kotlin.test.Test +import okhttp3.Dns +import okhttp3.Protocol +import okhttp3.internal.OkHttpInternalApi + +class DnsCallStateMachineTest { + @Test + fun `happy path`() = testDnsCallStateMachine( + request = Dns.Request(hostname = "lysine.dev"), + ) { + enqueue() + + val query0 = takeQuery("lysine.dev", TYPE_HTTPS) + val query1 = takeQuery("lysine.dev", TYPE_AAAA) + val query2 = takeQuery("lysine.dev", TYPE_A) + + respondIpAddresses( + query = query1.query, + addresses = listOf(InetAddress.getByName("1:2::3:4")), + ) + takeOnRecordsIpAddresses( + addresses = listOf(InetAddress.getByName("1:2::3:4")) + ) + + respondIpAddresses( + query = query2.query, + addresses = listOf(InetAddress.getByName("10.20.30.40")), + ) + takeOnRecordsIpAddresses( + addresses = listOf(InetAddress.getByName("10.20.30.40")) + ) + + respondServiceMetadata( + query = query0.query, + alpnIds = listOf("h2"), + ) + takeOnRecordsServiceMetadata( + last = true, + alpnIds = listOf(Protocol.HTTP_2) + ) + } + + @Test + fun `failure returned last`() = testDnsCallStateMachine( + request = Dns.Request(hostname = "lysine.dev"), + ) { + enqueue() + + val query0 = takeQuery("lysine.dev", TYPE_HTTPS) + val query1 = takeQuery("lysine.dev", TYPE_AAAA) + val query2 = takeQuery("lysine.dev", TYPE_A) + + respondFailure( + query = query1.query, + e = IOException("boom!"), + ) + + respondIpAddresses( + query = query2.query, + addresses = listOf(InetAddress.getByName("10.20.30.40")), + ) + takeOnRecordsIpAddresses( + addresses = listOf(InetAddress.getByName("10.20.30.40")) + ) + + respondServiceMetadata( + query = query0.query, + alpnIds = listOf("h2"), + ) + takeOnRecordsServiceMetadata( + alpnIds = listOf(Protocol.HTTP_2) + ) + + takeOnFailure("boom!") + } + + /** + * Confirm that the state machine calls doesn't call any [Dns.Callback] methods until the previous + * call to a [Dns.Callback] method has returned. + * + * Usually this will be a concurrency problem, but we can exercise it just as well by making a + * re-entrant call on a single thread. + */ + @Test + fun `calls to onRecords are serialized`() = + testDnsCallStateMachine(request = Dns.Request(hostname = "lysine.dev")) { + enqueue() + + val query0 = takeQuery("lysine.dev", TYPE_HTTPS) + val query1 = takeQuery("lysine.dev", TYPE_AAAA) + val query2 = takeQuery("lysine.dev", TYPE_A) + + onNextEvent = { + respondIpAddresses( + query = query2.query, + addresses = listOf(InetAddress.getByName("10.20.30.40")), + ) + respondIpAddresses( + query = query1.query, + addresses = listOf(InetAddress.getByName("1:2::3:4")), + ) + } + respondServiceMetadata( + query = query0.query, + alpnIds = listOf("h2"), + ) + takeOnRecordsServiceMetadata( + alpnIds = listOf(Protocol.HTTP_2) + ) + takeOnRecordsIpAddresses( + last = true, + addresses = listOf( + InetAddress.getByName("10.20.30.40"), + InetAddress.getByName("1:2::3:4"), + ) + ) + } + + /** + * The implementation still enqueues canceled queries, because that's an easy way to jump to a + * dispatcher thread to post the failures back to the callback. + */ + @Test + fun `cancel before enqueue`() = testDnsCallStateMachine( + request = Dns.Request(hostname = "lysine.dev"), + ) { + call.cancel() + enqueue() + + val query0 = takeCancel("lysine.dev", TYPE_HTTPS) + takeQuery("lysine.dev", TYPE_HTTPS) + val query1 = takeCancel("lysine.dev", TYPE_AAAA) + takeQuery("lysine.dev", TYPE_AAAA) + val query2 = takeCancel("lysine.dev", TYPE_A) + takeQuery("lysine.dev", TYPE_A) + + respondFailure(query0.query, IOException("canceled")) + respondFailure(query1.query, IOException("canceled")) + respondFailure(query2.query, IOException("canceled")) + + takeOnFailure("canceled") + } + + /** Cancels are asynchronous and if the canceled query completes anyway, that's fine. */ + @Test + fun `cancel ignored if canceled query completes`() = testDnsCallStateMachine( + request = Dns.Request(hostname = "lysine.dev"), + ) { + enqueue() + + val query0 = takeQuery("lysine.dev", TYPE_HTTPS) + val query1 = takeQuery("lysine.dev", TYPE_AAAA) + val query2 = takeQuery("lysine.dev", TYPE_A) + + respondIpAddresses( + query = query1.query, + addresses = listOf(InetAddress.getByName("1:2::3:4")), + ) + takeOnRecordsIpAddresses( + addresses = listOf(InetAddress.getByName("1:2::3:4")) + ) + + call.cancel() + + takeCancel("lysine.dev", TYPE_HTTPS) + takeCancel("lysine.dev", TYPE_A) + + respondIpAddresses( + query = query2.query, + addresses = listOf(InetAddress.getByName("10.20.30.40")), + ) + takeOnRecordsIpAddresses( + addresses = listOf(InetAddress.getByName("10.20.30.40")) + ) + + respondServiceMetadata( + query = query0.query, + alpnIds = listOf("h2"), + ) + takeOnRecordsServiceMetadata( + last = true, + alpnIds = listOf(Protocol.HTTP_2) + ) + } +} diff --git a/okhttp/src/commonTest/kotlin/okhttp3/internal/dns/DnsCallStateMachineTester.kt b/okhttp/src/commonTest/kotlin/okhttp3/internal/dns/DnsCallStateMachineTester.kt new file mode 100644 index 000000000000..ddf4b19de806 --- /dev/null +++ b/okhttp/src/commonTest/kotlin/okhttp3/internal/dns/DnsCallStateMachineTester.kt @@ -0,0 +1,270 @@ +@file:OptIn(OkHttpInternalApi::class) + +package okhttp3.internal.dns + +import assertk.assertThat +import assertk.assertions.hasMessage +import assertk.assertions.isEqualTo +import java.io.IOException +import java.net.InetAddress +import java.util.concurrent.LinkedBlockingDeque +import okhttp3.Dns +import okhttp3.Protocol +import okhttp3.dnsResponse +import okhttp3.internal.OkHttpInternalApi +import okhttp3.internal.dns.DnsCallStateMachineTester.Event.OnRecords +import okhttp3.internal.dns.DnsCallStateMachineTester.Event.QueryEnqueued +import okio.ByteString + +/** + * Test the DNS state machine. + * + * This has helpers to operate on the state machine: + * + * This tracks all effects from the state machine as events: creating queries, canceling queries, + * calling callbacks. + */ +fun testDnsCallStateMachine( + request: Dns.Request, + includeIPv6: Boolean = true, + includeServiceMetadata: Boolean = true, + block: DnsCallStateMachineTester.() -> Unit, +) { + val tester = DnsCallStateMachineTester(request, includeIPv6, includeServiceMetadata) + tester.block() +} + +class DnsCallStateMachineTester internal constructor( + request: Dns.Request, + includeIPv6: Boolean = true, + includeServiceMetadata: Boolean = true, +) { + private val events = LinkedBlockingDeque() + var onNextEvent: (() -> Unit)? = null + + /** Defend against re-entrant calls. */ + private var acceptCallbacks: Boolean = true + + private val transport = object : DnsCallStateMachine.Transport { + override fun newQuery(dnsMessage: DnsMessage) = Query(dnsMessage) + + override fun enqueue(query: Query) { + postEvent(QueryEnqueued(query)) + } + + override fun cancel(query: Query) { + postEvent(Event.QueryCanceled(query)) + } + } + + val call: Dns.Call = object : Dns.Call { + override val request: Dns.Request = request + + override fun enqueue(callback: Dns.Callback) { + stateMachine.start(callback) + } + + override fun cancel() { + stateMachine.cancel() + } + + override fun isCanceled() = stateMachine.canceled + } + + private val callback = object : Dns.Callback { + override fun onRecords( + call: Dns.Call, + last: Boolean, + records: List + ) { + check(call == this@DnsCallStateMachineTester.call) + check(acceptCallbacks) { "unexpected callback" } + + acceptCallbacks = false + try { + postEvent(OnRecords(last, records)) + } finally { + acceptCallbacks = true + } + } + + override fun onFailure(call: Dns.Call, e: IOException) { + check(call == this@DnsCallStateMachineTester.call) + check(acceptCallbacks) { "unexpected callback" } + + acceptCallbacks = false + try { + postEvent(Event.OnFailure(e)) + } finally { + acceptCallbacks = true + } + } + } + + val stateMachine = DnsCallStateMachine( + transport = transport, + call = call, + canceledException = null, + includeIPv6 = includeIPv6, + includeServiceMetadata = includeServiceMetadata, + ) + + /** Start the DNS call. */ + fun enqueue() { + check(acceptCallbacks) { "unexpected enqueue" } + + acceptCallbacks = false + try { + call.enqueue(callback) + } finally { + acceptCallbacks = true + } + } + + private fun postEvent(e: Event) { + events.put(e) + + onNextEvent?.invoke() + onNextEvent = null + } + + /** Asserts that the next-posted event is a query enqueue. */ + fun takeQuery(hostname: String, type: Int): QueryEnqueued { + val event = events.take() as QueryEnqueued + assertThat(event.hostname).isEqualTo(hostname) + assertThat(event.type).isEqualTo(type) + return event + } + + /** Asserts that the next-posted event is a query cancel. */ + fun takeCancel(hostname: String, type: Int): Event.QueryCanceled { + val event = events.take() as Event.QueryCanceled + assertThat(event.hostname).isEqualTo(hostname) + assertThat(event.type).isEqualTo(type) + return event + } + + /** Respond to a [TYPE_A] or [TYPE_AAAA] query with a (possibly-empty) list of IP addresses. */ + fun respondIpAddresses( + query: Query, + timeToLive: Int = 300, + addresses: List = listOf(), + ) { + stateMachine.onQueryResponse( + query, + dnsResponse( + query.dnsMessage, + addresses.map { address -> + ResourceRecord.IpAddress( + name = query.dnsMessage.questions.single().name, + timeToLive = timeToLive, + address = address, + ) + } + ) + ) + } + + /** Respond to a [TYPE_HTTPS] query with service metadata. */ + fun respondServiceMetadata( + query: Query, + timeToLive: Int = 300, + alpnIds: List? = null, + echConfigList: ByteString? = null, + ) { + stateMachine.onQueryResponse( + query, + dnsResponse( + query.dnsMessage, + listOf( + ResourceRecord.Https( + name = query.dnsMessage.questions.single().name, + timeToLive = timeToLive, + alpnIds = alpnIds, + echConfigList = echConfigList, + ) + ) + ) + ) + } + + /** Respond to any query with a failure. */ + fun respondFailure( + query: Query, + e: IOException, + ) { + stateMachine.onQueryFailure(query, e) + } + + /** + * Asserts that the next-posted event is a call to [Dns.Callback.onRecords] with a list of IP + * addresses. + */ + fun takeOnRecordsIpAddresses( + last: Boolean = false, + addresses: List, + ): OnRecords { + val event = events.take() as OnRecords + assertThat(event.last).isEqualTo(last) + assertThat(event.records.map { (it as Dns.Record.IpAddress).address }) + .isEqualTo(addresses) + return event + } + + /** + * Asserts that the next-posted event is a call to [Dns.Callback.onRecords] with service metadata. + */ + fun takeOnRecordsServiceMetadata( + last: Boolean = false, + alpnIds: List? = null, + echConfigList: ByteString? = null, + ): OnRecords { + val event = events.take() as OnRecords + assertThat(event.last).isEqualTo(last) + + val serviceMetadata = event.records.single() as Dns.Record.ServiceMetadata + assertThat(serviceMetadata.alpnIds).isEqualTo(alpnIds) + assertThat(serviceMetadata.echConfigList).isEqualTo(echConfigList) + return event + } + + /** Asserts that the next-posted event is a call to [Dns.Callback.onFailure]. */ + fun takeOnFailure(message: String): Event.OnFailure { + val event = events.take() as Event.OnFailure + assertThat(event.e).hasMessage(message) + return event + } + + class Query( + val dnsMessage: DnsMessage, + ) + + sealed interface Event { + data class QueryEnqueued( + val query: Query, + ) : Event { + val hostname: String + get() = query.dnsMessage.questions.single().name + val type: Int + get() = query.dnsMessage.questions.single().type + } + + data class QueryCanceled( + val query: Query, + ) : Event { + val hostname: String + get() = query.dnsMessage.questions.single().name + val type: Int + get() = query.dnsMessage.questions.single().type + } + + data class OnRecords( + val last: Boolean, + val records: List + ) : Event + + data class OnFailure( + val e: IOException, + ) : Event + } +} From b33a1950f7d3cd6c66a0f3f789bb6a29ff7ae10c Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Wed, 22 Jul 2026 12:57:21 -0400 Subject: [PATCH 2/2] Spotless --- .../src/main/kotlin/okhttp3/FakeDns.kt | 2 +- .../internal/dns/DnsCallStateMachineTest.kt | 263 +++++++++--------- .../internal/dns/DnsCallStateMachineTester.kt | 159 ++++++----- 3 files changed, 230 insertions(+), 194 deletions(-) diff --git a/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt b/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt index b632f84608e0..f81392d16689 100644 --- a/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt +++ b/okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt @@ -326,7 +326,7 @@ class FakeDns( fun dnsResponse( request: DnsMessage, - answers: List + answers: List, ): DnsMessage { // QR = 1 (Response) // OPCODE = 0 (standard query) diff --git a/okhttp/src/commonTest/kotlin/okhttp3/internal/dns/DnsCallStateMachineTest.kt b/okhttp/src/commonTest/kotlin/okhttp3/internal/dns/DnsCallStateMachineTest.kt index 77a7d791340f..f123821707fe 100644 --- a/okhttp/src/commonTest/kotlin/okhttp3/internal/dns/DnsCallStateMachineTest.kt +++ b/okhttp/src/commonTest/kotlin/okhttp3/internal/dns/DnsCallStateMachineTest.kt @@ -26,74 +26,76 @@ import okhttp3.internal.OkHttpInternalApi class DnsCallStateMachineTest { @Test - fun `happy path`() = testDnsCallStateMachine( - request = Dns.Request(hostname = "lysine.dev"), - ) { - enqueue() - - val query0 = takeQuery("lysine.dev", TYPE_HTTPS) - val query1 = takeQuery("lysine.dev", TYPE_AAAA) - val query2 = takeQuery("lysine.dev", TYPE_A) - - respondIpAddresses( - query = query1.query, - addresses = listOf(InetAddress.getByName("1:2::3:4")), - ) - takeOnRecordsIpAddresses( - addresses = listOf(InetAddress.getByName("1:2::3:4")) - ) - - respondIpAddresses( - query = query2.query, - addresses = listOf(InetAddress.getByName("10.20.30.40")), - ) - takeOnRecordsIpAddresses( - addresses = listOf(InetAddress.getByName("10.20.30.40")) - ) - - respondServiceMetadata( - query = query0.query, - alpnIds = listOf("h2"), - ) - takeOnRecordsServiceMetadata( - last = true, - alpnIds = listOf(Protocol.HTTP_2) - ) - } + fun `happy path`() = + testDnsCallStateMachine( + request = Dns.Request(hostname = "lysine.dev"), + ) { + enqueue() + + val query0 = takeQuery("lysine.dev", TYPE_HTTPS) + val query1 = takeQuery("lysine.dev", TYPE_AAAA) + val query2 = takeQuery("lysine.dev", TYPE_A) + + respondIpAddresses( + query = query1.query, + addresses = listOf(InetAddress.getByName("1:2::3:4")), + ) + takeOnRecordsIpAddresses( + addresses = listOf(InetAddress.getByName("1:2::3:4")), + ) + + respondIpAddresses( + query = query2.query, + addresses = listOf(InetAddress.getByName("10.20.30.40")), + ) + takeOnRecordsIpAddresses( + addresses = listOf(InetAddress.getByName("10.20.30.40")), + ) + + respondServiceMetadata( + query = query0.query, + alpnIds = listOf("h2"), + ) + takeOnRecordsServiceMetadata( + last = true, + alpnIds = listOf(Protocol.HTTP_2), + ) + } @Test - fun `failure returned last`() = testDnsCallStateMachine( - request = Dns.Request(hostname = "lysine.dev"), - ) { - enqueue() - - val query0 = takeQuery("lysine.dev", TYPE_HTTPS) - val query1 = takeQuery("lysine.dev", TYPE_AAAA) - val query2 = takeQuery("lysine.dev", TYPE_A) - - respondFailure( - query = query1.query, - e = IOException("boom!"), - ) - - respondIpAddresses( - query = query2.query, - addresses = listOf(InetAddress.getByName("10.20.30.40")), - ) - takeOnRecordsIpAddresses( - addresses = listOf(InetAddress.getByName("10.20.30.40")) - ) - - respondServiceMetadata( - query = query0.query, - alpnIds = listOf("h2"), - ) - takeOnRecordsServiceMetadata( - alpnIds = listOf(Protocol.HTTP_2) - ) - - takeOnFailure("boom!") - } + fun `failure returned last`() = + testDnsCallStateMachine( + request = Dns.Request(hostname = "lysine.dev"), + ) { + enqueue() + + val query0 = takeQuery("lysine.dev", TYPE_HTTPS) + val query1 = takeQuery("lysine.dev", TYPE_AAAA) + val query2 = takeQuery("lysine.dev", TYPE_A) + + respondFailure( + query = query1.query, + e = IOException("boom!"), + ) + + respondIpAddresses( + query = query2.query, + addresses = listOf(InetAddress.getByName("10.20.30.40")), + ) + takeOnRecordsIpAddresses( + addresses = listOf(InetAddress.getByName("10.20.30.40")), + ) + + respondServiceMetadata( + query = query0.query, + alpnIds = listOf("h2"), + ) + takeOnRecordsServiceMetadata( + alpnIds = listOf(Protocol.HTTP_2), + ) + + takeOnFailure("boom!") + } /** * Confirm that the state machine calls doesn't call any [Dns.Callback] methods until the previous @@ -126,14 +128,15 @@ class DnsCallStateMachineTest { alpnIds = listOf("h2"), ) takeOnRecordsServiceMetadata( - alpnIds = listOf(Protocol.HTTP_2) + alpnIds = listOf(Protocol.HTTP_2), ) takeOnRecordsIpAddresses( last = true, - addresses = listOf( - InetAddress.getByName("10.20.30.40"), - InetAddress.getByName("1:2::3:4"), - ) + addresses = + listOf( + InetAddress.getByName("10.20.30.40"), + InetAddress.getByName("1:2::3:4"), + ), ) } @@ -142,65 +145,67 @@ class DnsCallStateMachineTest { * dispatcher thread to post the failures back to the callback. */ @Test - fun `cancel before enqueue`() = testDnsCallStateMachine( - request = Dns.Request(hostname = "lysine.dev"), - ) { - call.cancel() - enqueue() - - val query0 = takeCancel("lysine.dev", TYPE_HTTPS) - takeQuery("lysine.dev", TYPE_HTTPS) - val query1 = takeCancel("lysine.dev", TYPE_AAAA) - takeQuery("lysine.dev", TYPE_AAAA) - val query2 = takeCancel("lysine.dev", TYPE_A) - takeQuery("lysine.dev", TYPE_A) - - respondFailure(query0.query, IOException("canceled")) - respondFailure(query1.query, IOException("canceled")) - respondFailure(query2.query, IOException("canceled")) - - takeOnFailure("canceled") - } + fun `cancel before enqueue`() = + testDnsCallStateMachine( + request = Dns.Request(hostname = "lysine.dev"), + ) { + call.cancel() + enqueue() + + val query0 = takeCancel("lysine.dev", TYPE_HTTPS) + takeQuery("lysine.dev", TYPE_HTTPS) + val query1 = takeCancel("lysine.dev", TYPE_AAAA) + takeQuery("lysine.dev", TYPE_AAAA) + val query2 = takeCancel("lysine.dev", TYPE_A) + takeQuery("lysine.dev", TYPE_A) + + respondFailure(query0.query, IOException("canceled")) + respondFailure(query1.query, IOException("canceled")) + respondFailure(query2.query, IOException("canceled")) + + takeOnFailure("canceled") + } /** Cancels are asynchronous and if the canceled query completes anyway, that's fine. */ @Test - fun `cancel ignored if canceled query completes`() = testDnsCallStateMachine( - request = Dns.Request(hostname = "lysine.dev"), - ) { - enqueue() - - val query0 = takeQuery("lysine.dev", TYPE_HTTPS) - val query1 = takeQuery("lysine.dev", TYPE_AAAA) - val query2 = takeQuery("lysine.dev", TYPE_A) - - respondIpAddresses( - query = query1.query, - addresses = listOf(InetAddress.getByName("1:2::3:4")), - ) - takeOnRecordsIpAddresses( - addresses = listOf(InetAddress.getByName("1:2::3:4")) - ) - - call.cancel() - - takeCancel("lysine.dev", TYPE_HTTPS) - takeCancel("lysine.dev", TYPE_A) - - respondIpAddresses( - query = query2.query, - addresses = listOf(InetAddress.getByName("10.20.30.40")), - ) - takeOnRecordsIpAddresses( - addresses = listOf(InetAddress.getByName("10.20.30.40")) - ) - - respondServiceMetadata( - query = query0.query, - alpnIds = listOf("h2"), - ) - takeOnRecordsServiceMetadata( - last = true, - alpnIds = listOf(Protocol.HTTP_2) - ) - } + fun `cancel ignored if canceled query completes`() = + testDnsCallStateMachine( + request = Dns.Request(hostname = "lysine.dev"), + ) { + enqueue() + + val query0 = takeQuery("lysine.dev", TYPE_HTTPS) + val query1 = takeQuery("lysine.dev", TYPE_AAAA) + val query2 = takeQuery("lysine.dev", TYPE_A) + + respondIpAddresses( + query = query1.query, + addresses = listOf(InetAddress.getByName("1:2::3:4")), + ) + takeOnRecordsIpAddresses( + addresses = listOf(InetAddress.getByName("1:2::3:4")), + ) + + call.cancel() + + takeCancel("lysine.dev", TYPE_HTTPS) + takeCancel("lysine.dev", TYPE_A) + + respondIpAddresses( + query = query2.query, + addresses = listOf(InetAddress.getByName("10.20.30.40")), + ) + takeOnRecordsIpAddresses( + addresses = listOf(InetAddress.getByName("10.20.30.40")), + ) + + respondServiceMetadata( + query = query0.query, + alpnIds = listOf("h2"), + ) + takeOnRecordsServiceMetadata( + last = true, + alpnIds = listOf(Protocol.HTTP_2), + ) + } } diff --git a/okhttp/src/commonTest/kotlin/okhttp3/internal/dns/DnsCallStateMachineTester.kt b/okhttp/src/commonTest/kotlin/okhttp3/internal/dns/DnsCallStateMachineTester.kt index ddf4b19de806..0e088aae3423 100644 --- a/okhttp/src/commonTest/kotlin/okhttp3/internal/dns/DnsCallStateMachineTester.kt +++ b/okhttp/src/commonTest/kotlin/okhttp3/internal/dns/DnsCallStateMachineTester.kt @@ -45,69 +45,76 @@ class DnsCallStateMachineTester internal constructor( /** Defend against re-entrant calls. */ private var acceptCallbacks: Boolean = true - private val transport = object : DnsCallStateMachine.Transport { - override fun newQuery(dnsMessage: DnsMessage) = Query(dnsMessage) + private val transport = + object : DnsCallStateMachine.Transport { + override fun newQuery(dnsMessage: DnsMessage) = Query(dnsMessage) - override fun enqueue(query: Query) { - postEvent(QueryEnqueued(query)) - } - - override fun cancel(query: Query) { - postEvent(Event.QueryCanceled(query)) - } - } - - val call: Dns.Call = object : Dns.Call { - override val request: Dns.Request = request + override fun enqueue(query: Query) { + postEvent(QueryEnqueued(query)) + } - override fun enqueue(callback: Dns.Callback) { - stateMachine.start(callback) + override fun cancel(query: Query) { + postEvent(Event.QueryCanceled(query)) + } } - override fun cancel() { - stateMachine.cancel() - } + val call: Dns.Call = + object : Dns.Call { + override val request: Dns.Request = request - override fun isCanceled() = stateMachine.canceled - } + override fun enqueue(callback: Dns.Callback) { + stateMachine.start(callback) + } - private val callback = object : Dns.Callback { - override fun onRecords( - call: Dns.Call, - last: Boolean, - records: List - ) { - check(call == this@DnsCallStateMachineTester.call) - check(acceptCallbacks) { "unexpected callback" } - - acceptCallbacks = false - try { - postEvent(OnRecords(last, records)) - } finally { - acceptCallbacks = true + override fun cancel() { + stateMachine.cancel() } + + override fun isCanceled() = stateMachine.canceled } - override fun onFailure(call: Dns.Call, e: IOException) { - check(call == this@DnsCallStateMachineTester.call) - check(acceptCallbacks) { "unexpected callback" } + private val callback = + object : Dns.Callback { + override fun onRecords( + call: Dns.Call, + last: Boolean, + records: List, + ) { + check(call == this@DnsCallStateMachineTester.call) + check(acceptCallbacks) { "unexpected callback" } + + acceptCallbacks = false + try { + postEvent(OnRecords(last, records)) + } finally { + acceptCallbacks = true + } + } - acceptCallbacks = false - try { - postEvent(Event.OnFailure(e)) - } finally { - acceptCallbacks = true + override fun onFailure( + call: Dns.Call, + e: IOException, + ) { + check(call == this@DnsCallStateMachineTester.call) + check(acceptCallbacks) { "unexpected callback" } + + acceptCallbacks = false + try { + postEvent(Event.OnFailure(e)) + } finally { + acceptCallbacks = true + } } } - } - val stateMachine = DnsCallStateMachine( - transport = transport, - call = call, - canceledException = null, - includeIPv6 = includeIPv6, - includeServiceMetadata = includeServiceMetadata, - ) + val stateMachine = + DnsCallStateMachine( + transport = transport, + call = call, + canceledException = null, + includeIPv6 = includeIPv6, + includeServiceMetadata = includeServiceMetadata, + ) /** Start the DNS call. */ fun enqueue() { @@ -129,7 +136,10 @@ class DnsCallStateMachineTester internal constructor( } /** Asserts that the next-posted event is a query enqueue. */ - fun takeQuery(hostname: String, type: Int): QueryEnqueued { + fun takeQuery( + hostname: String, + type: Int, + ): QueryEnqueued { val event = events.take() as QueryEnqueued assertThat(event.hostname).isEqualTo(hostname) assertThat(event.type).isEqualTo(type) @@ -137,7 +147,10 @@ class DnsCallStateMachineTester internal constructor( } /** Asserts that the next-posted event is a query cancel. */ - fun takeCancel(hostname: String, type: Int): Event.QueryCanceled { + fun takeCancel( + hostname: String, + type: Int, + ): Event.QueryCanceled { val event = events.take() as Event.QueryCanceled assertThat(event.hostname).isEqualTo(hostname) assertThat(event.type).isEqualTo(type) @@ -156,12 +169,15 @@ class DnsCallStateMachineTester internal constructor( query.dnsMessage, addresses.map { address -> ResourceRecord.IpAddress( - name = query.dnsMessage.questions.single().name, + name = + query.dnsMessage.questions + .single() + .name, timeToLive = timeToLive, address = address, ) - } - ) + }, + ), ) } @@ -178,13 +194,16 @@ class DnsCallStateMachineTester internal constructor( query.dnsMessage, listOf( ResourceRecord.Https( - name = query.dnsMessage.questions.single().name, + name = + query.dnsMessage.questions + .single() + .name, timeToLive = timeToLive, alpnIds = alpnIds, echConfigList = echConfigList, - ) - ) - ) + ), + ), + ), ) } @@ -244,23 +263,35 @@ class DnsCallStateMachineTester internal constructor( val query: Query, ) : Event { val hostname: String - get() = query.dnsMessage.questions.single().name + get() = + query.dnsMessage.questions + .single() + .name val type: Int - get() = query.dnsMessage.questions.single().type + get() = + query.dnsMessage.questions + .single() + .type } data class QueryCanceled( val query: Query, ) : Event { val hostname: String - get() = query.dnsMessage.questions.single().name + get() = + query.dnsMessage.questions + .single() + .name val type: Int - get() = query.dnsMessage.questions.single().type + get() = + query.dnsMessage.questions + .single() + .type } data class OnRecords( val last: Boolean, - val records: List + val records: List, ) : Event data class OnFailure(