refactor: modernize Cast discovery and app activation
This commit is contained in:
@@ -366,6 +366,87 @@ private func testIdentity() throws -> (SecIdentity, URL) {
|
||||
@main
|
||||
@MainActor
|
||||
struct CastSessionCheck {
|
||||
static func discovery() async throws {
|
||||
let prefix = "terebi-discovery-" + UUID().uuidString
|
||||
let firstID = prefix + "-first", secondID = prefix + "-second"
|
||||
let discovery = CastDiscovery()
|
||||
var listeners: [NWListener] = []
|
||||
var accepted: [NWConnection] = []
|
||||
defer {
|
||||
discovery.stop()
|
||||
for listener in listeners { listener.cancel() }
|
||||
for connection in accepted { connection.cancel() }
|
||||
}
|
||||
func advertise(_ suffix: String, id: String, name: String) throws -> NWListener {
|
||||
let listener = try NWListener(using: .tcp)
|
||||
listener.service = .init(name: prefix + suffix, type: "_googlecast._tcp",
|
||||
txtRecord: NWTXTRecord(["id": id, "fn": name]))
|
||||
listener.newConnectionHandler = { connection in
|
||||
MainActor.assumeIsolated {
|
||||
accepted.append(connection)
|
||||
connection.start(queue: .main)
|
||||
}
|
||||
}
|
||||
listener.start(queue: .main)
|
||||
listeners.append(listener)
|
||||
return listener
|
||||
}
|
||||
let first = try advertise("-a", id: firstID, name: "TV 10")
|
||||
let second = try advertise("-b", id: secondID, name: "TV 2")
|
||||
let duplicate = try advertise("-c", id: firstID, name: "TV 10")
|
||||
let fallback = try advertise("-fallback", id: "", name: "")
|
||||
try await until("Bonjour publishers are ready") { listeners.allSatisfy { $0.state == .ready } }
|
||||
let duplicatePort = duplicate.port!.rawValue
|
||||
discovery.start()
|
||||
discovery.start() // Repeated menu requests must not replace a running search.
|
||||
func ownDevices() -> [CastDevice] {
|
||||
discovery.devices.filter { $0.id == firstID || $0.id == secondID }
|
||||
}
|
||||
try await until("Bonjour TXT identity/name, deduplication and natural device order", attempts: 1500) {
|
||||
ownDevices().map(\.id) == [secondID, firstID] &&
|
||||
ownDevices().map(\.name) == ["TV 2", "TV 10"]
|
||||
}
|
||||
try await until("Empty TXT values fall back to the service name and identity", attempts: 1500) {
|
||||
discovery.devices.contains { $0.name == prefix + "-fallback" && $0.port == fallback.port?.rawValue }
|
||||
}
|
||||
let fallbackID = discovery.devices.first { $0.name == prefix + "-fallback" }!.id
|
||||
let device = ownDevices()[0]
|
||||
require(device.port == second.port?.rawValue, "Discovery preserves the advertised Cast port")
|
||||
let connection = NWConnection(host: NWEndpoint.Host(device.host),
|
||||
port: NWEndpoint.Port(rawValue: device.port)!, using: .tcp)
|
||||
defer { connection.cancel() }
|
||||
connection.start(queue: .main)
|
||||
try await until("Resolved host/port connects to the advertised receiver") {
|
||||
connection.state == .ready && !accepted.isEmpty
|
||||
}
|
||||
second.service = .init(name: prefix + "-b", type: "_googlecast._tcp",
|
||||
txtRecord: NWTXTRecord(["id": secondID, "fn": "TV 1"]))
|
||||
try await until("TXT changes update the device without changing its identity", attempts: 1500) {
|
||||
ownDevices().map(\.id) == [secondID, firstID] && ownDevices()[0].name == "TV 1"
|
||||
}
|
||||
first.cancel()
|
||||
try await until("A duplicate advertisement keeps the receiver available", attempts: 1500) {
|
||||
ownDevices().first(where: { $0.id == firstID })?.port == duplicatePort
|
||||
}
|
||||
duplicate.cancel()
|
||||
try await until("Removing the last advertisement removes the receiver", attempts: 1500) {
|
||||
ownDevices().map(\.id) == [secondID]
|
||||
}
|
||||
discovery.stop()
|
||||
require(discovery.devices.isEmpty, "Stop clears published receivers")
|
||||
discovery.start()
|
||||
try await until("Restart ignores cancelled browser/resolver callbacks and rediscovers the receiver", attempts: 1500) {
|
||||
ownDevices().map(\.id) == [secondID] && ownDevices()[0].name == "TV 1"
|
||||
}
|
||||
try await until("Service identity without TXT stays stable across restart", attempts: 1500) {
|
||||
discovery.devices.contains { $0.id == fallbackID && $0.name == prefix + "-fallback" }
|
||||
}
|
||||
second.cancel()
|
||||
try await until("Receiver removal after restart", attempts: 1500) { ownDevices().isEmpty }
|
||||
require(discovery.error == nil, "Successful browsing leaves no permission/network error")
|
||||
print("PASS: Bonjour discovery TXT identity/name/update, natural order, duplicate removal, routable host/port, stop/restart")
|
||||
}
|
||||
|
||||
static func codec() throws {
|
||||
let payload = Data("{\"type\":\"PING\"}".utf8)
|
||||
let message = CastWire.Message(source: "sender-0", destination: "receiver-0",
|
||||
@@ -921,10 +1002,15 @@ struct CastSessionCheck {
|
||||
}
|
||||
|
||||
static func main() async throws {
|
||||
if CommandLine.arguments.count == 2, CommandLine.arguments[1] == "--discovery" {
|
||||
try await discovery()
|
||||
return
|
||||
}
|
||||
if CommandLine.arguments.count == 6, CommandLine.arguments[1] == "--peer" {
|
||||
try await realPeer(CommandLine.arguments)
|
||||
return
|
||||
}
|
||||
try await discovery()
|
||||
try codec()
|
||||
let (identity, directory) = try testIdentity()
|
||||
defer { try? FileManager.default.removeItem(at: directory) }
|
||||
|
||||
Reference in New Issue
Block a user