oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2import XCTest
3
4@testable import CallsKit
5
6private func reading(_ rms: Float, buffers: Int = 10) -> CaptureLevel.Reading {
7 CaptureLevel.Reading(rms: rms, peak: rms, buffers: buffers, at: 0)
8}
9private func dbfs(_ db: Float) -> Float { pow(10, db / 20) }
10
11/// The microphone verdict: working on the processed level, silent on the
12/// level before the suppressor, over a five-second window.
13final class MicJudgeTests: XCTestCase {
14 func testSpeechAfterTheSuppressorIsWorking() {
15 var judge = MicJudge()
16 judge.start(at: 0)
17 XCTAssertEqual(judge.observe(input: reading(0), output: reading(0), at: 0.1), .listening)
18 XCTAssertEqual(
19 judge.observe(input: reading(dbfs(-30)), output: reading(dbfs(-40)), at: 1), .working)
20 // Stays working between words.
21 XCTAssertEqual(
22 judge.observe(input: reading(dbfs(-60)), output: reading(dbfs(-80)), at: 3), .working)
23 }
24
25 func testFiveSecondsOfNothingBeforeTheSuppressorIsSilent() {
26 var judge = MicJudge()
27 judge.start(at: 0)
28 XCTAssertEqual(judge.observe(input: reading(dbfs(-100)), output: reading(0), at: 4.9), .listening)
29 XCTAssertEqual(judge.observe(input: reading(dbfs(-100)), output: reading(0), at: 5), .silent)
30 XCTAssertEqual(
31 MicJudge.sentence(.silent, device: "Scarlett 18i8 USB"), "Sound is arriving from Scarlett 18i8 USB, but it is silent.")
32 XCTAssertNotNil(MicJudge.advice(.silent))
33 }
34
35 /// Nothing arriving at all is not the same as silence arriving, and is
36 /// said differently: up to 0.3.7 both were "We can't hear anything",
37 /// and Paul's AirPods could not tell which (2026-09-26).
38 func testNoBuffersAtAllIsNoAudioNotSilent() {
39 var judge = MicJudge()
40 judge.start(at: 0)
41 XCTAssertEqual(judge.observe(input: reading(0, buffers: 0), output: reading(0, buffers: 0), at: 4.9), .listening)
42 XCTAssertEqual(judge.observe(input: reading(0, buffers: 0), output: reading(0, buffers: 0), at: 6), .noAudio)
43 XCTAssertEqual(
44 MicJudge.sentence(.noAudio, device: "Paul’s AirPods Pro (Bluetooth, 24 kHz)"),
45 "No sound is arriving from Paul’s AirPods Pro (Bluetooth, 24 kHz). Calls asked for it and nothing came back.")
46 XCTAssertNotNil(MicJudge.advice(.noAudio))
47 // Buffers of silence arriving later are the device's silence.
48 XCTAssertEqual(judge.observe(input: reading(0), output: reading(0), at: 12), .silent)
49 }
50
51 /// A quiet room DeepFilterNet has taken down 30 dB: dead after the
52 /// suppressor, alive before it. Not "can't hear anything".
53 func testAQuietRoomPulledDownByTheSuppressorIsNotSilent() {
54 var judge = MicJudge()
55 judge.start(at: 0)
56 for t in stride(from: 0.1, through: 8, by: 0.1) {
57 judge.observe(input: reading(dbfs(-65)), output: reading(dbfs(-95)), at: t)
58 }
59 XCTAssertEqual(judge.verdict, .hearing)
60 XCTAssertTrue(MicJudge.sentence(.hearing, device: "MacBook Pro Microphone").contains("MacBook Pro Microphone"))
61 }
62
63 func testSpeechBelowTheLineIsHearingNotWorking() {
64 var judge = MicJudge()
65 judge.start(at: 0)
66 XCTAssertEqual(judge.observe(input: reading(dbfs(-50)), output: reading(dbfs(-50)), at: 1), .hearing)
67 }
68
69 func testWorkingThatGoesDeadIsSaidAndComesBack() {
70 var judge = MicJudge()
71 judge.start(at: 0)
72 judge.observe(input: reading(dbfs(-20)), output: reading(dbfs(-20)), at: 1)
73 XCTAssertEqual(judge.observe(input: reading(0), output: reading(0), at: 5.9), .working)
74 XCTAssertEqual(judge.observe(input: reading(0), output: reading(0), at: 6), .silent)
75 XCTAssertEqual(judge.observe(input: reading(dbfs(-60)), output: reading(dbfs(-70)), at: 7), .hearing)
76 XCTAssertEqual(judge.observe(input: reading(dbfs(-20)), output: reading(dbfs(-20)), at: 8), .working)
77 }
78
79 /// In a call alone, WebRTC has not opened the microphone: no audio is
80 /// not the device's fault and is not said to be.
81 func testACallThatHasNotStartedTheMicrophoneIsNotCalledSilent() {
82 var judge = MicJudge()
83 judge.start(at: 0)
84 XCTAssertEqual(
85 judge.observe(input: reading(0, buffers: 0), output: reading(0, buffers: 0), at: 6, captureExpected: false),
86 .notStarted)
87 // Buffers of silence, though, are the device.
88 XCTAssertEqual(
89 judge.observe(input: reading(0), output: reading(0), at: 12, captureExpected: false), .silent)
90 }
91
92 func testTimeSpentPlayingDoesNotCountAsSilence() {
93 var judge = MicJudge()
94 judge.start(at: 0)
95 judge.observe(input: reading(dbfs(-20)), output: reading(dbfs(-20)), at: 1)
96 judge.resume(after: 10) // the test played for ten seconds
97 XCTAssertEqual(judge.observe(input: reading(0), output: reading(0), at: 12), .working)
98 }
99}
100
101final class MeterStepsTests: XCTestCase {
102 func testStepsAreFiveDecibelsFromMinusSixty() {
103 XCTAssertEqual(MeterSteps.step(0), 0)
104 XCTAssertEqual(MeterSteps.step(dbfs(-70)), 0)
105 XCTAssertEqual(MeterSteps.step(dbfs(-60)), 1)
106 XCTAssertEqual(MeterSteps.step(dbfs(-26)), 7)
107 XCTAssertEqual(MeterSteps.step(1), MeterSteps.count)
108 XCTAssertEqual(MeterSteps.words(0), "silent")
109 XCTAssertEqual(MeterSteps.words(7), "speaking level")
110 }
111}
112
113final class SelfTestRecorderTests: XCTestCase {
114 private func feed(_ recorder: SelfTestRecorder, value: Float, buffers: Int, frames: Int = 480, rate: Int = 48_000) {
115 let buffer = [Float](repeating: value, count: frames)
116 for _ in 0..<buffers {
117 buffer.withUnsafeBufferPointer { recorder.append($0.baseAddress!, frames: frames, rate: rate) }
118 }
119 }
120
121 func testNothingIsKeptUntilArmed() {
122 let recorder = SelfTestRecorder(seconds: 0.1)
123 feed(recorder, value: 1000, buffers: 5)
124 XCTAssertEqual(recorder.recorded, 0)
125 }
126
127 func testRecordsItsLengthAndNoMore() {
128 let recorder = SelfTestRecorder(seconds: 0.1)
129 recorder.arm()
130 feed(recorder, value: 16_384, buffers: 25)
131 XCTAssertTrue(recorder.full)
132 let (samples, rate) = recorder.take()
133 XCTAssertEqual(rate, 48_000)
134 XCTAssertEqual(samples.count, 4_800)
135 XCTAssertEqual(samples.first, 0.5, "WebRTC's scale in, -1...1 out")
136 }
137
138 func testTakingEmptiesIt() {
139 let recorder = SelfTestRecorder(seconds: 0.1)
140 recorder.arm()
141 feed(recorder, value: 1000, buffers: 3)
142 _ = recorder.take()
143 XCTAssertEqual(recorder.recorded, 0)
144 XCTAssertTrue(recorder.take().samples.isEmpty)
145 feed(recorder, value: 1000, buffers: 3)
146 XCTAssertEqual(recorder.recorded, 0, "not armed again by taking")
147 }
148
149 func testDiscardThrowsItAway() {
150 let recorder = SelfTestRecorder(seconds: 0.1)
151 recorder.arm()
152 feed(recorder, value: 1000, buffers: 3)
153 recorder.discard()
154 XCTAssertEqual(recorder.recorded, 0)
155 XCTAssertTrue(recorder.take().samples.isEmpty)
156 }
157
158 func testARateChangeStartsAgain() {
159 let recorder = SelfTestRecorder(seconds: 0.1)
160 recorder.arm()
161 feed(recorder, value: 1000, buffers: 3, frames: 160, rate: 16_000)
162 feed(recorder, value: 1000, buffers: 2)
163 let (samples, rate) = recorder.take()
164 XCTAssertEqual(rate, 48_000)
165 XCTAssertEqual(samples.count, 960)
166 }
167}
168
169final class TestChimeTests: XCTestCase {
170 func testTheChimeIsAudibleNeverClipsAndEndsInSilence() {
171 let chime = TestChime.samples()
172 XCTAssertEqual(Double(chime.count) / TestChime.rate, 1.6, accuracy: 0.001)
173 XCTAssertLessThan(chime.map(abs).max()!, 0.9)
174 XCTAssertGreaterThan(SampleLevel.db(SampleLevel.rms(chime)), -30)
175 XCTAssertEqual(chime.last, 0)
176 }
177}
178
179// MARK: - The controller
180
181private final class PassThrough: NoiseSuppressor {
182 let name = "Pass"
183 let frameLength = 480
184 func process(_ frame: UnsafeMutablePointer<Float>) -> Float { 1 }
185}
186
187private final class TestClock: @unchecked Sendable {
188 var now = 1000.0
189}
190
191@MainActor private final class FakeCapture: AudioSelfTestCapture {
192 let denoiser = CaptureDenoiser(suppressor: PassThrough())
193 var microphoneName = "Scarlett 18i8 USB"
194 var microphoneDetail = ""
195 var speakerName = "MacBook Pro Speakers"
196 var speakerID: String? = "speakers"
197 var inCall = false
198 var captureExpected = true
199 var muted = false
200 var reduceNoise = true
201 var stopsForPlayback = false
202 var allowed = true
203 var starts = 0
204 var stops = 0
205 /// Set: `start` waits until it is released, as a start held up by a
206 /// Bluetooth headset's switch does.
207 var holdStart = false
208 var failStart: Error?
209 var switching: String?
210 /// Whether the microphone would be open now: started and not stopped
211 /// since.
212 var open = false
213 private var held: CheckedContinuation<Void, Never>?
214
215 init() { denoiser.audioProcessingInitialize(sampleRate: 48_000, channels: 1) }
216
217 func permitted() async -> Bool { allowed }
218 func switchingMicrophone() async -> String? { switching }
219 func start() async throws {
220 starts += 1
221 if holdStart { await withCheckedContinuation { held = $0 } }
222 if let failStart { throw failStart }
223 open = true
224 }
225 func stop() {
226 stops += 1
227 open = false
228 }
229 /// Lets a held start finish.
230 func release() {
231 held?.resume()
232 held = nil
233 }
234 var isHeld: Bool { held != nil }
235 func levels() -> (input: CaptureLevel.Reading, output: CaptureLevel.Reading) {
236 (denoiser.inputLevel.take(), denoiser.level.take())
237 }
238 func diagnostics() -> [(String, [String])] { [("Fake", ["fake device lines"])] }
239
240 /// One 10 ms buffer through the hook, as WebRTC hands it over.
241 func feed(_ amplitude: Float, rate: Int = 48_000) {
242 let frames = rate / 100
243 var samples = (0..<frames).map { amplitude * sin(Float($0) * 2 * .pi * 440 / Float(rate)) }
244 samples.withUnsafeMutableBufferPointer { b in
245 denoiser.handle(b.baseAddress!, frames: frames, channels: 1)
246 }
247 }
248}
249
250@MainActor private final class FakePlayer: AudioSelfTestPlayer {
251 var played: [[Float]] = []
252 var rates: [Double] = []
253 func play(_ samples: [Float], sampleRate: Double, speaker: String?, meter: OutputMeter) async throws {
254 played.append(samples)
255 rates.append(sampleRate)
256 samples.withUnsafeBufferPointer { meter.record($0.baseAddress!, count: $0.count) }
257 }
258 func stop() {}
259}
260
261@MainActor final class AudioSelfTestModelTests: XCTestCase {
262 private func make(
263 _ configure: (FakeCapture) -> Void = { _ in }, seconds: Double = 0.2
264 ) -> (AudioSelfTest, FakeCapture, FakePlayer, TestClock) {
265 let capture = FakeCapture()
266 configure(capture)
267 let player = FakePlayer()
268 let clock = TestClock()
269 let test = AudioSelfTest(
270 capture: capture, player: player, routes: nil, recordSeconds: seconds, clock: { clock.now })
271 return (test, capture, player, clock)
272 }
273
274 func testSpeechIsWorkingAndMoves() async {
275 let (test, capture, _, clock) = make()
276 await test.open()
277 defer { test.close() }
278 capture.feed(8_000)
279 clock.now += 0.1
280 test.tick()
281 XCTAssertEqual(test.micVerdict, .working)
282 XCTAssertEqual(test.micSentence, "Your microphone is working.")
283 XCTAssertGreaterThan(test.micStep, 0)
284 }
285
286 func testSilenceForFiveSecondsNamesTheDevice() async {
287 let (test, capture, _, clock) = make()
288 await test.open()
289 defer { test.close() }
290 for _ in 0..<50 {
291 capture.feed(0)
292 clock.now += 0.1
293 test.tick()
294 }
295 XCTAssertEqual(test.micVerdict, .silent)
296 XCTAssertEqual(test.micStep, 0)
297 XCTAssertEqual(test.micSentence, "Sound is arriving from Scarlett 18i8 USB, but it is silent.")
298 }
299
300 /// The verdict names the device the microphone really is, with its
301 /// connection and rate, and says when nothing arrives at all.
302 func testNoAudioNamesTheActualDeviceAndRate() async {
303 let (test, _, _, clock) = make { $0.microphoneDetail = "Paul’s AirPods Pro (Bluetooth, 24 kHz)" }
304 await test.open()
305 defer { test.close() }
306 for _ in 0..<60 {
307 clock.now += 0.1
308 test.tick()
309 }
310 XCTAssertEqual(test.micVerdict, .noAudio)
311 XCTAssertEqual(
312 test.micSentence,
313 "No sound is arriving from Paul’s AirPods Pro (Bluetooth, 24 kHz). Calls asked for it and nothing came back.")
314 }
315
316 /// A Bluetooth microphone at 24 kHz, which the suppressor passes through,
317 /// still moves the meter and is judged working.
318 func testATwentyFourKilohertzMicrophoneIsMeteredAndWorking() async {
319 let (test, capture, _, clock) = make { $0.denoiser.audioProcessingInitialize(sampleRate: 24_000, channels: 1) }
320 await test.open()
321 defer { test.close() }
322 capture.feed(8_000, rate: 24_000)
323 clock.now += 0.1
324 test.tick()
325 XCTAssertEqual(test.micVerdict, .working)
326 XCTAssertGreaterThan(test.micStep, 0)
327 }
328
329 /// "Copy audio diagnostics": names, verdict and levels, and no audio.
330 func testDiagnosticsSayWhatWasMeasuredAndHoldNoAudio() async {
331 let (test, capture, _, clock) = make { $0.microphoneDetail = "Paul’s AirPods Pro (Bluetooth, 24 kHz)" }
332 await test.open()
333 defer { test.close() }
334 capture.feed(8_000)
335 clock.now += 0.1
336 test.tick()
337 let text = test.diagnosticsText()
338 XCTAssertTrue(text.contains("Paul’s AirPods Pro (Bluetooth, 24 kHz)"), text)
339 XCTAssertTrue(text.contains("verdict working"), text)
340 XCTAssertTrue(text.contains("## Level history"), text)
341 XCTAssertTrue(text.contains("## Audio log (last 50 lines)"), text)
342 XCTAssertTrue(text.contains("fake device lines"), text)
343 XCTAssertTrue(text.contains("dBFS"), text)
344 XCTAssertLessThan(text.count, 40_000, "names and numbers, not samples")
345 }
346
347 func testNoPermissionSaysSoAndStartsNothing() async {
348 let (test, capture, _, _) = make { $0.allowed = false }
349 await test.open()
350 XCTAssertEqual(test.micProblem, AudioSelfTest.noPermission)
351 XCTAssertEqual(capture.starts, 0)
352 test.close()
353 XCTAssertEqual(capture.stops, 0)
354 }
355
356 func testTheSpeakerTestPlaysTheChimeThroughTheChosenSpeaker() async {
357 let (test, _, player, _) = make()
358 await test.open()
359 defer { test.close() }
360 await test.playTestSound()
361 XCTAssertEqual(test.speaker, .played)
362 XCTAssertEqual(player.played.count, 1)
363 XCTAssertEqual(player.played.first?.count, TestChime.samples().count)
364 XCTAssertTrue(test.speakerSentence?.contains("MacBook Pro Speakers") ?? false)
365 }
366
367 func testRecordingPlaysBackWhatWasSentAndKeepsNothing() async {
368 let (test, capture, player, _) = make()
369 await test.open()
370 defer { test.close() }
371 let recording = Task { await test.recordAndPlayBack() }
372 while test.recording != .recording { await Task.yield() }
373 for _ in 0..<30 { capture.feed(8_000) }
374 await recording.value
375 XCTAssertEqual(test.recording, .played(silent: false))
376 XCTAssertEqual(player.played.count, 1)
377 XCTAssertEqual(player.played.first?.count, 9_600)
378 XCTAssertEqual(player.rates.first, 48_000)
379 XCTAssertEqual(test.lastRecording?.seconds ?? 0, 0.2, accuracy: 0.001)
380 XCTAssertFalse(test.holdsRecording)
381 XCTAssertNil(capture.denoiser.sink, "the tap comes off with the recording")
382 }
383
384 func testASilentRecordingIsSaidToBe() async {
385 let (test, capture, _, _) = make()
386 await test.open()
387 defer { test.close() }
388 let recording = Task { await test.recordAndPlayBack() }
389 while test.recording != .recording { await Task.yield() }
390 for _ in 0..<30 { capture.feed(0) }
391 await recording.value
392 XCTAssertEqual(test.recording, .played(silent: true))
393 XCTAssertTrue(test.recordingSentence?.contains("Scarlett 18i8 USB") ?? false)
394 }
395
396 func testNothingRecordedFailsInsteadOfWaitingForever() async {
397 let (test, _, player, clock) = make()
398 await test.open()
399 defer { test.close() }
400 let recording = Task { await test.recordAndPlayBack() }
401 while test.recording != .recording { await Task.yield() }
402 clock.now += 10
403 await recording.value
404 guard case .failed = test.recording else { return XCTFail("\(test.recording)") }
405 XCTAssertTrue(player.played.isEmpty)
406 }
407
408 func testClosingMidRecordingThrowsItAway() async {
409 let (test, capture, player, _) = make()
410 await test.open()
411 let recording = Task { await test.recordAndPlayBack() }
412 while test.recording != .recording { await Task.yield() }
413 for _ in 0..<5 { capture.feed(8_000) }
414 test.close()
415 await recording.value
416 XCTAssertFalse(test.holdsRecording)
417 XCTAssertTrue(player.played.isEmpty)
418 XCTAssertNil(capture.denoiser.sink)
419 XCTAssertEqual(capture.stops, 1)
420 }
421
422 func testInACallNothingPlaysAndMutedIsSaid() async {
423 let (test, capture, player, clock) = make {
424 $0.inCall = true
425 $0.muted = true
426 }
427 await test.open()
428 defer { test.close() }
429 XCTAssertEqual(test.speaker, .unavailableInCall)
430 XCTAssertEqual(test.recording, .unavailableInCall)
431 await test.playTestSound()
432 await test.recordAndPlayBack()
433 XCTAssertTrue(player.played.isEmpty)
434 clock.now += 0.1
435 test.tick()
436 XCTAssertEqual(test.micSentence, AudioSelfTest.mutedInCall)
437 capture.muted = false
438 capture.feed(8_000)
439 test.tick()
440 XCTAssertEqual(test.micVerdict, .working)
441 }
442
443 func testMeterOnlyPublishesChanges() async {
444 let (test, capture, _, clock) = make()
445 await test.open()
446 defer { test.close() }
447 var published = 0
448 let watch = test.objectWillChange.sink { published += 1 }
449 defer { watch.cancel() }
450 capture.feed(8_000)
451 test.tick()
452 let first = published
453 for _ in 0..<10 {
454 capture.feed(8_000)
455 clock.now += 0.1
456 test.tick()
457 }
458 XCTAssertEqual(published, first, "the same level and verdict are not published again")
459 }
460
461 // MARK: The start, off the main thread
462
463 /// Waits (yielding the main actor) until `condition` holds.
464 private func until(_ condition: () -> Bool) async {
465 for _ in 0..<200 where !condition() { await Task.yield() }
466 }
467
468 /// The window opens at once and says it is starting; the verdict and
469 /// meter take over only once the microphone has started.
470 func testStartingIsSaidUntilTheMicrophoneHasStarted() async {
471 let (test, capture, _, clock) = make { $0.holdStart = true }
472 let opening = Task { await test.open() }
473 await until { capture.isHeld }
474 XCTAssertEqual(test.startPhase, .starting(switching: nil))
475 XCTAssertEqual(test.startingSentence, "Starting microphone…")
476 XCTAssertEqual(test.micSentence, "Starting microphone…")
477 XCTAssertNil(test.micAdvice)
478 capture.release()
479 await opening.value
480 XCTAssertEqual(test.startPhase, .running)
481 XCTAssertNil(test.startingSentence)
482 capture.feed(8_000)
483 clock.now += 0.1
484 test.tick()
485 XCTAssertEqual(test.micVerdict, .working)
486 test.close()
487 }
488
489 /// A Bluetooth headset is named while it switches.
490 func testABluetoothSwitchIsSaid() async {
491 let (test, capture, _, _) = make {
492 $0.holdStart = true
493 $0.switching = "Paul’s AirPods Pro"
494 }
495 let opening = Task { await test.open() }
496 await until { capture.isHeld }
497 XCTAssertEqual(test.startPhase, .starting(switching: "Paul’s AirPods Pro"))
498 XCTAssertEqual(test.startingSentence, "Switching Paul’s AirPods Pro to microphone mode…")
499 capture.release()
500 await opening.value
501 XCTAssertEqual(test.startPhase, .running)
502 test.close()
503 }
504
505 /// Closed while the microphone starts: the start, when it finishes, is
506 /// stopped at once, and nothing is left open or judged.
507 func testClosingMidStartLeavesNothingOpen() async {
508 let (test, capture, _, _) = make { $0.holdStart = true }
509 let opening = Task { await test.open() }
510 await until { capture.isHeld }
511 test.close()
512 XCTAssertEqual(test.startPhase, .cancelled)
513 capture.release()
514 await opening.value
515 XCTAssertFalse(capture.open, "the microphone the late start opened was closed")
516 XCTAssertEqual(capture.starts, 1)
517 XCTAssertGreaterThanOrEqual(capture.stops, 1)
518 XCTAssertEqual(test.startPhase, .cancelled)
519 XCTAssertNil(test.micProblem, "a cancelled start is not reported as a failure")
520 }
521
522 /// Closed while permission is being asked: nothing starts at all.
523 func testClosingWhileAskingStartsNothing() async {
524 let (test, capture, _, _) = make()
525 test.close()
526 await test.open()
527 XCTAssertEqual(capture.starts, 0)
528 XCTAssertEqual(test.startPhase, .cancelled)
529 }
530
531 func testAStartThatFailsSaysWhy() async {
532 let (test, capture, _, _) = make {
533 $0.failStart = NSError(domain: "t", code: 1, userInfo: [NSLocalizedDescriptionKey: "no device"])
534 }
535 await test.open()
536 XCTAssertEqual(test.startPhase, .failed)
537 XCTAssertEqual(test.micProblem, "The microphone would not start: no device")
538 XCTAssertFalse(capture.open)
539 test.close()
540 }
541
542 func testOpeningTwiceStartsOnce() async {
543 let (test, capture, _, _) = make()
544 await test.open()
545 await test.open()
546 XCTAssertEqual(capture.starts, 1)
547 test.close()
548 test.close()
549 XCTAssertEqual(capture.stops, 1)
550 }
551}
552
553/// The start's rules alone.
554final class MicStartTests: XCTestCase {
555 func testTheHappyPath() {
556 var start = MicStart()
557 XCTAssertTrue(start.open())
558 XCTAssertEqual(start.phase, .asking)
559 XCTAssertEqual(start.sentence, "Asking to use the microphone…")
560 XCTAssertTrue(start.permission(true))
561 XCTAssertEqual(start.sentence, "Starting microphone…")
562 start.microphone(bluetoothSwitching: "AirPods")
563 XCTAssertEqual(start.sentence, "Switching AirPods to microphone mode…")
564 XCTAssertEqual(start.finished(ok: true), .keep)
565 XCTAssertEqual(start.phase, .running)
566 XCTAssertNil(start.sentence)
567 XCTAssertTrue(start.close(), "a running microphone is stopped on close")
568 XCTAssertEqual(start.phase, .cancelled)
569 }
570
571 func testOpenOnlyOnce() {
572 var start = MicStart()
573 XCTAssertTrue(start.open())
574 XCTAssertFalse(start.open())
575 start.close()
576 XCTAssertFalse(start.open(), "a closed test does not open again")
577 }
578
579 func testNoPermission() {
580 var start = MicStart()
581 _ = start.open()
582 XCTAssertFalse(start.permission(false))
583 XCTAssertEqual(start.phase, .failed)
584 XCTAssertFalse(start.close(), "nothing to stop")
585 }
586
587 func testPermissionAfterClose() {
588 var start = MicStart()
589 _ = start.open()
590 XCTAssertFalse(start.close(), "nothing started yet")
591 XCTAssertFalse(start.permission(true), "the answer comes back to a closed window")
592 XCTAssertEqual(start.phase, .cancelled)
593 }
594
595 func testALateStartIsStopped() {
596 var start = MicStart()
597 _ = start.open()
598 _ = start.permission(true)
599 XCTAssertTrue(start.close(), "a start in flight may open the microphone")
600 XCTAssertEqual(start.finished(ok: true), .stop)
601 XCTAssertEqual(start.phase, .cancelled)
602 }
603
604 func testALateFailureIsQuiet() {
605 var start = MicStart()
606 _ = start.open()
607 _ = start.permission(true)
608 start.close()
609 XCTAssertEqual(start.finished(ok: false), .failed)
610 XCTAssertEqual(start.phase, .cancelled)
611 }
612
613 func testRestartAfterPlayback() {
614 var start = MicStart()
615 _ = start.open()
616 _ = start.permission(true)
617 _ = start.finished(ok: true)
618 XCTAssertTrue(start.restart())
619 XCTAssertTrue(start.isStarting)
620 XCTAssertEqual(start.finished(ok: true), .keep)
621 start.close()
622 XCTAssertFalse(start.restart(), "not after close")
623 }
624
625 func testTheBluetoothNameOnlyWhileStarting() {
626 var start = MicStart()
627 start.microphone(bluetoothSwitching: "AirPods")
628 XCTAssertEqual(start.phase, .idle)
629 }
630}
631