Major rework of the network code, splitting out the speech recognition aspects and creating separate threads for read and write. This improves handling of network connection closure by the other end and other network connectivity errors. Add some unit tests for the connection code.

This commit is contained in:
Jeremy Rand 2022-03-16 18:13:56 -04:00
parent 923d0bf967
commit d7c430f588
9 changed files with 576 additions and 51 deletions

View File

@ -8,6 +8,14 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
9D05BAAA27DFDE6300D9CC4B /* GSConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D05BAA927DFDE6300D9CC4B /* GSConnection.swift */; }; 9D05BAAA27DFDE6300D9CC4B /* GSConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D05BAA927DFDE6300D9CC4B /* GSConnection.swift */; };
9D2A6D1E27E235E400DF3D85 /* GSServerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D2A6D1D27E235E400DF3D85 /* GSServerMock.swift */; };
9D2A6D1F27E236D500DF3D85 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D51564C26A36B410075EBC7 /* Result.swift */; };
9D2A6D2027E236D800DF3D85 /* Socket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D51565026A36B410075EBC7 /* Socket.swift */; };
9D2A6D2127E236DC00DF3D85 /* TCPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D51565126A36B410075EBC7 /* TCPClient.swift */; };
9D2A6D2227E236E000DF3D85 /* UDPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D51564F26A36B410075EBC7 /* UDPClient.swift */; };
9D2A6D2327E236E400DF3D85 /* ytcpsocket.c in Sources */ = {isa = PBXBuildFile; fileRef = 9D51564E26A36B410075EBC7 /* ytcpsocket.c */; };
9D2A6D2427E236FD00DF3D85 /* yudpsocket.c in Sources */ = {isa = PBXBuildFile; fileRef = 9D51564D26A36B410075EBC7 /* yudpsocket.c */; };
9D2A6D2727E24BD600DF3D85 /* SpeechForwarderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D2A6D2627E24BD600DF3D85 /* SpeechForwarderMock.swift */; };
9D5155F326A1EF7B0075EBC7 /* ListenerGSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D5155F226A1EF7B0075EBC7 /* ListenerGSApp.swift */; }; 9D5155F326A1EF7B0075EBC7 /* ListenerGSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D5155F226A1EF7B0075EBC7 /* ListenerGSApp.swift */; };
9D5155F726A1EF7C0075EBC7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D5155F626A1EF7C0075EBC7 /* Assets.xcassets */; }; 9D5155F726A1EF7C0075EBC7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D5155F626A1EF7C0075EBC7 /* Assets.xcassets */; };
9D5155FA26A1EF7C0075EBC7 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D5155F926A1EF7C0075EBC7 /* Preview Assets.xcassets */; }; 9D5155FA26A1EF7C0075EBC7 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D5155F926A1EF7C0075EBC7 /* Preview Assets.xcassets */; };
@ -51,6 +59,8 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
9D05BAA927DFDE6300D9CC4B /* GSConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GSConnection.swift; sourceTree = "<group>"; }; 9D05BAA927DFDE6300D9CC4B /* GSConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GSConnection.swift; sourceTree = "<group>"; };
9D0DC15826F2E47A007EB92D /* ListenerGS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ListenerGS.entitlements; sourceTree = "<group>"; }; 9D0DC15826F2E47A007EB92D /* ListenerGS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ListenerGS.entitlements; sourceTree = "<group>"; };
9D2A6D1D27E235E400DF3D85 /* GSServerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GSServerMock.swift; sourceTree = "<group>"; };
9D2A6D2627E24BD600DF3D85 /* SpeechForwarderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechForwarderMock.swift; sourceTree = "<group>"; };
9D5155EF26A1EF7B0075EBC7 /* ListenerGS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ListenerGS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9D5155EF26A1EF7B0075EBC7 /* ListenerGS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ListenerGS.app; sourceTree = BUILT_PRODUCTS_DIR; };
9D5155F226A1EF7B0075EBC7 /* ListenerGSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListenerGSApp.swift; sourceTree = "<group>"; }; 9D5155F226A1EF7B0075EBC7 /* ListenerGSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListenerGSApp.swift; sourceTree = "<group>"; };
9D5155F626A1EF7C0075EBC7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 9D5155F626A1EF7C0075EBC7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -166,6 +176,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
9D51560426A1EF7C0075EBC7 /* ListenerGSTests.swift */, 9D51560426A1EF7C0075EBC7 /* ListenerGSTests.swift */,
9D2A6D1D27E235E400DF3D85 /* GSServerMock.swift */,
9D2A6D2627E24BD600DF3D85 /* SpeechForwarderMock.swift */,
9D51560626A1EF7C0075EBC7 /* Info.plist */, 9D51560626A1EF7C0075EBC7 /* Info.plist */,
); );
path = ListenerGSTests; path = ListenerGSTests;
@ -388,7 +400,15 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
9D2A6D2427E236FD00DF3D85 /* yudpsocket.c in Sources */,
9D2A6D2227E236E000DF3D85 /* UDPClient.swift in Sources */,
9D2A6D2027E236D800DF3D85 /* Socket.swift in Sources */,
9D2A6D2727E24BD600DF3D85 /* SpeechForwarderMock.swift in Sources */,
9D2A6D2127E236DC00DF3D85 /* TCPClient.swift in Sources */,
9D2A6D1F27E236D500DF3D85 /* Result.swift in Sources */,
9D2A6D1E27E235E400DF3D85 /* GSServerMock.swift in Sources */,
9D51560526A1EF7C0075EBC7 /* ListenerGSTests.swift in Sources */, 9D51560526A1EF7C0075EBC7 /* ListenerGSTests.swift in Sources */,
9D2A6D2327E236E400DF3D85 /* ytcpsocket.c in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -590,6 +610,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_ENTITLEMENTS = "";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = VD9FGCW36C; DEVELOPMENT_TEAM = VD9FGCW36C;
INFOPLIST_FILE = ListenerGSTests/Info.plist; INFOPLIST_FILE = ListenerGSTests/Info.plist;
@ -612,6 +633,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_ENTITLEMENTS = "";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = VD9FGCW36C; DEVELOPMENT_TEAM = VD9FGCW36C;
INFOPLIST_FILE = ListenerGSTests/Info.plist; INFOPLIST_FILE = ListenerGSTests/Info.plist;

View File

@ -42,7 +42,7 @@ extension GSConnectionState: CustomStringConvertible
protocol SpeechForwarderProtocol protocol SpeechForwarderProtocol
{ {
func startListening() -> Bool func startListening(connection: GSConnection) -> Bool
func stopListening() func stopListening()
} }
@ -53,11 +53,12 @@ class GSConnection : ObservableObject {
var speechForwarder : SpeechForwarderProtocol? var speechForwarder : SpeechForwarderProtocol?
let LISTEN_STATE_MSG = 1 static let LISTEN_STATE_MSG = 1
let LISTEN_TEXT_MSG = 2 static let LISTEN_TEXT_MSG = 2
let LISTEN_SEND_MORE = 3 static let LISTEN_SEND_MORE = 3
static let port = 19026
let port = 19026
private var destination = "" private var destination = ""
private var client: TCPClient? private var client: TCPClient?
@ -65,14 +66,15 @@ class GSConnection : ObservableObject {
private let readQueue = OperationQueue() private let readQueue = OperationQueue()
private let writeQueue = OperationQueue() private let writeQueue = OperationQueue()
private var mainQueue = OperationQueue.main
private var condition = NSCondition() private var condition = NSCondition()
private var stopListeningFlag = false
private var canSend = true private var canSend = true
func changeState(newState : GSConnectionState) private func changeState(newState : GSConnectionState)
{ {
if (state == newState) { let oldState = state
if (oldState == newState) {
return; return;
} }
@ -80,24 +82,24 @@ class GSConnection : ObservableObject {
switch (newState) switch (newState)
{ {
case .disconnected: case .disconnected:
legalTransition = ((state == .connected) || (state == .connecting)) legalTransition = ((oldState == .connected) || (oldState == .connecting) || (oldState == .stoplistening))
case .connecting: case .connecting:
legalTransition = (state == .disconnected) legalTransition = (oldState == .disconnected)
case .connected: case .connected:
legalTransition = ((state == .connecting) || (state == .listening) || (state == .stoplistening)) legalTransition = ((oldState == .connecting) || (oldState == .listening) || (oldState == .stoplistening))
case .listening: case .listening:
legalTransition = (state == .connected) legalTransition = (oldState == .connected)
case .stoplistening: case .stoplistening:
legalTransition = ((state == .connected) || (state == .listening)) legalTransition = ((oldState == .connected) || (oldState == .listening))
} }
if (!legalTransition) { if (!legalTransition) {
logger.error("Illegal requested state transition from \(self.state) to \(newState)") logger.error("Illegal requested state transition from \(oldState) to \(newState)")
errorOccurred(title: "Bad State Change", message: "Illegal state transition from \(self.state) to \(newState)") errorOccurred(title: "Bad State Change", message: "Illegal state transition from \(oldState) to \(newState)")
} else { } else {
state = newState state = newState
} }
@ -105,7 +107,7 @@ class GSConnection : ObservableObject {
func errorOccurred(title: String, message : String) func errorOccurred(title: String, message : String)
{ {
OperationQueue.main.addOperation { mainQueue.addOperation {
self.errorMessage = GSConnectionErrorMessage(title: title, message: message) self.errorMessage = GSConnectionErrorMessage(title: title, message: message)
} }
} }
@ -123,19 +125,19 @@ class GSConnection : ObservableObject {
private func doConnect() { private func doConnect() {
logger.debug("Attempting to connect to \(self.destination)") logger.debug("Attempting to connect to \(self.destination)")
client = TCPClient(address: destination, port: Int32(port)) client = TCPClient(address: destination, port: Int32(GSConnection.port))
guard let client = client else { guard let client = client else {
OperationQueue.main.addOperation { self.connectionFailed() } mainQueue.addOperation { self.connectionFailed() }
return return
} }
switch client.connect(timeout: 10) { switch client.connect(timeout: 10) {
case .success: case .success:
OperationQueue.main.addOperation { self.connectionSuccessful() } mainQueue.addOperation { self.connectionSuccessful() }
case .failure(let error): case .failure(let error):
client.close() client.close()
self.client = nil self.client = nil
logger.error("Failed to connect to \(self.destination): \(String(describing: error))") logger.error("Failed to connect to \(self.destination): \(String(describing: error))")
OperationQueue.main.addOperation { self.connectionFailed() } mainQueue.addOperation { self.connectionFailed() }
return return
} }
@ -143,10 +145,15 @@ class GSConnection : ObservableObject {
guard let byteArray = client.read(2) else { guard let byteArray = client.read(2) else {
break break
} }
if (byteArray.count != 2) {
break
}
let data = Data(byteArray) let data = Data(byteArray)
do { do {
let unpacked = try unpack("<h", data) let unpacked = try unpack("<h", data)
if (unpacked[0] as? Int == LISTEN_SEND_MORE) { if (unpacked[0] as? Int == GSConnection.LISTEN_SEND_MORE) {
condition.lock() condition.lock()
canSend = true canSend = true
condition.broadcast() condition.broadcast()
@ -166,7 +173,7 @@ class GSConnection : ObservableObject {
client.close() client.close()
self.client = nil self.client = nil
OperationQueue.main.addOperation { self.disconnect() } mainQueue.addOperation { self.disconnect() }
} }
func connect(destination : String) { func connect(destination : String) {
@ -193,6 +200,9 @@ class GSConnection : ObservableObject {
} }
condition.broadcast() condition.broadcast()
condition.unlock() condition.unlock()
waitForWriteQueue()
waitForReadQueue()
self.changeState(newState:.disconnected) self.changeState(newState:.disconnected)
} }
@ -213,7 +223,7 @@ class GSConnection : ObservableObject {
private func sendListenMsg(isListening: Bool) -> Bool { private func sendListenMsg(isListening: Bool) -> Bool {
guard let client = client else { return false } guard let client = client else { return false }
switch (client.send(data: pack("<hh", [LISTEN_STATE_MSG, isListening ? 1 : 0]))) { switch (client.send(data: pack("<hh", [GSConnection.LISTEN_STATE_MSG, isListening ? 1 : 0]))) {
case .success: case .success:
break break
case .failure(let error): case .failure(let error):
@ -232,9 +242,9 @@ class GSConnection : ObservableObject {
return return
} }
OperationQueue.main.addOperation { self.mainQueue.addOperation {
self.changeState(newState: .listening) self.changeState(newState: .listening)
if (!speechForwarder.startListening()) { if (!speechForwarder.startListening(connection: self)) {
self.logger.error("Unable to start listening") self.logger.error("Unable to start listening")
self.errorOccurred(title: "Speech Error", message: "Unable to start listening for speech") self.errorOccurred(title: "Speech Error", message: "Unable to start listening for speech")
self.stopListening() self.stopListening()
@ -247,7 +257,7 @@ class GSConnection : ObservableObject {
_ = self.sendListenMsg(isListening: false) _ = self.sendListenMsg(isListening: false)
OperationQueue.main.addOperation { self.mainQueue.addOperation {
if (self.state == .stoplistening) { if (self.state == .stoplistening) {
self.changeState(newState: .connected) self.changeState(newState: .connected)
} }
@ -318,29 +328,51 @@ class GSConnection : ObservableObject {
let nsEnc = CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringBuiltInEncodings.macRoman.rawValue)) let nsEnc = CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringBuiltInEncodings.macRoman.rawValue))
let encoding = String.Encoding(rawValue: nsEnc) // String.Encoding let encoding = String.Encoding(rawValue: nsEnc) // String.Encoding
if let bytes = stringToSend.data(using: encoding) { if let bytes = stringToSend.data(using: encoding) {
switch (client.send(data: pack("<hh", [LISTEN_TEXT_MSG, bytes.count]))) { switch (client.send(data: pack("<hh", [GSConnection.LISTEN_TEXT_MSG, bytes.count]))) {
case .success: case .success:
switch (client.send(data: bytes)) { switch (client.send(data: bytes)) {
case .success: case .success:
logger.debug("Sent text \"\(stringToSend)\"") logger.debug("Sent text \"\(stringToSend)\"")
break break
case .failure(let error): case .failure(let error):
OperationQueue.main.addOperation { mainQueue.addOperation {
self.errorOccurred(title: "Write Error", message: "Unable to send text to the GS") self.errorOccurred(title: "Write Error", message: "Unable to send text to the GS")
self.stopListening() self.disconnect()
} }
logger.error("Failed to send text: \(String(describing: error))") logger.error("Failed to send text: \(String(describing: error))")
return false return false
} }
case .failure(let error): case .failure(let error):
OperationQueue.main.addOperation { mainQueue.addOperation {
self.errorOccurred(title: "Write Error", message: "Unable to send text to the GS") self.errorOccurred(title: "Write Error", message: "Unable to send text to the GS")
self.stopListening() self.disconnect()
} }
logger.error("Failed to send text: \(String(describing: error))") logger.error("Failed to send text: \(String(describing: error))")
} }
} }
return true return true
} }
func setMainQueueForTest() {
mainQueue = OperationQueue()
}
func waitForMain() {
mainQueue.waitUntilAllOperationsAreFinished()
}
func waitForReadQueue() {
readQueue.waitUntilAllOperationsAreFinished()
}
func waitForWriteQueue() {
writeQueue.waitUntilAllOperationsAreFinished()
}
func waitForAllQueues() {
waitForWriteQueue()
waitForReadQueue()
waitForMain()
}
} }

View File

@ -74,7 +74,7 @@ struct GSView: View {
case .connected: case .connected:
Button("\(Image(systemName: "ear.and.waveform")) Listen and Send Text") { Button("\(Image(systemName: "ear.and.waveform")) Listen and Send Text") {
connection.listen(speechForwarder: SpeechForwarder(connection: connection)) connection.listen(speechForwarder: SpeechForwarder())
} }
.buttonStyle(GSButtonStyle()) .buttonStyle(GSButtonStyle())

View File

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0</string> <string>1.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>483</string> <string>548</string>
<key>LSApplicationCategoryType</key> <key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string> <string>public.app-category.utilities</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>

View File

@ -11,8 +11,6 @@ import Speech
class SpeechForwarder : SpeechForwarderProtocol { class SpeechForwarder : SpeechForwarderProtocol {
private var connection : GSConnection
private let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: Locale.preferredLanguages[0]))! private let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: Locale.preferredLanguages[0]))!
private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest? private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
@ -23,11 +21,7 @@ class SpeechForwarder : SpeechForwarderProtocol {
private let logger = Logger() private let logger = Logger()
init(connection : GSConnection) { func startListening(connection : GSConnection) -> Bool {
self.connection = connection
}
func startListening() -> Bool {
SFSpeechRecognizer.requestAuthorization { authStatus in SFSpeechRecognizer.requestAuthorization { authStatus in
OperationQueue.main.addOperation { OperationQueue.main.addOperation {
switch authStatus { switch authStatus {
@ -35,16 +29,16 @@ class SpeechForwarder : SpeechForwarderProtocol {
break break
case .denied, .restricted, .notDetermined: case .denied, .restricted, .notDetermined:
self.connection.stopListening() connection.stopListening()
default: default:
self.connection.stopListening() connection.stopListening()
} }
} }
} }
do { do {
try startRecording() try startRecording(connection: connection)
logger.debug("Started listening") logger.debug("Started listening")
} }
catch { catch {
@ -64,7 +58,7 @@ class SpeechForwarder : SpeechForwarderProtocol {
recognitionTask = nil recognitionTask = nil
} }
private func startRecording() throws { private func startRecording(connection : GSConnection) throws {
// Cancel the previous task if it's running. // Cancel the previous task if it's running.
recognitionTask?.cancel() recognitionTask?.cancel()
@ -95,18 +89,17 @@ class SpeechForwarder : SpeechForwarderProtocol {
if let result = result { if let result = result {
// Update the text view with the results. // Update the text view with the results.
OperationQueue.main.addOperation { self.connection.set(text: result.bestTranscription.formattedString) } OperationQueue.main.addOperation { connection.set(text: result.bestTranscription.formattedString) }
isFinal = result.isFinal isFinal = result.isFinal
} }
if error != nil { if error != nil {
self.logger.error("Error from recognizer: \(String(describing: error))") self.logger.error("Error from recognizer: \(String(describing: error))")
self.connection.errorOccurred(title: "Recognizer Error", message: "Speech recognizer failed with an error")
} }
if error != nil || isFinal { if error != nil || isFinal {
OperationQueue.main.addOperation { OperationQueue.main.addOperation {
self.connection.stopListening() connection.stopListening()
} }
} }
} }

View File

@ -0,0 +1,133 @@
//
// GSServerMock.swift
// ListenerGSTests
//
// Created by Jeremy Rand on 2022-03-16.
//
import Foundation
@testable import ListenerGS
class GSServerMock {
private let server = TCPServer(address: "127.0.0.1", port: Int32(GSConnection.port))
private var client : TCPClient?
deinit {
server.close()
disconnect()
}
func accept() -> Bool {
let result = server.listen()
if (!result.isSuccess) {
return false
}
client = server.accept(timeout: 10)
return (client != nil)
}
func hasClient() -> Bool {
return client != nil
}
func disconnect() {
if let client = client {
client.close()
self.client = nil
}
}
func getListenState(isListening : Bool) -> Bool {
guard let client = client else { return false }
guard let byteArray = client.read(4) else {
return false
}
if (byteArray.count != 4) {
return false
}
let data = Data(byteArray)
do {
let unpacked = try unpack("<hh", data)
if (unpacked[0] as? Int != GSConnection.LISTEN_STATE_MSG) {
return false
}
if (unpacked[1] as? Int != (isListening ? 1 : 0)) {
return false
}
return true
}
catch {
return false
}
}
func sendMore() -> Bool {
guard let client = client else { return false }
let result = client.send(data: pack("<h", [GSConnection.LISTEN_SEND_MORE]))
return result.isSuccess
}
func sendMoreBad() -> Bool {
guard let client = client else { return false }
let result = client.send(data: pack("<h", [6502]))
return result.isSuccess
}
func getText() -> String {
guard let client = client else { return "" }
guard let headerByteArray = client.read(4) else {
return ""
}
if (headerByteArray.count != 4) {
return ""
}
let headerData = Data(headerByteArray)
var textLength = 0
do {
let unpacked = try unpack("<hh", headerData)
if (unpacked[0] as? Int != GSConnection.LISTEN_TEXT_MSG) {
return ""
}
textLength = unpacked[1] as! Int
}
catch {
return ""
}
if (textLength == 0) {
return ""
}
guard let bodyByteArray = client.read(textLength) else {
return ""
}
if (bodyByteArray.count != textLength) {
return ""
}
let bodyData = Data(bodyByteArray)
let nsEnc = CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringBuiltInEncodings.macRoman.rawValue))
let encoding = String.Encoding(rawValue: nsEnc) // String.Encoding
let result = String(data:bodyData, encoding: encoding)
guard let result = result else { return "" }
return result
}
func getDisconnect() -> Bool {
guard let client = client else { return false }
guard let headerByteArray = client.read(1) else {
return true
}
return (headerByteArray.count == 0)
}
}

View File

@ -17,17 +17,335 @@ class ListenerGSTests: XCTestCase {
override func tearDownWithError() throws { override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class. // Put teardown code here. This method is called after the invocation of each test method in the class.
} }
func testExample() throws { func waitForConnection(connection: GSConnection) {
// This is an example of a functional test case. for _ in (1...1000) {
// Use XCTAssert and related functions to verify your tests produce the correct results. if (connection.state != .connecting) {
return
}
usleep(10000)
}
} }
func testNoConnection() throws {
let connection = GSConnection()
connection.setMainQueueForTest()
XCTAssertEqual(connection.state, .disconnected)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "")
connection.connect(destination: "127.0.0.1")
connection.waitForReadQueue()
connection.waitForMain()
XCTAssertEqual(connection.state, .disconnected)
XCTAssertNotNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "")
}
func testNormalPath() throws {
let connection = GSConnection()
connection.setMainQueueForTest()
XCTAssertEqual(connection.state, .disconnected)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "")
let server = GSServerMock()
connection.connect(destination: "127.0.0.1")
XCTAssertEqual(connection.state, .connecting)
XCTAssert(server.accept())
XCTAssert(server.hasClient())
connection.waitForMain()
waitForConnection(connection: connection)
XCTAssertEqual(connection.state, .connected)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "")
let speechForwarder = SpeechForwarderMock()
XCTAssert(!speechForwarder.isListening)
connection.listen(speechForwarder: speechForwarder)
XCTAssert(server.getListenState(isListening: true))
connection.waitForMain()
XCTAssert(speechForwarder.isListening)
XCTAssertEqual(connection.state, .listening)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "")
connection.set(text: "Hello, world!")
XCTAssert(speechForwarder.isListening)
XCTAssertEqual(connection.state, .listening)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "Hello, world!")
XCTAssertEqual(server.getText(), "Hello, world!")
connection.set(text: "Rewrite everything...")
XCTAssert(speechForwarder.isListening)
XCTAssertEqual(connection.state, .listening)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "Rewrite everything...")
connection.set(text: "Hello, everyone!")
connection.stopListening()
XCTAssert(!speechForwarder.isListening)
XCTAssertEqual(connection.state, .stoplistening)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "Hello, everyone!")
XCTAssert(server.sendMore())
XCTAssertEqual(server.getText(), "\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}\u{7f}everyone!")
connection.waitForWriteQueue()
connection.waitForMain()
XCTAssert(server.getListenState(isListening: false))
XCTAssertEqual(connection.state, .connected)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "Hello, everyone!")
server.disconnect()
connection.waitForReadQueue()
connection.waitForMain()
XCTAssertEqual(connection.state, .disconnected)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "Hello, everyone!")
}
func testDisconnectWhileListening() throws {
let connection = GSConnection()
connection.setMainQueueForTest()
XCTAssertEqual(connection.state, .disconnected)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "")
let server = GSServerMock()
connection.connect(destination: "127.0.0.1")
XCTAssertEqual(connection.state, .connecting)
XCTAssert(server.accept())
XCTAssert(server.hasClient())
connection.waitForMain()
waitForConnection(connection: connection)
XCTAssertEqual(connection.state, .connected)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "")
let speechForwarder = SpeechForwarderMock()
XCTAssert(!speechForwarder.isListening)
connection.listen(speechForwarder: speechForwarder)
XCTAssert(server.getListenState(isListening: true))
connection.waitForMain()
XCTAssert(speechForwarder.isListening)
XCTAssertEqual(connection.state, .listening)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "")
connection.set(text: "Hello, world!")
XCTAssert(speechForwarder.isListening)
XCTAssertEqual(connection.state, .listening)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "Hello, world!")
XCTAssertEqual(server.getText(), "Hello, world!")
connection.set(text: "Rewrite everything...")
connection.disconnect()
connection.waitForAllQueues()
XCTAssert(!speechForwarder.isListening)
XCTAssertEqual(connection.state, .disconnected)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "Rewrite everything...")
XCTAssert(server.getDisconnect())
}
func testBadSendMore() throws {
let connection = GSConnection()
connection.setMainQueueForTest()
XCTAssertEqual(connection.state, .disconnected)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "")
let server = GSServerMock()
connection.connect(destination: "127.0.0.1")
XCTAssertEqual(connection.state, .connecting)
XCTAssert(server.accept())
XCTAssert(server.hasClient())
connection.waitForMain()
waitForConnection(connection: connection)
XCTAssertEqual(connection.state, .connected)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "")
let speechForwarder = SpeechForwarderMock()
XCTAssert(!speechForwarder.isListening)
connection.listen(speechForwarder: speechForwarder)
XCTAssert(server.getListenState(isListening: true))
connection.waitForMain()
XCTAssert(speechForwarder.isListening)
XCTAssertEqual(connection.state, .listening)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "")
connection.set(text: "Hello, world!")
XCTAssert(speechForwarder.isListening)
XCTAssertEqual(connection.state, .listening)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "Hello, world!")
XCTAssertEqual(server.getText(), "Hello, world!")
connection.set(text: "Rewrite everything...")
XCTAssert(speechForwarder.isListening)
XCTAssertEqual(connection.state, .listening)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "Rewrite everything...")
connection.set(text: "Hello, everyone!")
connection.stopListening()
XCTAssert(!speechForwarder.isListening)
XCTAssertEqual(connection.state, .stoplistening)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "Hello, everyone!")
XCTAssert(server.sendMoreBad())
connection.waitForAllQueues()
XCTAssert(server.getDisconnect())
XCTAssertEqual(connection.state, .disconnected)
XCTAssertNotNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "Hello, everyone!")
}
func testServerDisconnect() throws {
let connection = GSConnection()
connection.setMainQueueForTest()
XCTAssertEqual(connection.state, .disconnected)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "")
let server = GSServerMock()
connection.connect(destination: "127.0.0.1")
XCTAssertEqual(connection.state, .connecting)
XCTAssert(server.accept())
XCTAssert(server.hasClient())
connection.waitForMain()
waitForConnection(connection: connection)
XCTAssertEqual(connection.state, .connected)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "")
server.disconnect()
connection.waitForReadQueue()
connection.waitForMain()
XCTAssertEqual(connection.state, .disconnected)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "")
}
func testServerDisconnectionWhileListening() throws {
let connection = GSConnection()
connection.setMainQueueForTest()
XCTAssertEqual(connection.state, .disconnected)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "")
let server = GSServerMock()
connection.connect(destination: "127.0.0.1")
XCTAssertEqual(connection.state, .connecting)
XCTAssert(server.accept())
XCTAssert(server.hasClient())
connection.waitForMain()
waitForConnection(connection: connection)
XCTAssertEqual(connection.state, .connected)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "")
let speechForwarder = SpeechForwarderMock()
XCTAssert(!speechForwarder.isListening)
connection.listen(speechForwarder: speechForwarder)
XCTAssert(server.getListenState(isListening: true))
connection.waitForMain()
XCTAssert(speechForwarder.isListening)
XCTAssertEqual(connection.state, .listening)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "")
connection.set(text: "Hello, world!")
XCTAssert(speechForwarder.isListening)
XCTAssertEqual(connection.state, .listening)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "Hello, world!")
XCTAssertEqual(server.getText(), "Hello, world!")
connection.set(text: "Rewrite everything...")
XCTAssert(speechForwarder.isListening)
XCTAssertEqual(connection.state, .listening)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "Rewrite everything...")
connection.set(text: "Hello, everyone!")
server.disconnect()
connection.waitForAllQueues()
XCTAssert(!speechForwarder.isListening)
XCTAssertEqual(connection.state, .disconnected)
XCTAssertNil(connection.errorMessage)
XCTAssertEqual(connection.textHeard, "Hello, everyone!")
}
// Other tests:
// - Connection deconstruction - When I remove the reference to the connection, it stays up. I think self has references because of closures that are still running in the queues.
/*
func testPerformanceExample() throws { func testPerformanceExample() throws {
// This is an example of a performance test case. // This is an example of a performance test case.
self.measure { self.measure {
// Put the code you want to measure the time of here. // Put the code you want to measure the time of here.
} }
} }
*/
} }

View File

@ -0,0 +1,25 @@
//
// SpeechForwarderMock.swift
// ListenerGSTests
//
// Created by Jeremy Rand on 2022-03-16.
//
import Foundation
@testable import ListenerGS
class SpeechForwarderMock : SpeechForwarderProtocol {
var isListening = false
var startListeningResult = true
func startListening(connection: GSConnection) -> Bool {
isListening = startListeningResult
return startListeningResult
}
func stopListening() {
assert(isListening)
isListening = false
}
}

View File

@ -22,6 +22,7 @@ class ListenerGSUITests: XCTestCase {
// Put teardown code here. This method is called after the invocation of each test method in the class. // Put teardown code here. This method is called after the invocation of each test method in the class.
} }
/*
func testExample() throws { func testExample() throws {
// UI tests must launch the application that they test. // UI tests must launch the application that they test.
let app = XCUIApplication() let app = XCUIApplication()
@ -39,4 +40,5 @@ class ListenerGSUITests: XCTestCase {
} }
} }
} }
*/
} }