Files

281 lines
11 KiB
Swift

import Testing
import Foundation
import AVFoundation
import CoreGraphics
@testable import TinySTT
struct TextTests {
@Test func punctuationAndWhitespace() {
#expect(TextPostProcessor.finalize("今天开会", addPeriod: true) == "今天开会。")
#expect(TextPostProcessor.finalize("已经结束!", addPeriod: true) == "已经结束!")
#expect(TextPostProcessor.finalize("他说“你好。”", addPeriod: true) == "他说“你好。”")
#expect(TextPostProcessor.finalize("保持 两个空格", addPeriod: false) == "保持 两个空格")
#expect(TextPostProcessor.finalize("嗯,那个,下午开会", addPeriod: false) == "嗯,那个,下午开会")
#expect(TextPostProcessor.finalize("\n \t", addPeriod: true).isEmpty)
}
@Test func shortcutRoundTrip() throws {
let spec = HotkeySpec.make(kind: .combo, keyCode: 49, modifiers: 1 << 19)
#expect(spec.label == "⌥Space")
#expect(try JSONDecoder().decode(HotkeySpec.self, from: JSONEncoder().encode(spec)) == spec)
#expect(HotkeySpec.rightOption.keyCode == 61)
#expect(HotkeySpec.modifierName(forKeyCode: 58) == "左 Option")
}
@Test func audioLevelAndConversion() throws {
let source = AVAudioFormat(standardFormatWithSampleRate: 48000, channels: 1)!
let target = AVAudioFormat(standardFormatWithSampleRate: 16000, channels: 1)!
let buffer = AVAudioPCMBuffer(pcmFormat: source, frameCapacity: 1024)!
buffer.frameLength = 1024
buffer.floatChannelData![0].initialize(repeating: 0, count: 1024)
#expect(AudioMath.level(of: buffer) == 0)
buffer.floatChannelData![0].update(repeating: 0.2, count: 1024)
#expect(AudioMath.level(of: buffer) > 0.5)
let converted = try #require(AudioMath.convert(buffer, using: AVAudioConverter(from: source, to: target)!, to: target))
#expect(converted.frameLength > 0)
#expect(converted.format.sampleRate == 16000)
}
@Test func timeoutDoesNotWaitForUncooperativeTask() async {
let start = Date()
let value: Int? = await withTimeout(0.02) {
await withCheckedContinuation { continuation in
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { continuation.resume(returning: 42) }
}
}
#expect(value == nil)
#expect(Date().timeIntervalSince(start) < 0.3)
#expect(await withTimeout(1) { 7 } == 7)
}
}
@MainActor
private final class FakeSession: RecognitionSession {
var onTextChanged: ((String) -> Void)?
var onAudioLevel: ((Float) -> Void)?
var onFailure: ((String) -> Void)?
var cancelled = false
var finishDelay = false
var failStart = false
var authorizationDelay = false
var started = false
var result = "测试"
func authorize() async throws {
if authorizationDelay { try? await Task.sleep(for: .milliseconds(100)) }
}
func start() throws {
started = true
if failStart { throw NSError(domain: "test", code: 1) }
}
func finish() async -> String {
if finishDelay { try? await Task.sleep(for: .milliseconds(80)) }
return result
}
func cancel() async { cancelled = true }
}
@Suite(.serialized)
@MainActor
struct SessionTests {
private func settle() async { try? await Task.sleep(for: .milliseconds(30)) }
@Test func cancelWhileStartingThenRestart() async {
let old = FakeSession()
let fresh = FakeSession()
var calls = 0
let controller = DictationController(acquire: {
calls += 1
if calls == 1 { try? await Task.sleep(for: .milliseconds(100)); return old }
return fresh
}, microphone: { true }, secureInput: { false }, replenish: {})
controller.testMode = true
controller.start()
await settle()
#expect(controller.state == .starting)
controller.stop() // release before preparation finishes
#expect(controller.state == .idle)
controller.testMode = true
controller.start()
await settle()
#expect(controller.state == .recording)
try? await Task.sleep(for: .milliseconds(120))
#expect(old.cancelled)
#expect(controller.state == .recording)
controller.cancel()
}
@Test func cancelDuringFinalizationDiscardsResult() async {
let fake = FakeSession()
fake.finishDelay = true
let controller = DictationController(acquire: { fake }, microphone: { true }, secureInput: { false }, replenish: {})
controller.testMode = true
controller.start()
await settle()
#expect(controller.state == .recording)
controller.stop()
#expect(controller.state == .finalizing)
controller.cancel()
try? await Task.sleep(for: .milliseconds(150))
#expect(controller.state == .idle)
#expect(controller.lastResult.isEmpty)
#expect(!controller.testMode)
}
@Test func testModeAndStartFailureCleanup() async {
let fake = FakeSession()
fake.failStart = true
let controller = DictationController(acquire: { fake }, microphone: { true }, secureInput: { false }, replenish: {})
controller.testMode = true
controller.start()
await settle()
#expect(controller.state == .idle)
#expect(fake.cancelled)
#expect(!controller.testMode)
}
@Test func successfulTestKeepsTextWithoutInsertion() async {
let fake = FakeSession()
let controller = DictationController(acquire: { fake }, microphone: { true }, secureInput: { false }, replenish: {})
controller.testMode = true
controller.start()
await settle()
try? await Task.sleep(for: .milliseconds(400))
controller.stop()
await settle()
#expect(controller.state == .idle)
#expect(controller.lastResult == "测试。")
#expect(!controller.testMode)
}
@Test func emptyTestResultReportsFailure() async {
let fake = FakeSession()
fake.result = ""
let controller = DictationController(acquire: { fake }, microphone: { true }, secureInput: { false }, replenish: {})
var error: String?
controller.onError = { error = $0 }
controller.testMode = true
controller.start()
await settle()
try? await Task.sleep(for: .milliseconds(400))
controller.stop()
await settle()
#expect(controller.state == .idle)
#expect(controller.lastResult.isEmpty)
#expect(error?.contains("测试未识别到文字") == true)
}
@Test func cancellationWhileAuthorizingNeverStartsMicrophone() async {
let fake = FakeSession()
fake.authorizationDelay = true
let controller = DictationController(acquire: { fake }, microphone: { true }, secureInput: { false }, replenish: {})
controller.testMode = true
controller.start()
await settle()
#expect(controller.state == .starting)
#expect(!fake.started)
controller.cancel()
try? await Task.sleep(for: .milliseconds(130))
#expect(controller.state == .idle)
#expect(!fake.started)
#expect(fake.cancelled)
}
@Test func microphoneDenialDoesNotAcquireSession() async {
var acquired = false
let controller = DictationController(acquire: { acquired = true; return FakeSession() }, microphone: { false }, secureInput: { false }, replenish: {})
controller.testMode = true
controller.start()
await settle()
#expect(!acquired)
#expect(controller.state == .idle)
#expect(!controller.testMode)
}
@Test func recognitionFailureCancelsWithoutInsertion() async {
let fake = FakeSession()
let controller = DictationController(acquire: { fake }, microphone: { true }, secureInput: { false }, replenish: {})
var error: String?
controller.onError = { error = $0 }
controller.testMode = true
controller.start()
await settle()
fake.onFailure?("model stopped")
await settle()
#expect(controller.state == .idle)
#expect(fake.cancelled)
#expect(error?.contains("model stopped") == true)
#expect(controller.lastResult.isEmpty)
}
@Test func secureInputNeverStartsCapture() {
var acquired = false
let controller = DictationController(acquire: { acquired = true; return FakeSession() }, microphone: { true }, secureInput: { true }, replenish: {})
controller.start()
#expect(controller.state == .idle)
#expect(!acquired)
}
}
@MainActor
struct HotkeyEventTests {
@Test func rightOptionUsesEventSnapshotAndReleasesWithLeftHeld() async throws {
let monitor = HotkeyMonitor(spec: .rightOption, secureInput: { false })
var pressed = 0
var released = 0
monitor.handler = { event in
switch event {
case .pressed: pressed += 1
case .released: released += 1
default: break
}
}
let event = try #require(CGEvent(keyboardEventSource: nil, virtualKey: 61, keyDown: true))
event.flags = CGEventFlags(rawValue: CGEventFlags.maskAlternate.rawValue | 0x40)
monitor.handle(type: .flagsChanged, event: event)
monitor.handle(type: .flagsChanged, event: event)
try? await Task.sleep(for: .milliseconds(20))
#expect(pressed == 1)
event.flags = CGEventFlags(rawValue: CGEventFlags.maskAlternate.rawValue | 0x20)
monitor.handle(type: .flagsChanged, event: event)
try? await Task.sleep(for: .milliseconds(20))
#expect(released == 1)
event.flags = []
monitor.handle(type: .flagsChanged, event: event)
try? await Task.sleep(for: .milliseconds(20))
#expect(released == 1)
}
}
struct TranscriptTests {
@Test func finalWithShiftedStartReplacesProvisionalText() {
var transcript = TranscriptAccumulator()
transcript.apply(start: 0.011, end: 3, text: "今天开会", final: false)
transcript.apply(start: 0, end: 3.1, text: "今天开会。", final: true)
#expect(transcript.text == "今天开会。")
transcript.apply(start: 0.011, end: 3, text: "今天开会", final: false)
#expect(transcript.text == "今天开会。")
}
@Test func separateSegmentsAndRevisionsRemainInOrder() {
var transcript = TranscriptAccumulator()
transcript.apply(start: 0, end: 1, text: "第一句。", final: true)
transcript.apply(start: 1, end: 2, text: "第二", final: false)
transcript.apply(start: 2, end: 3, text: "第三", final: false)
transcript.apply(start: 1.01, end: 2, text: "第二句。", final: true)
#expect(transcript.text == "第一句。第二句。第三")
transcript.apply(start: 2, end: 3, text: "第三句。", final: true)
#expect(transcript.text == "第一句。第二句。第三句。")
}
}
struct UnicodeInputTests {
@Test func chunksPreserveChineseEmojiAndLongInput() {
let text = String(repeating: "中文🙂é", count: 30) + "结束。"
let chunks = Inserter.unicodeChunks(text)
#expect(chunks.allSatisfy { !$0.isEmpty && $0.count <= 20 })
#expect(chunks.flatMap { $0 } == Array(text.utf16))
#expect(chunks.map { String(decoding: $0, as: UTF16.self) }.joined() == text)
#expect(Inserter.unicodeChunks("").isEmpty)
}
}