p2p-swift / Tests / IrdP2PTests / DirectBudgetTests.swift
980 words
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import Foundation
import Testing
@testable import IrdP2P
@testable import IrdP2PSignaling
import IrdP2PCrypto

/// lowPeer 00…01, highPeer 0f1e…, thirdPeer ff…00: low and high are each other's nearest
/// (distance 0f…); third's nearest is high (f0…), but high prefers low.
@Suite struct DirectBudgetTests {
    @Test func distanceIsTheBytewiseXor() throws {
        #expect(DirectBudget.distance(lowPeer, lowPeer) == [UInt8](repeating: 0, count: 16))
        #expect(DirectBudget.distance(lowPeer, highPeer) == [0x0f, 0x1e, 0x2d, 0x3c, 0x4b, 0x5a, 0x69, 0x78, 0x87, 0x96, 0xa5, 0xb4, 0xc3, 0xd2, 0xe1, 0xf1])
        #expect(DirectBudget.distance(lowPeer, highPeer) == DirectBudget.distance(highPeer, lowPeer))
        let all: Set<PeerId> = [lowPeer, highPeer, thirdPeer]
        #expect(DirectBudget.nearest(to: lowPeer, among: all, limit: 1) == [highPeer])
        #expect(DirectBudget.nearest(to: highPeer, among: all, limit: 1) == [lowPeer])
        #expect(DirectBudget.nearest(to: thirdPeer, among: all, limit: 1) == [highPeer])
        #expect(DirectBudget.nearest(to: thirdPeer, among: all, limit: 5) == [lowPeer, highPeer])
        #expect(DirectBudget.nearest(to: thirdPeer, among: all, limit: 0).isEmpty)
        // the first differing byte decides, not the numeric order of the ids
        let a = try PeerId("80000000000000000000000000000000"), b = try PeerId("81ffffffffffffffffffffffffffffff"), c = try PeerId("7fffffffffffffffffffffffffffffff")
        #expect(DirectBudget.nearest(to: a, among: [a, b, c], limit: 1) == [b])
    }

    @Test func bothEndsComputeTheSameDirectSet() {
        for limit in [1, 2, 3, 7] {
            let members = Set((0..<12).map { _ in PeerId.random() })
            var degree: [PeerId: Int] = [:]
            for a in members {
                for b in members where a != b {
                    let ab = DirectBudget.allows(a, b, members: members, limit: limit)
                    #expect(ab == DirectBudget.allows(b, a, members: members, limit: limit))   // what a decides about b, b decides about a
                    // …also when each end passes only the roster it sees (everyone but itself)
                    #expect(ab == DirectBudget.allows(b, a, members: members.subtracting([b]), limit: limit))
                    #expect(ab == (DirectBudget.nearest(to: a, among: members, limit: limit).contains(b) && DirectBudget.nearest(to: b, among: members, limit: limit).contains(a)))
                    if ab { degree[a, default: 0] += 1 }
                    #expect(DirectBudget.allows(a, b, members: members, limit: nil))   // no cap: everyone
                }
            }
            #expect(degree.values.allSatisfy { $0 <= limit })
            #expect(!degree.isEmpty)   // the two closest members always pick each other
        }
    }

    func capped(_ h: Harness, _ limit: Int?) -> RoomOptions {
        var o = h.options()
        o.maxDirectPeers = limit
        return o
    }

