Cover the circuit-health thresholds

Uses the log lines from the report verbatim. Pins both directions: a
same-instant burst and occasional failures far apart leave the indicator
alone, while four failures still going after twenty seconds downgrade it.
Also covers reset and a backwards clock jump.
This commit is contained in:
Taksh 2026-08-16 08:01:14 +05:30
parent 71dd9dfca3
commit e65fce66d0

View File

@ -0,0 +1,121 @@
package com.bitchat.android.net
import com.bitchat.android.util.AppConstants
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class TorCircuitHealthPolicyTest {
private val policy = TorCircuitHealthPolicy()
// Verbatim from the report in issue #610, trimmed at the scrubbed detail.
private val obtainExitCircuit =
"Arti: ERROR: Failed to connect through Tor: Error { detail: ObtainExitCircuit " +
"{ exit_ports: [scrubbed], cause: RequestFailed(RetryError { doing: " +
"\"find or build a tunnel\", errors: [...] }) } }"
private val socksError =
"Arti: ERROR: SOCKS connection error: tor: error connecting to Tor: " +
"Failed to obtain exit circuit for ports [scrubbed]"
@Test
fun `recognises the failures arti actually logs`() {
assertTrue(policy.isCircuitFailure(obtainExitCircuit))
assertTrue(policy.isCircuitFailure(socksError))
}
@Test
fun `does not treat bootstrap progress as a failure`() {
assertFalse(policy.isCircuitFailure("Sufficiently bootstrapped; system SOCKS now functional"))
assertFalse(policy.isCircuitFailure("We have found that guard [scrubbed] is usable."))
assertFalse(policy.isCircuitFailure("AMEx: state changed to Running"))
}
@Test
fun `a burst of simultaneous failures does not downgrade`() {
// The report shows three threads failing inside the same millisecond. That is an
// ordinary circuit loss, not a dead connection.
val t = 500_000L
repeat(20) {
assertFalse(
"a same-instant burst must not move the indicator",
policy.onCircuitFailure(t)
)
}
assertFalse(policy.onCircuitFailure(t + 1_000L))
}
@Test
fun `failures that persist past the minimum span downgrade`() {
val start = 500_000L
assertFalse(policy.onCircuitFailure(start))
assertFalse(policy.onCircuitFailure(start + 5_000L))
assertFalse(policy.onCircuitFailure(start + 10_000L))
// Fourth failure, and by now the outage has lasted past the minimum span.
assertTrue(
policy.onCircuitFailure(start + AppConstants.Tor.CIRCUIT_FAILURE_MIN_SPAN_MS)
)
}
@Test
fun `enough elapsed time alone is not enough`() {
val start = 500_000L
assertFalse(policy.onCircuitFailure(start))
// Long span, but only two failures in it.
assertFalse(policy.onCircuitFailure(start + AppConstants.Tor.CIRCUIT_FAILURE_WINDOW_MS))
}
@Test
fun `failures spread beyond the window start a fresh tally`() {
var t = 500_000L
repeat(10) {
assertFalse(
"occasional failures far apart are normal Tor behaviour",
policy.onCircuitFailure(t)
)
t += AppConstants.Tor.CIRCUIT_FAILURE_WINDOW_MS + 1_000L
}
}
@Test
fun `reset clears an in-progress tally`() {
val start = 500_000L
policy.onCircuitFailure(start)
policy.onCircuitFailure(start + 5_000L)
policy.onCircuitFailure(start + 10_000L)
policy.reset()
// Without the reset this next one would have been the fourth and would downgrade.
assertFalse(
policy.onCircuitFailure(start + AppConstants.Tor.CIRCUIT_FAILURE_MIN_SPAN_MS)
)
}
@Test
fun `a backwards clock jump restarts the window instead of downgrading`() {
val start = 500_000L
policy.onCircuitFailure(start)
policy.onCircuitFailure(start + 5_000L)
policy.onCircuitFailure(start + 10_000L)
assertFalse(policy.onCircuitFailure(start - 60_000L))
}
@Test
fun `a sustained outage downgrades exactly once per tally`() {
val start = 500_000L
var downgrades = 0
var t = start
repeat(4) {
if (policy.onCircuitFailure(t)) {
downgrades++
policy.reset()
}
t += 7_000L
}
assertTrue("a real outage must downgrade", downgrades == 1)
}
}