mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-10-18 07:33:46 +00:00
test(macOS): cover relay activity gating and assertion lifecycle
Covers the full transport truth table plus the bookkeeping a leak would otherwise only reveal in Instruments: no assertion while idle, begin exactly once, no stacking when a second transport comes up, hold through losing one transport, release with the last, re-arm after a radio cycles, and idempotent release.
This commit is contained in:
parent
eb19016442
commit
2f685bc8ec
240
bitchatTests/RelayActivityAssertionTests.swift
Normal file
240
bitchatTests/RelayActivityAssertionTests.swift
Normal file
@ -0,0 +1,240 @@
|
||||
//
|
||||
// RelayActivityAssertionTests.swift
|
||||
// bitchatTests
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import bitchat
|
||||
|
||||
/// macOS App Nap freezes Tor and BLE relay work once every window is minimized
|
||||
/// or occluded (#1593). The fix holds a `ProcessInfo` activity — but only while
|
||||
/// a transport is actually relaying, so an idle configuration does not pay for
|
||||
/// an assertion it cannot use.
|
||||
@MainActor
|
||||
struct RelayActivityAssertionTests {
|
||||
|
||||
/// Counts assertions so a leak (begin without end) or a double-begin is
|
||||
/// visible to the test rather than only to Instruments.
|
||||
private final class SpyAsserter: ProcessActivityAsserting {
|
||||
private(set) var beginCount = 0
|
||||
private(set) var endCount = 0
|
||||
private(set) var lastReason: String?
|
||||
var outstanding: Int { beginCount - endCount }
|
||||
|
||||
func beginActivity(reason: String) -> NSObjectProtocol {
|
||||
beginCount += 1
|
||||
lastReason = reason
|
||||
return NSString(string: "activity-\(beginCount)")
|
||||
}
|
||||
|
||||
func endActivity(_ token: NSObjectProtocol) {
|
||||
endCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
private typealias State = RelayActivityAssertion.TransportState
|
||||
|
||||
private func state(tor: Bool = false, ready: Bool = false, ble: Bool = false) -> State {
|
||||
State(torDesired: tor, torReady: ready, bluetoothPoweredOn: ble)
|
||||
}
|
||||
|
||||
// MARK: - Policy
|
||||
|
||||
@Test("An idle configuration holds no assertion")
|
||||
func idleConfigurationHoldsNothing() {
|
||||
// Tor off and Bluetooth off: nothing to relay, so nothing to protect.
|
||||
#expect(!RelayActivityAssertion.shouldHold(state()))
|
||||
}
|
||||
|
||||
@Test("BLE alone justifies the assertion")
|
||||
func bluetoothAloneJustifiesTheAssertion() {
|
||||
// Mesh-only, Tor deliberately off — the BLE relay is exactly what App
|
||||
// Nap would freeze on a minimized window.
|
||||
#expect(RelayActivityAssertion.shouldHold(state(ble: true)))
|
||||
}
|
||||
|
||||
@Test("Tor bootstrap justifies the assertion before it reports ready")
|
||||
func torBootstrapJustifiesTheAssertion() {
|
||||
// Dropping the assertion here is what would leave a minimized window
|
||||
// stuck "starting tor…" indefinitely.
|
||||
#expect(RelayActivityAssertion.shouldHold(state(tor: true)))
|
||||
}
|
||||
|
||||
@Test("Tor carrying traffic holds even if the policy flag lagged")
|
||||
func torReadyWithoutDesireStillHolds() {
|
||||
#expect(RelayActivityAssertion.shouldHold(state(ready: true)))
|
||||
}
|
||||
|
||||
@Test("Every transport combination matches the documented policy")
|
||||
func everyTransportCombinationMatchesPolicy() {
|
||||
for tor in [false, true] {
|
||||
for ready in [false, true] {
|
||||
for ble in [false, true] {
|
||||
#expect(
|
||||
RelayActivityAssertion.shouldHold(state(tor: tor, ready: ready, ble: ble))
|
||||
== (tor || ready || ble),
|
||||
"tor=\(tor) ready=\(ready) ble=\(ble)"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Assertion lifecycle
|
||||
|
||||
@Test("Nothing is asserted before a transport comes up")
|
||||
func doesNotHoldOnCreation() {
|
||||
let spy = SpyAsserter()
|
||||
let assertion = RelayActivityAssertion(asserter: spy)
|
||||
|
||||
#expect(!assertion.isHolding)
|
||||
#expect(spy.beginCount == 0)
|
||||
}
|
||||
|
||||
@Test("The assertion begins once, with the documented reason")
|
||||
func beginsOnceWhenATransportComesUp() {
|
||||
let spy = SpyAsserter()
|
||||
let assertion = RelayActivityAssertion(asserter: spy)
|
||||
|
||||
assertion.apply(state(ble: true))
|
||||
|
||||
#expect(assertion.isHolding)
|
||||
#expect(spy.beginCount == 1)
|
||||
#expect(spy.lastReason == RelayActivityAssertion.activityReason)
|
||||
}
|
||||
|
||||
@Test("Further active updates do not stack assertions")
|
||||
func repeatedActiveUpdatesDoNotStack() {
|
||||
// The three Combine sources fire independently; a second "still active"
|
||||
// update must not leak another ProcessInfo token.
|
||||
let spy = SpyAsserter()
|
||||
let assertion = RelayActivityAssertion(asserter: spy)
|
||||
|
||||
assertion.apply(state(ble: true))
|
||||
assertion.apply(state(tor: true, ble: true))
|
||||
assertion.apply(state(tor: true, ready: true, ble: true))
|
||||
|
||||
#expect(spy.beginCount == 1)
|
||||
#expect(spy.outstanding == 1)
|
||||
}
|
||||
|
||||
@Test("The assertion survives losing one transport and drops with the last")
|
||||
func releasesOnlyWhenTheLastTransportGoesDown() {
|
||||
let spy = SpyAsserter()
|
||||
let assertion = RelayActivityAssertion(asserter: spy)
|
||||
|
||||
assertion.apply(state(tor: true, ble: true))
|
||||
assertion.apply(state(tor: true)) // bluetooth off, Tor still up
|
||||
#expect(assertion.isHolding)
|
||||
#expect(spy.endCount == 0)
|
||||
|
||||
assertion.apply(state()) // everything off
|
||||
#expect(!assertion.isHolding)
|
||||
#expect(spy.outstanding == 0)
|
||||
}
|
||||
|
||||
@Test("Repeated idle updates do not double-release")
|
||||
func repeatedIdleUpdatesDoNotDoubleRelease() {
|
||||
let spy = SpyAsserter()
|
||||
let assertion = RelayActivityAssertion(asserter: spy)
|
||||
|
||||
assertion.apply(state(ble: true))
|
||||
assertion.apply(state())
|
||||
assertion.apply(state())
|
||||
|
||||
#expect(spy.endCount == 1)
|
||||
#expect(spy.outstanding == 0)
|
||||
}
|
||||
|
||||
@Test("A transport cycling off and on re-arms the exemption")
|
||||
func reacquiresAfterTransportsComeBack() {
|
||||
// e.g. the Bluetooth radio cycling — the relay must not stay throttled.
|
||||
let spy = SpyAsserter()
|
||||
let assertion = RelayActivityAssertion(asserter: spy)
|
||||
|
||||
assertion.apply(state(ble: true))
|
||||
assertion.apply(state())
|
||||
assertion.apply(state(ble: true))
|
||||
|
||||
#expect(assertion.isHolding)
|
||||
#expect(spy.beginCount == 2)
|
||||
#expect(spy.endCount == 1)
|
||||
#expect(spy.outstanding == 1)
|
||||
}
|
||||
|
||||
// MARK: - update()
|
||||
|
||||
@Test("A single-flag update drives the policy")
|
||||
func updateAppliesASingleFlag() {
|
||||
let spy = SpyAsserter()
|
||||
let assertion = RelayActivityAssertion(asserter: spy)
|
||||
|
||||
assertion.update { $0.bluetoothPoweredOn = true }
|
||||
#expect(assertion.isHolding)
|
||||
#expect(spy.beginCount == 1)
|
||||
|
||||
assertion.update { $0.bluetoothPoweredOn = false }
|
||||
#expect(!assertion.isHolding)
|
||||
#expect(spy.outstanding == 0)
|
||||
}
|
||||
|
||||
@Test("A no-op update does not touch the assertion")
|
||||
func updateIgnoresUnchangedState() {
|
||||
// `removeDuplicates()` upstream should prevent this, but the sinks are
|
||||
// independent and the assertion must not depend on that.
|
||||
let spy = SpyAsserter()
|
||||
let assertion = RelayActivityAssertion(asserter: spy)
|
||||
|
||||
assertion.update { $0.bluetoothPoweredOn = true }
|
||||
assertion.update { $0.bluetoothPoweredOn = true }
|
||||
|
||||
#expect(spy.beginCount == 1)
|
||||
}
|
||||
|
||||
// MARK: - Teardown
|
||||
|
||||
@Test("Release hands the assertion back")
|
||||
func releaseHandsTheAssertionBack() {
|
||||
// applicationWillTerminate path.
|
||||
let spy = SpyAsserter()
|
||||
let assertion = RelayActivityAssertion(asserter: spy)
|
||||
|
||||
assertion.apply(state(tor: true))
|
||||
assertion.release()
|
||||
|
||||
#expect(!assertion.isHolding)
|
||||
#expect(spy.outstanding == 0)
|
||||
}
|
||||
|
||||
@Test("Release is idempotent")
|
||||
func releaseIsIdempotent() {
|
||||
let spy = SpyAsserter()
|
||||
let assertion = RelayActivityAssertion(asserter: spy)
|
||||
|
||||
assertion.apply(state(tor: true))
|
||||
assertion.release()
|
||||
assertion.release()
|
||||
|
||||
#expect(spy.endCount == 1)
|
||||
#expect(spy.outstanding == 0)
|
||||
}
|
||||
|
||||
@Test("Release without a held assertion is safe")
|
||||
func releaseWithoutActivityIsSafe() {
|
||||
let spy = SpyAsserter()
|
||||
let assertion = RelayActivityAssertion(asserter: spy)
|
||||
|
||||
assertion.release()
|
||||
|
||||
#expect(spy.beginCount == 0)
|
||||
#expect(spy.endCount == 0)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
Loading…
x
Reference in New Issue
Block a user