    @Test func uncappedIsTheDefaultAndConnectsEveryone() async throws {
        #expect(RoomOptions().maxDirectPeers == nil)
        let h = Harness()
        let (a, _, _) = try await h.join(peer: lowPeer, options: capped(h, nil))
        let (b, _, _) = try await h.join(peer: highPeer, options: capped(h, nil))
        let (c, _, _) = try await h.join(peer: thirdPeer, options: capped(h, nil))
        #expect(await eventuallyTrue {
            let t = [await a.transport(of: highPeer), await a.transport(of: thirdPeer), await b.transport(of: thirdPeer), await c.transport(of: lowPeer)]
            return t.allSatisfy { $0 == .direct }
        })
        await a.leave(); await b.leave(); await c.leave()
    }

    /// low and high settle first, then third joins: nobody may offer to it.
    func cappedTrio(_ h: Harness) async throws -> (rooms: [Room], collectors: [Collector], factories: [RecordingFactory]) {
        let (a, ca, fa) = try await h.join(peer: lowPeer, options: capped(h, 1))
        let (b, cb, fb) = try await h.join(peer: highPeer, options: capped(h, 1))
        #expect(await ca.waitFor(.peerReady(highPeer, .direct)))
        #expect(await cb.waitFor(.peerReady(lowPeer, .direct)))
        let (c, cc, fc) = try await h.join(peer: thirdPeer, options: capped(h, 1))
        #expect(await eventuallyTrue { await c.members == [lowPeer, highPeer] })
        #expect(await eventuallyTrue { await a.members == [highPeer, thirdPeer] })
        #expect(await eventuallyTrue { await b.members == [lowPeer, thirdPeer] })
        try? await Task.sleep(for: .milliseconds(50))
        return ([a, b, c], [ca, cb, cc], [fa, fb, fc])
    }

    @Test func noOfferIsSentToPeersOutsideTheBudget() async throws {
        let h = Harness()
        let (rooms, collectors, factories) = try await cappedTrio(h)
        #expect(factories.map(\.count) == [1, 1, 0])   // one connection, low–high; third was never offered to
        #expect(await h.webrtc.connectionAttempts == 1)
        #expect(await rooms[0].transport(of: thirdPeer) == .relayed)
        #expect(await rooms[1].transport(of: thirdPeer) == .relayed)
        #expect(await rooms[2].transport(of: lowPeer) == .relayed)
        #expect(await rooms[2].transport(of: highPeer) == .relayed)
        // the relay carries what the mesh does not
        try await rooms[2].send(to: lowPeer, .json(.string("via relay")))
        #expect(await collectors[0].waitFor(.message(from: thirdPeer, .json(.string("via relay")))))
        for r in rooms { await r.leave() }
    }

    @Test func peersOutsideTheBudgetRefuseToAnswer() async throws {
        let h = Harness()
        // high and third are each other's only peer, so they connect; low arrives uncapped and offers to both
        let (b, cb, fb) = try await h.join(peer: highPeer, options: capped(h, 1))
        let (c, cc, fc) = try await h.join(peer: thirdPeer, options: capped(h, 1))
        #expect(await cc.waitFor(.peerReady(highPeer, .direct)))
        let (a, ca, fa) = try await h.join(peer: lowPeer, options: capped(h, nil))
        #expect(await ca.waitFor(.peerReady(highPeer, .direct)))   // high's nearest is low: answered
        #expect(await eventuallyTrue { fa.count == 2 })             // low did offer to third as well…
        try await a.send(to: thirdPeer, .json(.string("after the offer")))
        #expect(await cc.waitFor(.message(from: lowPeer, .json(.string("after the offer")))))
        try? await Task.sleep(for: .milliseconds(50))
        #expect(fc.count == 1)                                       // …and third, whose nearest is high, built nothing for it
        #expect(await c.transport(of: lowPeer) == .relayed)
        #expect(await a.transport(of: thirdPeer) == .relayed)
        // the open high–third channel is outside high's budget now, and stays
        #expect(await b.transport(of: thirdPeer) == .direct)
        #expect(cb.count(.peerReady(thirdPeer, .relayed)) == 1)   // only the one at verification
        // the refusal reached low as `restart`: its ladder retries, and is refused again
        #expect(await eventuallyTrue { h.clock.sleeperCount >= 4 })   // three keepalives + low's restart
        h.clock.advance(by: .seconds(2))
        #expect(await eventuallyTrue { fa.count == 3 })
        try? await Task.sleep(for: .milliseconds(50))
        #expect(fc.count == 1 && fb.count == 2)
        #expect(await c.transport(of: lowPeer) == .relayed)
        await a.leave(); await b.leave(); await c.leave()
    }

    @Test func wakeRespectsTheBudget() async throws {
        let h = Harness()
        let (rooms, collectors, factories) = try await cappedTrio(h)
        for r in rooms { await r.wake() }
        try? await Task.sleep(for: .milliseconds(50))
        #expect(factories.map(\.count) == [1, 1, 0])   // third is verified and relayed, yet nobody renegotiates it
        // a lost channel inside the budget is still retried by wake(), and only that one
        await h.webrtc.failAll()
        #expect(await collectors[0].waitFor(timeout: .seconds(5)) { _ in collectors[0].count(.peerReady(highPeer, .relayed)) >= 2 } != nil)
        await rooms[0].wake()
        #expect(await eventuallyTrue { await rooms[0].transport(of: highPeer) == .direct })
        try? await Task.sleep(for: .milliseconds(50))
        #expect(factories.map(\.count) == [2, 2, 0])
        #expect(await rooms[0].transport(of: thirdPeer) == .relayed)
        for r in rooms { await r.leave() }
    }

    @Test func aLeaveFreesTheBudget() async throws {
        let h = Harness()
        let (rooms, collectors, factories) = try await cappedTrio(h)
        await rooms[1].leave()   // high is gone: low and third are each other's nearest now
        #expect(await collectors[0].waitFor(.peerReady(thirdPeer, .direct)))
        #expect(await collectors[2].waitFor(.peerReady(lowPeer, .direct)))
        #expect(factories[2].count == 1)
        await rooms[0].leave(); await rooms[2].leave()
    }
}

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/p2p-swift.git