mirror of
https://github.com/jeremysrand/ListenerApp.git
synced 2024-10-12 02:23:37 +00:00
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:
parent
923d0bf967
commit
d7c430f588
@ -8,6 +8,14 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
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 */; };
|
||||
9D5155F726A1EF7C0075EBC7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D5155F626A1EF7C0075EBC7 /* Assets.xcassets */; };
|
||||
9D5155FA26A1EF7C0075EBC7 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D5155F926A1EF7C0075EBC7 /* Preview Assets.xcassets */; };
|
||||
@ -51,6 +59,8 @@
|
||||
/* Begin PBXFileReference section */
|
||||
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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
@ -166,6 +176,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9D51560426A1EF7C0075EBC7 /* ListenerGSTests.swift */,
|
||||
9D2A6D1D27E235E400DF3D85 /* GSServerMock.swift */,
|
||||
9D2A6D2627E24BD600DF3D85 /* SpeechForwarderMock.swift */,
|
||||
9D51560626A1EF7C0075EBC7 /* Info.plist */,
|
||||
);
|
||||
path = ListenerGSTests;
|
||||
@ -388,7 +400,15 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
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 */,
|
||||
9D2A6D2327E236E400DF3D85 /* ytcpsocket.c in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -590,6 +610,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_ENTITLEMENTS = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = VD9FGCW36C;
|
||||
INFOPLIST_FILE = ListenerGSTests/Info.plist;
|
||||
@ -612,6 +633,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_ENTITLEMENTS = "";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = VD9FGCW36C;
|
||||
INFOPLIST_FILE = ListenerGSTests/Info.plist;
|
||||
|
@ -42,7 +42,7 @@ extension GSConnectionState: CustomStringConvertible
|
||||
|
||||
protocol SpeechForwarderProtocol
|
||||
{
|
||||
func startListening() -> Bool
|
||||
func startListening(connection: GSConnection) -> Bool
|
||||
func stopListening()
|
||||
}
|
||||
|
||||
@ -53,11 +53,12 @@ class GSConnection : ObservableObject {
|
||||
|
||||
var speechForwarder : SpeechForwarderProtocol?
|
||||
|
||||
let LISTEN_STATE_MSG = 1
|
||||
let LISTEN_TEXT_MSG = 2
|
||||
let LISTEN_SEND_MORE = 3
|
||||
static let LISTEN_STATE_MSG = 1
|
||||
static let LISTEN_TEXT_MSG = 2
|
||||
static let LISTEN_SEND_MORE = 3
|
||||
|
||||
static let port = 19026
|
||||
|
||||
let port = 19026
|
||||
private var destination = ""
|
||||
private var client: TCPClient?
|
||||
|
||||
@ -65,14 +66,15 @@ class GSConnection : ObservableObject {
|
||||
|
||||
private let readQueue = OperationQueue()
|
||||
private let writeQueue = OperationQueue()
|
||||
private var mainQueue = OperationQueue.main
|
||||
|
||||
private var condition = NSCondition()
|
||||
private var stopListeningFlag = false
|
||||
private var canSend = true
|
||||
|
||||
func changeState(newState : GSConnectionState)
|
||||
private func changeState(newState : GSConnectionState)
|
||||
{
|
||||
if (state == newState) {
|
||||
let oldState = state
|
||||
if (oldState == newState) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -80,24 +82,24 @@ class GSConnection : ObservableObject {
|
||||
switch (newState)
|
||||
{
|
||||
case .disconnected:
|
||||
legalTransition = ((state == .connected) || (state == .connecting))
|
||||
legalTransition = ((oldState == .connected) || (oldState == .connecting) || (oldState == .stoplistening))
|
||||
|
||||
case .connecting:
|
||||
legalTransition = (state == .disconnected)
|
||||
legalTransition = (oldState == .disconnected)
|
||||
|
||||
case .connected:
|
||||
legalTransition = ((state == .connecting) || (state == .listening) || (state == .stoplistening))
|
||||
legalTransition = ((oldState == .connecting) || (oldState == .listening) || (oldState == .stoplistening))
|
||||
|
||||
case .listening:
|
||||
legalTransition = (state == .connected)
|
||||
legalTransition = (oldState == .connected)
|
||||
|
||||
case .stoplistening:
|
||||
legalTransition = ((state == .connected) || (state == .listening))
|
||||
legalTransition = ((oldState == .connected) || (oldState == .listening))
|
||||
}
|
||||
|
||||
if (!legalTransition) {
|
||||
logger.error("Illegal requested state transition from \(self.state) to \(newState)")
|
||||
errorOccurred(title: "Bad State Change", message: "Illegal 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 \(oldState) to \(newState)")
|
||||
} else {
|
||||
state = newState
|
||||
}
|
||||
@ -105,7 +107,7 @@ class GSConnection : ObservableObject {
|
||||
|
||||
func errorOccurred(title: String, message : String)
|
||||
{
|
||||
OperationQueue.main.addOperation {
|
||||
mainQueue.addOperation {
|
||||
self.errorMessage = GSConnectionErrorMessage(title: title, message: message)
|
||||
}
|
||||
}
|
||||
@ -123,19 +125,19 @@ class GSConnection : ObservableObject {
|
||||
|
||||
private func doConnect() {
|
||||
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 {
|
||||
OperationQueue.main.addOperation { self.connectionFailed() }
|
||||
mainQueue.addOperation { self.connectionFailed() }
|
||||
return
|
||||
}
|
||||
switch client.connect(timeout: 10) {
|
||||
case .success:
|
||||
OperationQueue.main.addOperation { self.connectionSuccessful() }
|
||||
mainQueue.addOperation { self.connectionSuccessful() }
|
||||
case .failure(let error):
|
||||
client.close()
|
||||
self.client = nil
|
||||
logger.error("Failed to connect to \(self.destination): \(String(describing: error))")
|
||||
OperationQueue.main.addOperation { self.connectionFailed() }
|
||||
mainQueue.addOperation { self.connectionFailed() }
|
||||
return
|
||||
}
|
||||
|
||||
@ -143,10 +145,15 @@ class GSConnection : ObservableObject {
|
||||
guard let byteArray = client.read(2) else {
|
||||
break
|
||||
}
|
||||
|
||||
if (byteArray.count != 2) {
|
||||
break
|
||||
}
|
||||
|
||||
let data = Data(byteArray)
|
||||
do {
|
||||
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()
|
||||
canSend = true
|
||||
condition.broadcast()
|
||||
@ -166,7 +173,7 @@ class GSConnection : ObservableObject {
|
||||
|
||||
client.close()
|
||||
self.client = nil
|
||||
OperationQueue.main.addOperation { self.disconnect() }
|
||||
mainQueue.addOperation { self.disconnect() }
|
||||
}
|
||||
|
||||
func connect(destination : String) {
|
||||
@ -193,6 +200,9 @@ class GSConnection : ObservableObject {
|
||||
}
|
||||
condition.broadcast()
|
||||
condition.unlock()
|
||||
|
||||
waitForWriteQueue()
|
||||
waitForReadQueue()
|
||||
self.changeState(newState:.disconnected)
|
||||
}
|
||||
|
||||
@ -213,7 +223,7 @@ class GSConnection : ObservableObject {
|
||||
private func sendListenMsg(isListening: Bool) -> Bool {
|
||||
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:
|
||||
break
|
||||
case .failure(let error):
|
||||
@ -232,9 +242,9 @@ class GSConnection : ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
OperationQueue.main.addOperation {
|
||||
self.mainQueue.addOperation {
|
||||
self.changeState(newState: .listening)
|
||||
if (!speechForwarder.startListening()) {
|
||||
if (!speechForwarder.startListening(connection: self)) {
|
||||
self.logger.error("Unable to start listening")
|
||||
self.errorOccurred(title: "Speech Error", message: "Unable to start listening for speech")
|
||||
self.stopListening()
|
||||
@ -247,7 +257,7 @@ class GSConnection : ObservableObject {
|
||||
|
||||
_ = self.sendListenMsg(isListening: false)
|
||||
|
||||
OperationQueue.main.addOperation {
|
||||
self.mainQueue.addOperation {
|
||||
if (self.state == .stoplistening) {
|
||||
self.changeState(newState: .connected)
|
||||
}
|
||||
@ -318,29 +328,51 @@ class GSConnection : ObservableObject {
|
||||
let nsEnc = CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringBuiltInEncodings.macRoman.rawValue))
|
||||
let encoding = String.Encoding(rawValue: nsEnc) // String.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:
|
||||
switch (client.send(data: bytes)) {
|
||||
case .success:
|
||||
logger.debug("Sent text \"\(stringToSend)\"")
|
||||
break
|
||||
case .failure(let error):
|
||||
OperationQueue.main.addOperation {
|
||||
mainQueue.addOperation {
|
||||
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))")
|
||||
return false
|
||||
}
|
||||
case .failure(let error):
|
||||
OperationQueue.main.addOperation {
|
||||
mainQueue.addOperation {
|
||||
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))")
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func setMainQueueForTest() {
|
||||
mainQueue = OperationQueue()
|
||||
}
|
||||
|
||||
func waitForMain() {
|
||||
mainQueue.waitUntilAllOperationsAreFinished()
|
||||
}
|
||||
|
||||
func waitForReadQueue() {
|
||||
readQueue.waitUntilAllOperationsAreFinished()
|
||||
}
|
||||
|
||||
func waitForWriteQueue() {
|
||||
writeQueue.waitUntilAllOperationsAreFinished()
|
||||
}
|
||||
|
||||
func waitForAllQueues() {
|
||||
waitForWriteQueue()
|
||||
waitForReadQueue()
|
||||
waitForMain()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,7 +74,7 @@ struct GSView: View {
|
||||
|
||||
case .connected:
|
||||
Button("\(Image(systemName: "ear.and.waveform")) Listen and Send Text") {
|
||||
connection.listen(speechForwarder: SpeechForwarder(connection: connection))
|
||||
connection.listen(speechForwarder: SpeechForwarder())
|
||||
}
|
||||
.buttonStyle(GSButtonStyle())
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>483</string>
|
||||
<string>548</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.utilities</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
@ -11,8 +11,6 @@ import Speech
|
||||
|
||||
class SpeechForwarder : SpeechForwarderProtocol {
|
||||
|
||||
private var connection : GSConnection
|
||||
|
||||
private let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: Locale.preferredLanguages[0]))!
|
||||
|
||||
private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
|
||||
@ -23,11 +21,7 @@ class SpeechForwarder : SpeechForwarderProtocol {
|
||||
|
||||
private let logger = Logger()
|
||||
|
||||
init(connection : GSConnection) {
|
||||
self.connection = connection
|
||||
}
|
||||
|
||||
func startListening() -> Bool {
|
||||
func startListening(connection : GSConnection) -> Bool {
|
||||
SFSpeechRecognizer.requestAuthorization { authStatus in
|
||||
OperationQueue.main.addOperation {
|
||||
switch authStatus {
|
||||
@ -35,16 +29,16 @@ class SpeechForwarder : SpeechForwarderProtocol {
|
||||
break
|
||||
|
||||
case .denied, .restricted, .notDetermined:
|
||||
self.connection.stopListening()
|
||||
connection.stopListening()
|
||||
|
||||
default:
|
||||
self.connection.stopListening()
|
||||
connection.stopListening()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try startRecording()
|
||||
try startRecording(connection: connection)
|
||||
logger.debug("Started listening")
|
||||
}
|
||||
catch {
|
||||
@ -64,7 +58,7 @@ class SpeechForwarder : SpeechForwarderProtocol {
|
||||
recognitionTask = nil
|
||||
}
|
||||
|
||||
private func startRecording() throws {
|
||||
private func startRecording(connection : GSConnection) throws {
|
||||
|
||||
// Cancel the previous task if it's running.
|
||||
recognitionTask?.cancel()
|
||||
@ -95,18 +89,17 @@ class SpeechForwarder : SpeechForwarderProtocol {
|
||||
|
||||
if let result = result {
|
||||
// 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
|
||||
}
|
||||
|
||||
if error != nil {
|
||||
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 {
|
||||
OperationQueue.main.addOperation {
|
||||
self.connection.stopListening()
|
||||
connection.stopListening()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
133
ListenerGSTests/GSServerMock.swift
Normal file
133
ListenerGSTests/GSServerMock.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -17,17 +17,335 @@ class ListenerGSTests: XCTestCase {
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() throws {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
|
||||
func waitForConnection(connection: GSConnection) {
|
||||
for _ in (1...1000) {
|
||||
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 {
|
||||
// This is an example of a performance test case.
|
||||
self.measure {
|
||||
// Put the code you want to measure the time of here.
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
25
ListenerGSTests/SpeechForwarderMock.swift
Normal file
25
ListenerGSTests/SpeechForwarderMock.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
@ -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.
|
||||
}
|
||||
|
||||
/*
|
||||
func testExample() throws {
|
||||
// UI tests must launch the application that they test.
|
||||
let app = XCUIApplication()
|
||||
@ -39,4 +40,5 @@ class ListenerGSUITests: XCTestCase {
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user