oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1import XCTest
2
3@testable @_spi(Fixtures) import CallsKit
4
5/// Spec §15.26: the quality report built from fixture statistics — counts
6/// over the window, rates, the schema's enums and bounds — and nothing in
7/// it that names a person, a device or an address; the notices tallied;
8/// the device kinds; "Report a problem"'s anonymous diagnostics and its
9/// sheet's states.
10final class QualityReportTests: XCTestCase {
11 // MARK: - Fixtures
12
13 static let client = QualityClient(app: "mac", version: "0.4.8", os: "macos", osVersion: "26.0.1")
14
15 /// One connection's statistics, as WebRTC's report has them, with the
16 /// things that must never be reported planted in it: an address, a
17 /// port, a name in a track's label, a device's name.
18 static func sample(
19 generation: Int64 = 1, scale: Double = 1, screen: Bool = false, lostOut: Double = 3,
20 relayTLS: Bool = true, nan: Bool = false
21 ) -> QualityPeerSample {
22 var stats: [String: QualityStat] = [
23 "OA": QualityStat(
24 type: "outbound-rtp", numbers: ["bytesSent": 120_000 * scale, "packetsSent": 1500 * scale],
25 strings: ["kind": "audio", "mid": "0", "trackIdentifier": "Vicky Wallace's microphone"]),
26 "RIA": QualityStat(
27 type: "remote-inbound-rtp",
28 numbers: ["packetsLost": lostOut * scale, "roundTripTime": 0.042, "jitter": 0.004],
29 strings: ["kind": "audio", "localId": "OA"]),
30 "OV": QualityStat(
31 type: "outbound-rtp",
32 numbers: [
33 "bytesSent": 1_500_000 * scale, "packetsSent": 1800 * scale, "frameWidth": 1280, "frameHeight": 720,
34 "framesPerSecond": nan ? .nan : 29.97,
35 ],
36 strings: ["kind": "video", "mid": "1", "qualityLimitationReason": "bandwidth"]),
37 "IA": QualityStat(
38 type: "inbound-rtp",
39 numbers: [
40 "bytesReceived": 110_000 * scale, "packetsReceived": 1490 * scale, "packetsLost": 10 * scale,
41 "jitter": 0.012, "totalSamplesReceived": 1_440_000 * scale, "concealedSamples": 4800 * scale,
42 "insertedSamplesForDeceleration": 960 * scale, "removedSamplesForAcceleration": 480 * scale,
43 "jitterBufferDelay": 72_000 * 0.06 * scale, "jitterBufferEmittedCount": 72_000 * scale,
44 ],
45 strings: ["kind": "audio", "mid": "0"]),
46 "IV": QualityStat(
47 type: "inbound-rtp",
48 numbers: [
49 "bytesReceived": 1_200_000 * scale, "packetsReceived": 1700 * scale, "packetsLost": 5 * scale,
50 "jitter": 0.02, "freezeCount": 1 * scale, "totalFreezesDuration": 0.5 * scale,
51 "framesDecoded": 890 * scale, "framesDropped": 2 * scale, "frameWidth": 640, "frameHeight": 480,
52 "framesPerSecond": 30,
53 ],
54 strings: ["kind": "video", "mid": "1"]),
55 "T": QualityStat(type: "transport", strings: ["selectedCandidatePairId": "CP"]),
56 "CP": QualityStat(
57 type: "candidate-pair", numbers: ["currentRoundTripTime": 0.038, "nominated": 1],
58 strings: ["localCandidateId": "LC", "remoteCandidateId": "RC", "state": "succeeded"]),
59 "LC": QualityStat(
60 type: "local-candidate", numbers: ["port": 54321],
61 strings: [
62 "candidateType": relayTLS ? "relay" : "host", "protocol": "udp", "relayProtocol": "tls",
63 "address": "192.168.1.20", "url": "turns:relay.example.com:443",
64 ]),
65 "RC": QualityStat(
66 type: "remote-candidate", numbers: ["port": 61000],
67 strings: ["candidateType": "srflx", "protocol": "udp", "address": "203.0.113.7"]),
68 "MS": QualityStat(type: "media-source", strings: ["kind": "audio", "deviceName": "Paul's AirPods Pro"]),
69 ]
70 if screen {
71 stats["OS"] = QualityStat(
72 type: "outbound-rtp",
73 numbers: [
74 "bytesSent": 3_000_000 * scale, "packetsSent": 3000 * scale, "frameWidth": 1920, "frameHeight": 1080,
75 "framesPerSecond": 8,
76 ],
77 strings: ["kind": "video", "mid": "3", "qualityLimitationReason": "cpu"])
78 stats["IS"] = QualityStat(
79 type: "inbound-rtp",
80 numbers: ["bytesReceived": 900_000 * scale, "framesDecoded": 300 * scale, "freezeCount": 2 * scale],
81 strings: ["kind": "video", "mid": "4"])
82 }
83 return QualityPeerSample(
84 generation: generation, ownScreenMid: screen ? "3" : nil, screenMid: screen ? "4" : "", stats: stats)
85 }
86
87 func build(
88 _ builder: inout QualityReportBuilder, at now: Double, _ samples: [String: QualityPeerSample],
89 tally: QualityEventTally = QualityEventTally(), final: Bool = false
90 ) -> QualityReport {
91 var tally = tally
92 return builder.build(
93 at: now, samples: samples, events: &tally, client: Self.client,
94 devices: QualityDevices(input: "bluetooth", output: "built-in"), final: final)
95 }
96
97 // MARK: - Counts over the window
98
99 func testCountsAreTheWindowsAndRatesAreOverIt() throws {
100 var builder = QualityReportBuilder()
101 builder.start(at: 100)
102 _ = build(&builder, at: 130, ["abc123def": Self.sample(scale: 1)])
103 // Thirty seconds later every counter has doubled: the window's counts
104 // are the second half alone.
105 let report = build(&builder, at: 160, ["abc123def": Self.sample(scale: 2)])
106 XCTAssertEqual(report.seconds, 30)
107 XCTAssertNil(report.isFinal)
108 let peer = try XCTUnwrap(report.peers?.first)
109 XCTAssertEqual(peer.peer, "abc123def")
110 let outAudio = try XCTUnwrap(peer.out?.audio)
111 XCTAssertEqual(outAudio.packets, 1500)
112 XCTAssertEqual(outAudio.kbps, 32, "120,000 bytes in 30 s is 32 kbit/s")
113 XCTAssertEqual(outAudio.lost, 3)
114 XCTAssertEqual(outAudio.rtt, 42)
115 XCTAssertEqual(outAudio.jitter, 4)
116 let outVideo = try XCTUnwrap(peer.out?.video)
117 XCTAssertEqual(outVideo.kbps, 400)
118 XCTAssertEqual(outVideo.limit, "bandwidth")
119 XCTAssertEqual(outVideo.width, 1280)
120 XCTAssertEqual(outVideo.fps, 30, "rounded to a tenth")
121 let inAudio = try XCTUnwrap(peer.in?.audio)
122 XCTAssertEqual(inAudio.packets, 1490)
123 XCTAssertEqual(inAudio.lost, 10)
124 XCTAssertEqual(inAudio.jitter, 12)
125 XCTAssertEqual(inAudio.samples, 1_440_000)
126 XCTAssertEqual(inAudio.concealed, 4800)
127 XCTAssertEqual(inAudio.inserted, 960)
128 XCTAssertEqual(inAudio.removed, 480)
129 XCTAssertEqual(inAudio.jitterBufferMs, 60)
130 let inVideo = try XCTUnwrap(peer.in?.video)
131 XCTAssertEqual(inVideo.freezes, 1)
132 XCTAssertEqual(inVideo.freezeSeconds, 0.5)
133 XCTAssertEqual(inVideo.decoded, 890)
134 XCTAssertEqual(inVideo.dropped, 2)
135 XCTAssertNil(peer.in?.screen)
136 XCTAssertNil(peer.out?.screen)
137 XCTAssertEqual(peer.path, QualityPath(local: "relay", remote: "srflx", protocol: "tls"))
138 XCTAssertEqual(peer.rtt, 38)
139 }
140
141 func testTheFirstWindowOfAConnectionCountsFromItsStart() throws {
142 var builder = QualityReportBuilder()
143 builder.start(at: 0)
144 let report = build(&builder, at: 30, ["p1": Self.sample(scale: 1)])
145 XCTAssertEqual(report.peers?.first?.out?.audio?.packets, 1500)
146 }
147
148 func testANewGenerationOrACounterGoingBackStartsAgain() throws {
149 var builder = QualityReportBuilder()
150 builder.start(at: 0)
151 _ = build(&builder, at: 30, ["p1": Self.sample(generation: 1, scale: 4)])
152 // A new connection: its counts are its own, not less than the old one's.
153 var report = build(&builder, at: 60, ["p1": Self.sample(generation: 2, scale: 1)])
154 XCTAssertEqual(report.peers?.first?.in?.audio?.packets, 1490)
155 // The same connection, a counter gone backwards (a receiver rebuilt).
156 report = build(&builder, at: 90, ["p1": Self.sample(generation: 2, scale: 0.5)])
157 XCTAssertEqual(report.peers?.first?.in?.audio?.packets, 745)
158 }
159
160 func testScreensAreToldApartByMid() throws {
161 var builder = QualityReportBuilder()
162 builder.start(at: 0)
163 let report = build(&builder, at: 30, ["p1": Self.sample(screen: true)])
164 let peer = try XCTUnwrap(report.peers?.first)
165 XCTAssertEqual(peer.out?.screen?.width, 1920)
166 XCTAssertEqual(peer.out?.screen?.limit, "cpu")
167 XCTAssertEqual(peer.out?.video?.width, 1280, "the camera is not the screen")
168 XCTAssertEqual(peer.in?.screen?.decoded, 300)
169 XCTAssertEqual(peer.in?.screen?.freezes, 2)
170 XCTAssertEqual(peer.in?.video?.decoded, 890)
171 }
172
173 func testBoundsNaNAndNegativesAndThePeerCeiling() throws {
174 var builder = QualityReportBuilder()
175 builder.start(at: 0)
176 var samples: [String: QualityPeerSample] = [:]
177 for i in 0..<6 { samples["peer\(i)"] = Self.sample(lostOut: -5, nan: true) }
178 // A window longer than the schema's ten minutes.
179 let report = build(&builder, at: 900, samples)
180 XCTAssertEqual(report.seconds, 600)
181 XCTAssertEqual(report.peers?.count, 4, "no more than the schema's four")
182 XCTAssertEqual(report.peers?.map(\.peer), ["peer0", "peer1", "peer2", "peer3"])
183 let video = try XCTUnwrap(report.peers?.first?.out?.video)
184 XCTAssertNil(video.fps, "NaN is left out, not sent")
185 XCTAssertNil(report.peers?.first?.out?.audio?.lost, "a negative count is left out")
186 XCTAssertEqual(QualityReportBuilder.bound(2e6, QualityBounds.kbps), 1e6)
187 XCTAssertNil(QualityReportBuilder.bound(.infinity, 10))
188 XCTAssertNil(QualityReportBuilder.bound(-1, 10))
189 XCTAssertEqual(QualityReportBuilder.bound(1.26, 10), 1.3)
190 }
191
192 func testTheEnums() {
193 XCTAssertEqual(QualityWords.limit("none"), "none")
194 XCTAssertEqual(QualityWords.limit("cpu"), "cpu")
195 XCTAssertEqual(QualityWords.limit("something new"), "other")
196 XCTAssertNil(QualityWords.limit(""))
197 XCTAssertEqual(QualityWords.candidate("prflx"), "prflx")
198 XCTAssertEqual(QualityWords.candidate("mystery"), "unknown")
199 XCTAssertEqual(QualityWords.candidate(nil), "unknown")
200 XCTAssertEqual(QualityWords.transport("tcp", relayProtocol: nil, relay: false), "tcp")
201 XCTAssertEqual(QualityWords.transport("udp", relayProtocol: "tls", relay: true), "tls")
202 XCTAssertEqual(QualityWords.transport("udp", relayProtocol: "tls", relay: false), "udp")
203 XCTAssertEqual(QualityWords.transport(nil, relayProtocol: nil, relay: false), "unknown")
204 }
205
206 // MARK: - Nothing that names anyone
207
208 func testTheEncodedReportCarriesNoNamesAddressesOrDevices() throws {
209 var builder = QualityReportBuilder()
210 builder.start(at: 0)
211 var tally = QualityEventTally()
212 tally.observe(at: 1, call: false, reconnecting: ["p1"], cantHear: [])
213 let report = build(&builder, at: 30, ["p1": Self.sample(screen: true)], tally: tally, final: true)
214 let json = try XCTUnwrap(report.encoded())
215 for forbidden in [
216 "Vicky", "Wallace", "AirPods", "Paul", "192.168", "203.0.113", "54321", "61000", "relay.example.com",
217 "turns:", "address", "trackIdentifier", "deviceName",
218 ] {
219 XCTAssertFalse(json.contains(forbidden), "the report carries \(forbidden): \(json)")
220 }
221 // And it is the schema's shape.
222 let object = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(json.utf8)) as? [String: Any])
223 XCTAssertEqual(object["v"] as? Int, 1)
224 XCTAssertEqual(object["final"] as? Bool, true)
225 XCTAssertEqual(object["seconds"] as? Double, 30)
226 XCTAssertEqual((object["client"] as? [String: String])?["app"], "mac")
227 XCTAssertEqual((object["devices"] as? [String: String])?["input"], "bluetooth")
228 let peer = try XCTUnwrap((object["peers"] as? [[String: Any]])?.first)
229 XCTAssertNotNil(peer["in"], "\"in\" is the key, as the schema spells it")
230 XCTAssertNotNil((peer["path"] as? [String: Any])?["protocol"])
231 XCTAssertEqual((peer["events"] as? [String: Any])?["reconnecting"] as? Double, 1)
232 XCTAssertNil(object["isFinal"])
233 }
234
235 @MainActor func testTheClientIsDigitsAndDots() {
236 XCTAssertEqual(QualityClientInfo.version("0.4.8"), "0.4.8")
237 XCTAssertNil(QualityClientInfo.version("0.4.8 (dev)"))
238 XCTAssertNil(QualityClientInfo.version("dev"))
239 XCTAssertNil(QualityClientInfo.version("1..2"))
240 XCTAssertNil(QualityClientInfo.version(nil))
241 XCTAssertEqual(
242 QualityClientInfo.osVersion(OperatingSystemVersion(majorVersion: 26, minorVersion: 0, patchVersion: 1)), "26.0.1")
243 let current = QualityClientInfo.current()
244 XCTAssertEqual(current.app, "mac")
245 XCTAssertEqual(current.os, "macos")
246 XCTAssertNotNil(current.osVersion)
247 }
248
249 // MARK: - Notices
250
251 func testNoticesAreCountedWhenTheyBeginAndTimedWhileTheyShow() {
252 var tally = QualityEventTally()
253 tally.observe(at: 0, call: false, reconnecting: [], cantHear: ["b"])
254 tally.observe(at: 0.2, call: true, reconnecting: ["a"], cantHear: ["b"], noSound: ["a"])
255 tally.observe(at: 0.4, call: true, reconnecting: ["a"], cantHear: [])
256 // A gap of a minute (the Mac asleep) counts as no more than a second.
257 tally.observe(at: 60.4, call: false, reconnecting: [], cantHear: [], noSoundFromUs: ["c"], audioBlocked: ["c"])
258 tally.count(.keyframe, for: "a")
259 tally.count(.reattach, for: "c")
260 var builder = QualityReportBuilder()
261 builder.start(at: 0)
262 var mutable = tally
263 let report = builder.build(
264 at: 61, samples: [:], events: &mutable, client: Self.client, devices: nil, final: false)
265 XCTAssertEqual(report.events?.reconnecting, 1)
266 // Each look counts the time since the one before for what it sees.
267 XCTAssertEqual(report.events?.reconnectingSeconds, 0.4)
268 let peers = Dictionary(uniqueKeysWithValues: (report.peers ?? []).map { ($0.peer, $0) })
269 XCTAssertEqual(peers["a"]?.events?.reconnecting, 1)
270 XCTAssertEqual(peers["a"]?.events?.reconnectingSeconds, 0.4)
271 XCTAssertEqual(peers["a"]?.events?.noSound, true)
272 XCTAssertEqual(peers["a"]?.out?.screen?.keyframes, 1)
273 XCTAssertEqual(peers["b"]?.events?.cantHearSeconds, 0.2)
274 XCTAssertEqual(peers["c"]?.events?.noSoundFromUs, true)
275 XCTAssertEqual(peers["c"]?.events?.audioBlocked, true)
276 XCTAssertEqual(peers["c"]?.in?.screen?.reattaches, 1)
277 XCTAssertNil(peers["b"]?.events?.noSound)
278 }
279
280 func testANoticeStillShowingIsNotCountedAgainInTheNextWindow() {
281 var tally = QualityEventTally()
282 tally.observe(at: 0, call: true, reconnecting: ["a"], cantHear: [])
283 tally.observe(at: 1, call: true, reconnecting: ["a"], cantHear: [])
284 var builder = QualityReportBuilder()
285 builder.start(at: 0)
286 _ = builder.build(at: 1, samples: [:], events: &tally, client: Self.client, devices: nil, final: false)
287 tally.observe(at: 2, call: true, reconnecting: ["a"], cantHear: [])
288 let report = builder.build(at: 2, samples: [:], events: &tally, client: Self.client, devices: nil, final: false)
289 XCTAssertNil(report.events?.reconnecting, "the same notice, carried over")
290 XCTAssertEqual(report.events?.reconnectingSeconds, 1)
291 XCTAssertNil(report.peers?.first?.events?.reconnecting)
292 XCTAssertEqual(report.peers?.first?.events?.reconnectingSeconds, 1)
293 }
294
295 // MARK: - Devices
296
297 func testDeviceKindsFromCoreAudioTransports() {
298 XCTAssertEqual(DeviceKind.mac(transport: "built-in"), "built-in")
299 XCTAssertEqual(DeviceKind.mac(transport: "bluetooth"), "bluetooth")
300 XCTAssertEqual(DeviceKind.mac(transport: "usb"), "usb")
301 XCTAssertEqual(DeviceKind.mac(transport: "airplay"), "airplay")
302 XCTAssertEqual(DeviceKind.mac(transport: "virtual"), "virtual")
303 XCTAssertEqual(DeviceKind.mac(transport: "aggregate"), "virtual")
304 XCTAssertEqual(DeviceKind.mac(transport: "continuity"), "continuity")
305 XCTAssertEqual(DeviceKind.mac(transport: "thunderbolt"), "other")
306 XCTAssertEqual(DeviceKind.mac(transport: "0x12345678"), "other")
307 XCTAssertEqual(DeviceKind.mac(transport: nil), "unknown")
308 }
309
310 func testDeviceKindsFromAudioSessionPorts() {
311 // AVAudioSession.Port's raw values.
312 for (port, kind) in [
313 ("MicrophoneBuiltIn", "built-in"), ("Speaker", "built-in"), ("Receiver", "built-in"),
314 ("BluetoothHFP", "bluetooth"), ("BluetoothA2DPOutput", "bluetooth"), ("BluetoothLEOutput", "bluetooth"),
315 ("USBAudio", "usb"), ("Headphones", "wired"), ("MicrophoneWired", "wired"), ("AirPlay", "airplay"),
316 ("CarAudio", "other"), ("LineIn", "other"),
317 ] {
318 XCTAssertEqual(DeviceKind.ios(port: port), kind, port)
319 }
320 XCTAssertEqual(DeviceKind.ios(port: nil), "none")
321 }
322
323 func testEveryKindIsOneTheSchemaAllows() {
324 let allowed: Set = ["built-in", "bluetooth", "usb", "wired", "airplay", "virtual", "continuity", "other", "none", "unknown"]
325 for t in ["built-in", "bluetooth", "usb", "airplay", "virtual", "aggregate", "continuity", "display", "", "x"] {
326 XCTAssertTrue(allowed.contains(DeviceKind.mac(transport: t)))
327 }
328 for p in ["Speaker", "BluetoothHFP", "USBAudio", "Headphones", "AirPlay", "CarAudio", "", "x"] {
329 XCTAssertTrue(allowed.contains(DeviceKind.ios(port: p)))
330 }
331 }
332
333 // MARK: - Report a problem
334
335 static let roster = #"""
336 {"phase":"admitted","peer":"me0000aaaa","boot":"b","waiting":[],"peers":[
337 {"peer":"me0000aaaa","name":"Paul Campbell","kind":"member","gen":1},
338 {"peer":"abc123zzzz","name":"Vicky Wallace","kind":"member","gen":2,
339 "media":{"muted":true,"cameraOff":false,"noSoundFrom":["me0000aaaa"],"hearing":[]}}
340 ]}
341 """#
342
343 @MainActor func testTheAnonymousDiagnosticsNameNobody() {
344 let call = Call(playAudio: false)
345 call.showFixture(title: "Planning with Vicky", state: Self.roster)
346 let named = call.audioDiagnosticsText()
347 XCTAssertTrue(named.contains("Vicky Wallace: "), "Copy call diagnostics still names people")
348 let anonymous = call.anonymousDiagnosticsText()
349 XCTAssertTrue(anonymous.contains("peer abc123: "), anonymous)
350 XCTAssertTrue(anonymous.contains("muted"))
351 XCTAssertTrue(anonymous.contains("hears no sound from you"))
352 for forbidden in ["Vicky", "Wallace", "Paul Campbell", "Planning"] {
353 XCTAssertFalse(anonymous.contains(forbidden), "the report names \(forbidden)")
354 }
355 }
356
357 func testTheBridgesAnswers() {
358 XCTAssertEqual(ProblemOutcome(bridgeAnswer: #"{"ref":"K7Q2M9PD"}"#), .sent(ref: "K7Q2M9PD"))
359 XCTAssertEqual(ProblemOutcome(bridgeAnswer: #"{"outcome":"too-many"}"#), .tooMany)
360 XCTAssertEqual(ProblemOutcome(bridgeAnswer: #"{"outcome":"gone"}"#), .gone)
361 XCTAssertEqual(ProblemOutcome(bridgeAnswer: #"{"outcome":"failed"}"#), .failed)
362 XCTAssertEqual(ProblemOutcome(bridgeAnswer: "nonsense"), .failed)
363 XCTAssertEqual(ProblemOutcome(bridgeAnswer: #"{"ref":""}"#), .failed)
364 }
365
366 @MainActor func testTheSheetSendsAndShowsTheReference() async {
367 let sent = Box()
368 let model = ProblemReportModel { note in
369 await sent.set(note)
370 return .sent(ref: "K7Q2M9PD")
371 }
372 XCTAssertEqual(model.phase, .editing)
373 XCTAssertTrue(model.canSend, "the note is optional")
374 model.note = "Vicky sounded robotic"
375 XCTAssertEqual(model.counter, "21/1000")
376 await model.send()
377 XCTAssertEqual(model.phase, .sent(ref: "K7Q2M9PD"))
378 XCTAssertFalse(model.canSend, "sent once")
379 let note = await sent.value
380 XCTAssertEqual(note, "Vicky sounded robotic")
381 XCTAssertEqual(ProblemWords.reference("K7Q2M9PD"), "Reference K7Q2M9PD")
382 }
383
384 @MainActor func testTheSheetSaysWhyNotAndCanTryAgain() async {
385 let answers = Answers([.tooMany, .gone, .failed, .sent(ref: "ABCDEFGH")])
386 let model = ProblemReportModel { _ in await answers.next() }
387 await model.send()
388 XCTAssertEqual(model.phase, .refused("Too many reports from this call — try again in a minute."))
389 XCTAssertTrue(model.canSend)
390 await model.send()
391 XCTAssertEqual(model.phase, .refused("This call can no longer be reported."))
392 await model.send()
393 XCTAssertEqual(model.phase, .refused("The report could not be sent."))
394 await model.send()
395 XCTAssertEqual(model.phase, .sent(ref: "ABCDEFGH"))
396 }
397
398 @MainActor func testTheNoteIsAThousandCharactersAsTheServerCountsThem() {
399 let model = ProblemReportModel { _ in .failed }
400 model.note = String(repeating: "é", count: 1200)
401 XCTAssertEqual(model.note.unicodeScalars.count, 1000)
402 XCTAssertEqual(model.counter, "1000/1000")
403 }
404
405 func testTheWordsAreTheRulings() {
406 XCTAssertEqual(
407 ProblemWords.disclosure, "Sends this call's connection diagnostics to the Calls server log. No audio or video.")
408 XCTAssertEqual(ProblemWords.sent, "Sent — thank you")
409 XCTAssertEqual(
410 QualityReporting.disclosure, "Calls sends anonymous connection quality reports to help fix call problems.")
411 }
412
413 func testAProblemReportFitsTheServersLimit() throws {
414 let body = ProblemReportBody(
415 note: "x", diagnostics: String(repeating: "line of diagnostics\n", count: 10_000), client: Self.client
416 ).encoded()
417 XCTAssertLessThanOrEqual(body.utf8.count, QualityBounds.problemBody)
418 let object = try XCTUnwrap(JSONSerialization.jsonObject(with: Data(body.utf8)) as? [String: Any])
419 XCTAssertEqual(object["note"] as? String, "x")
420 XCTAssertTrue((object["diagnostics"] as? String)?.hasPrefix("(earlier lines cut)") == true)
421 }
422
423 func testTheReportingSwitchIsOnUnlessTurnedOff() throws {
424 let defaults = try XCTUnwrap(UserDefaults(suiteName: "quality-\(UUID().uuidString)"))
425 XCTAssertTrue(QualityReporting.enabled(defaults))
426 defaults.set(false, forKey: QualityReporting.enabledKey)
427 XCTAssertFalse(QualityReporting.enabled(defaults))
428 }
429
430 @MainActor func testAnEndedCallOffersReportAProblem() {
431 let ended = CallNotices.notices(
432 phase: "ended", status: "This meeting has ended", sharingScreen: false, screenNote: "", floor: false)
433 XCTAssertEqual(ended.first?.action, .reportProblem)
434 let call = Call(playAudio: false)
435 XCTAssertFalse(call.canReportProblem, "no call, nothing to report")
436 call.reportProblem()
437 XCTAssertNil(call.problemReport)
438 }
439}
440
441private actor Box {
442 var value = ""
443 func set(_ v: String) { value = v }
444}
445
446private actor Answers {
447 var queue: [ProblemOutcome]
448 init(_ queue: [ProblemOutcome]) { self.queue = queue }
449 func next() -> ProblemOutcome { queue.removeFirst() }
450}
451