Skip to content

Commit 203ed3f

Browse files
committed
added PrunableMetadata cached value tests, ensuring json encoding is deterministic and byte value comparisons work the way we expect them to.
1 parent 938ae95 commit 203ed3f

1 file changed

Lines changed: 133 additions & 0 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the swift-libp2p open source project
4+
//
5+
// Copyright (c) 2022-2025 swift-libp2p project authors
6+
// Licensed under MIT
7+
//
8+
// See LICENSE for license information
9+
// See CONTRIBUTORS for the list of swift-libp2p project authors
10+
//
11+
// SPDX-License-Identifier: MIT
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
import LibP2P
17+
import LibP2PNoise
18+
import LibP2PTesting
19+
import LibP2PYAMUX
20+
import Testing
21+
22+
@testable import LibP2PKadDHT
23+
24+
extension LibP2PKadDHTTests {
25+
26+
/// The pre-encoded peerstore metadata values.
27+
/// - TODO: Remove these tests once swift-libp2p revamps it's peerstore and metadata book
28+
@Suite("Peer Metadata Tests", .serialized)
29+
struct PeerMetadataTests {
30+
31+
@Test func testCachedEncodingsMatchAFreshEncode() throws {
32+
let necessary = try JSONEncoder().encode(MetadataBook.PrunableMetadata(prunable: .necessary)).byteArray
33+
let preferred = try JSONEncoder().encode(MetadataBook.PrunableMetadata(prunable: .preferred)).byteArray
34+
let prunable = try JSONEncoder().encode(MetadataBook.PrunableMetadata(prunable: .prunable)).byteArray
35+
36+
#expect(KadDHT.PeerPrunableMetadata.necessary == necessary)
37+
#expect(KadDHT.PeerPrunableMetadata.preferred == preferred)
38+
#expect(KadDHT.PeerPrunableMetadata.prunable == prunable)
39+
}
40+
41+
@Test func testEncodingIsStableAcrossCalls() throws {
42+
let encodings = try (0..<16).map { _ in
43+
try JSONEncoder().encode(MetadataBook.PrunableMetadata(prunable: .necessary)).byteArray
44+
}
45+
#expect(Set(encodings.map { Data($0) }).count == 1, "JSON encoding of the metadata isn't deterministic")
46+
}
47+
48+
@Test func testCachedEncodingsRoundTrip() throws {
49+
let necessary = try JSONDecoder().decode(
50+
MetadataBook.PrunableMetadata.self,
51+
from: Data(KadDHT.PeerPrunableMetadata.necessary)
52+
)
53+
let preferred = try JSONDecoder().decode(
54+
MetadataBook.PrunableMetadata.self,
55+
from: Data(KadDHT.PeerPrunableMetadata.preferred)
56+
)
57+
let prunable = try JSONDecoder().decode(
58+
MetadataBook.PrunableMetadata.self,
59+
from: Data(KadDHT.PeerPrunableMetadata.prunable)
60+
)
61+
62+
#expect(necessary.prunable == .necessary)
63+
#expect(preferred.prunable == .preferred)
64+
#expect(prunable.prunable == .prunable)
65+
}
66+
67+
@Test func testTheCachedValuesDiffer() {
68+
#expect(!KadDHT.PeerPrunableMetadata.necessary.isEmpty)
69+
#expect(!KadDHT.PeerPrunableMetadata.preferred.isEmpty)
70+
#expect(!KadDHT.PeerPrunableMetadata.prunable.isEmpty)
71+
#expect(KadDHT.PeerPrunableMetadata.necessary != KadDHT.PeerPrunableMetadata.prunable)
72+
#expect(KadDHT.PeerPrunableMetadata.necessary != KadDHT.PeerPrunableMetadata.preferred)
73+
#expect(KadDHT.PeerPrunableMetadata.prunable != KadDHT.PeerPrunableMetadata.preferred)
74+
}
75+
76+
// MARK: - Wiring
77+
78+
/// A peer dropped from the routing table gets marked prunable in the peerstore, via the
79+
/// table's `peerRemovedHandler`.
80+
@Test func testDroppedPeerIsMarkedPrunable() async throws {
81+
try await withApp(configure: LibP2PKadDHTTests.dhtHost()) { app in
82+
let node = app.dht.kadDHT
83+
let peer = try PeerID(.Ed25519)
84+
try await app.peers.add(
85+
peerInfo: PeerInfo(peer: peer, addresses: [try Multiaddr("/ip4/127.0.0.1/tcp/4001")])
86+
).get()
87+
#expect(try await node.routingTable.addPeer(peer, isQueryPeer: true).get())
88+
89+
#expect(try await node.routingTable.removePeer(peer).get())
90+
91+
/// The handler fires off the removal, so give it a moment to land.
92+
let stored = try await Self.awaitPrunableMetadata(for: peer, on: app)
93+
#expect(
94+
stored == KadDHT.PeerPrunableMetadata.prunable,
95+
"a peer the table let go of should be prunable in the peerstore"
96+
)
97+
}
98+
}
99+
100+
/// A bootstrap peer is marked necessary on the way in.
101+
@Test func testBootstrapPeerIsMarkedNecessary() async throws {
102+
let bootstrap = try generateRandomPeerInfo()
103+
try await withApp(
104+
configure: LibP2PKadDHTTests.dhtHost(bootstrapPeers: [bootstrap])
105+
) { app in
106+
let stored = try await Self.awaitPrunableMetadata(for: bootstrap.peer, on: app)
107+
#expect(
108+
stored == KadDHT.PeerPrunableMetadata.necessary,
109+
"a bootstrap peer should be necessary, not prunable"
110+
)
111+
}
112+
}
113+
}
114+
}
115+
116+
extension LibP2PKadDHTTests.PeerMetadataTests {
117+
118+
/// Waits for `peer` to carry prunability metadata, since it's written off the back of a handler
119+
/// rather than synchronously with the call that triggered it.
120+
fileprivate static func awaitPrunableMetadata(
121+
for peer: PeerID,
122+
on app: Application,
123+
timeout: Duration = .seconds(2)
124+
) async throws -> [UInt8]? {
125+
let deadline = ContinuousClock.now + timeout
126+
while true {
127+
let metadata = try? await app.peers.getMetadata(forPeer: peer).get()
128+
if let value = metadata?[MetadataBook.Keys.Prunable.rawValue] { return value }
129+
if ContinuousClock.now >= deadline { return nil }
130+
try await Task.sleep(for: .milliseconds(10))
131+
}
132+
}
133+
}

0 commit comments

Comments
 (0)