From d7c430f588c15ccd59527a0d2546619b63bdacc3 Mon Sep 17 00:00:00 2001 From: Jeremy Rand Date: Wed, 16 Mar 2022 18:13:56 -0400 Subject: [PATCH] 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. --- ListenerGS.xcodeproj/project.pbxproj | 22 ++ ListenerGS/GSConnection.swift | 94 +++++-- ListenerGS/GSView.swift | 2 +- ListenerGS/Info.plist | 2 +- ListenerGS/SpeechForwarder.swift | 21 +- ListenerGSTests/GSServerMock.swift | 133 +++++++++ ListenerGSTests/ListenerGSTests.swift | 326 +++++++++++++++++++++- ListenerGSTests/SpeechForwarderMock.swift | 25 ++ ListenerGSUITests/ListenerGSUITests.swift | 2 + 9 files changed, 576 insertions(+), 51 deletions(-) create mode 100644 ListenerGSTests/GSServerMock.swift create mode 100644 ListenerGSTests/SpeechForwarderMock.swift diff --git a/ListenerGS.xcodeproj/project.pbxproj b/ListenerGS.xcodeproj/project.pbxproj index c7a0fd5..a1ad5d7 100644 --- a/ListenerGS.xcodeproj/project.pbxproj +++ b/ListenerGS.xcodeproj/project.pbxproj @@ -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 = ""; }; 9D0DC15826F2E47A007EB92D /* ListenerGS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ListenerGS.entitlements; sourceTree = ""; }; + 9D2A6D1D27E235E400DF3D85 /* GSServerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GSServerMock.swift; sourceTree = ""; }; + 9D2A6D2627E24BD600DF3D85 /* SpeechForwarderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechForwarderMock.swift; sourceTree = ""; }; 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 = ""; }; 9D5155F626A1EF7C0075EBC7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -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; diff --git a/ListenerGS/GSConnection.swift b/ListenerGS/GSConnection.swift index 01398b2..cd131df 100644 --- a/ListenerGS/GSConnection.swift +++ b/ListenerGS/GSConnection.swift @@ -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(" Bool { guard let client = client else { return false } - switch (client.send(data: pack("CFBundleShortVersionString 1.0 CFBundleVersion - 483 + 548 LSApplicationCategoryType public.app-category.utilities LSRequiresIPhoneOS diff --git a/ListenerGS/SpeechForwarder.swift b/ListenerGS/SpeechForwarder.swift index c4d5337..2abe2af 100644 --- a/ListenerGS/SpeechForwarder.swift +++ b/ListenerGS/SpeechForwarder.swift @@ -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() } } } diff --git a/ListenerGSTests/GSServerMock.swift b/ListenerGSTests/GSServerMock.swift new file mode 100644 index 0000000..f30af17 --- /dev/null +++ b/ListenerGSTests/GSServerMock.swift @@ -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(" Bool { + guard let client = client else { return false } + + let result = client.send(data: pack(" Bool { + guard let client = client else { return false } + + let result = client.send(data: pack(" 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(" Bool { + guard let client = client else { return false } + + guard let headerByteArray = client.read(1) else { + return true + } + + return (headerByteArray.count == 0) + } +} diff --git a/ListenerGSTests/ListenerGSTests.swift b/ListenerGSTests/ListenerGSTests.swift index c441a6c..b2ec27a 100644 --- a/ListenerGSTests/ListenerGSTests.swift +++ b/ListenerGSTests/ListenerGSTests.swift @@ -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. } } + */ } diff --git a/ListenerGSTests/SpeechForwarderMock.swift b/ListenerGSTests/SpeechForwarderMock.swift new file mode 100644 index 0000000..be44449 --- /dev/null +++ b/ListenerGSTests/SpeechForwarderMock.swift @@ -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 + } + +} diff --git a/ListenerGSUITests/ListenerGSUITests.swift b/ListenerGSUITests/ListenerGSUITests.swift index 5b56706..ebbe5ae 100644 --- a/ListenerGSUITests/ListenerGSUITests.swift +++ b/ListenerGSUITests/ListenerGSUITests.swift @@ -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 { } } } + */ }