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 */
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;

View File

@ -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()
}
}

View File

@ -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())

View File

@ -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>

View File

@ -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()
}
}
}

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 {
// 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.
}
}
*/
}

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.
}
/*
func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
@ -39,4 +40,5 @@ class ListenerGSUITests: XCTestCase {
}
}
}
*/
}