Hi,Thanks. The problem I'm having is the different behaviour when I run the above code (yours or mine) in an iOS app v an OSX command line tool.So now, using your suggested code, I get different results between iOS and OSX. But with both I see no state change being reported. The examples all look the same, and I've looked at other networking frameworks. Is this something weird about running from Xcode?I am using this test snippet:let conn = Connection()
conn.startConnection()
sleep(10)
for index in 1...3 {
conn.send(message: "test message \(index)")
}And this is the code:import Foundation
import Network
@available(OSX 10.14, *)
class Connection: ConnectionDelegate {
var connection: NWConnection?
weak var delegate: ConnectionDelegate?
func startConnection() {
connection = NWConnection(host: "127.0.0.1", port: NWEndpoint.Port("80")!, using: .tcp)
connection?.stateUpdateHandler = { [weak self] newState in
guard let strongSelf = self else { return }
switch newState {
case .setup:
print("Connection setup")
case .preparing:
print("Connection preparing")
case .ready:
print("Connection Established")
strongSelf.delegate?.connectionDidUpdate(strongSelf, true)
case .waiting(let error):
print("Waiting with \(error)")
case .failed(let error):
print("Failed with \(error)")
default:
break
}
}
connection?.start(queue: .main)
print("connection start")
}
func connectionDidUpdate(_ conn: Connection, _ state: Bool) {
print("Did update state to ready: \(state), \(conn)")
}
func send(message: String) {
print("sending '\(message)' while connection state is \(connection?.state)")
connection?.send(content: Data(message.utf8), completion: NWConnection.SendCompletion.idempotent)
}
}
protocol ConnectionDelegate: AnyObject {
func connectionDidUpdate(_ conn: Connection, _ ready: Bool)
}Console output when run as an iOS app:connection start
sending 'test message 1' while connection state is Optional(Network.NWConnection.State.setup)
sending 'test message 2' while connection state is Optional(Network.NWConnection.State.setup)
sending 'test message 3' while connection state is Optional(Network.NWConnection.State.setup)Running nc in Terminal:% nc -l 80
test message 1test message 2test message 3%Console output when run as an OSX command-line-tool:connection start
sending 'test message 1' while connection state is Optional(Network.NWConnection.State.setup)
sending 'test message 2' while connection state is Optional(Network.NWConnection.State.setup)
sending 'test message 3' while connection state is Optional(Network.NWConnection.State.setup)
Program ended with exit code: 0Running nc in Terminal:% nc -l 80
test message 1%
Post
Replies
Boosts
Views
Activity
Hi,thanks for your help so far.The question is why is stateUpdateHandler not being called? Even though, clearly, the connection is coming up, data is sent (and received), and the connection closes. I need to be able to have some logic take action if the state changes. This is what I'm trying to figure out.cheers,
I'll take a look at this, and play around with the code.
ahh, yes I see. Yes, I get the correct results when using dispatchMain(). For anyone else, I found a good explanation of dispatchMain() here. It makes most sense if you follow the thread discussion all the way through.I also realised (as a result of the above) that I was mis-declaring 'conn = Connection()' inside methods in the iOS app. Which obviously meant conn disappeared as soon as the method finished. And that was why it was intermittently working (especially with the 'sleep 10' which was actually meant for the cli version). So I was confusing myself... 😁Using the above, I've now got the correct results when importing the framework into both an iOS app and the command line tool. Which is exactly what I need.many thanks for your help/patience.
Yes, I don't want it in the parent as that would mean test code polluting the main code. With mocking I can have all the mocked classes in my test modules.thanks
So, I had a thought. Setting my var via a convenience initialiser. And it seems to work fine.Modified my mock class:public class MockNetworkHandler: NetworkHandler {
// this var should let me switch on/off the network for unit tests
var websiteActive: Bool
public override init() {
self.websiteActive = true // default
super.init()
}
public convenience init(networkActive: Bool = true) {
self.init()
websiteActive = networkActive
}
override func websiteStatus(_ fullURL: String, _ completion: @escaping (Int) -> Void) {
if websiteActive {
completion(801) // 801 just to differentiate from a real request in this example
}
else{
completion(901) // 901 just to differentiate from a real request in this example
}
}
}In use:var mockClient = AppClient(mockNetHandler: MockNetworkHandler(networkActive: true))
mockClient.netHandler?.websiteStatus("https://apple.com", statusCompletion)
mockClient = AppClient(mockNetHandler: MockNetworkHandler(networkActive: false))
mockClient.netHandler?.websiteStatus("https://apple.com", statusCompletion)Output:status 801
status 901
I am trying to work out the best way to unit test classes that depend on networking. I do not want any unit test code/scaffolding in the main code-base. The main code is a Swift package, with unit tests alongside.So, here is an example of two unit tests I have which test the outcome of device.processQueue(). I must test this for both the network being active and inactive. func testDeviceProcessQueueConnReady() {
let device = CKDevice()
device.conn = ConnectionMock(networkResponse: .ready)
device.submitToQueue(payload: "some data") // create some data to send
device.startConnection()
device.processQueue()
let sendQueueLength = device.sendQueue.count
XCTAssertEqual(0, sendQueueLength) // payload should have been sent
}
func testDeviceProcessQueueConnNotReady() {
let device = CKDevice()
device.conn = ConnectionMock(networkResponse: .preparing)
device.submitToQueue(payload: "some data") // create some data to send
device.startConnection()
device.processQueue()
let sendQueueLength = device.sendQueue.count
XCTAssertEqual(1, sendQueueLength) // payload should still be queued
}And I create the mock class in my unit test modules:public class ConnectionMock: Connection {
var networkState = NWConnection.State.ready
init() {
super.init(host: "127.0.0.1", port: "8000")
}
convenience init(networkResponse: NWConnection.State) {
self.init()
self.networkState = networkResponse
}
public override func startConnection() {
self.delegate?.connectionDidUpdate(self, (NWConnection.State.ready == networkState))
}
public override func sendMessage(queueID: String, messageString: String) {
// immediate response for unitests, rather than async in normal code flow
self.delegate?.messageProcessed(queueID: queueID, result: (NWConnection.State.ready == networkState))
}
}So, without putting any code in the main code body (deployable app/package) I can simulate network conditions per unit test.cheers,
So the choices are1. after assigning the mock handler to mockClient.netHandler, the value can be changed by force-casting it back to MockNetworkHandler. (your first above)2. to set the value before assigning it to mockClient.netHandler, at which point netHandler is effectively cast to the NetworkHandler type. (additional initialisers, your second above)All makes sense now.thanks.
Ahh, cool. Thanks.For anyone else, here's this method with a navigation view.struct ContentView: View {
@EnvironmentObject var userStatus: UserStatus
var body: some View {
ZStack {
NavigationView {
NavigationLink(destination: Text("Second View")) {
Text("Hello, World!")
}
.navigationBarTitle("Navigation")
}
if !userStatus.hasAccount {
Text("test: has no account")
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color.red)
.edgesIgnoringSafeArea(.all)
}
}
}
}
class UserStatus: ObservableObject {
@Published var hasAccount: Bool = true
//...
}And with a tab view (which I'm using):struct ContentView: View {
@EnvironmentObject var userStatus: UserStatus
var body: some View {
ZStack {
TabView {
Text("First View")
.tabItem {
Image(systemName: "1.circle")
Text("First")
}.tag(0)
}
if !userStatus.hasAccount {
Text("test: has no account")
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(Color.red)
.edgesIgnoringSafeArea(.all)
}
}
}
}
class UserStatus: ObservableObject {
@Published var hasAccount: Bool = false
//...
}
So I implemented the control using a semaphore, and it seems to be working fine. Though not load tested. Semaphores seem easier to understand than dispatch groups, more explicit. But I'm not 100% which is idiomatic for Swift.
In the closure of the api_call to rotate-credentials I signal the semaphore like this :
{
	// rotate-credentials api call closure
	defer {
		self.semaphore.signal()
		print("semaphore signal - apiCredential")
	}
	// main closure code
}
The debug output gives the logical flow:
ident loaded: 7CC20A7BEAF242C8B853BAE960402EC7
secret loaded: A52189B0CA8D42ABAF22047439057B44
semaphore wait over - getnodestatus
Req http://127.0.0.1:8000/vendor/node/status, Resp 418
💥 credential trigger 💥
Triggered credential rotation for 7CC20A7BEAF242C8B853BAE960402EC7 with secret 0356FC2123804D4DB232379BD88CA06B, signed with A52189B0CA8D42ABAF22047439057B44.
Req http://127.0.0.1:8000/vendor/node/credential, Resp 201
KeychainManager: Successfully deleted data
KeychainManager: Item added successfully
Primary credential now: 0356FC2123804D4DB232379BD88CA06B
semaphore signal - apiCredential
semaphore wait over - getnodestatus
Req http://127.0.0.1:8000/vendor/node/status, Resp 204
semaphore wait over - getnodestatus
semaphore signal - getnodestatus
Req http://127.0.0.1:8000/vendor/node/status, Resp 204
semaphore signal - getnodestatus
semaphore wait over - getnodestatus
Req http://127.0.0.1:8000/vendor/node/status, Resp 204
semaphore signal - getnodestatus
Ach, my obvious mistake on the first one. I had been using a separate custom log method, which did have a label.
On the second, ahh yes I see. Compiled first time with the changes.
thanks very much,
For anyone happening upon this, here's the working version. In my case I'm drawing a collection of buttons.
struct collectionView: View {
let totalElements = 8
let elementWidth: CGFloat = 80.0
var body: some View {
GeometryReader<AnyView> { geometry in
let elementsPerRow = Int(geometry.size.width / self.elementWidth)
let rowCount = Int( (CGFloat(self.totalElements) / CGFloat(elementsPerRow)).rounded(.up) )
return AnyView(
VStack(alignment: .leading) {
ForEach(0..<rowCount) { rowIndex in
HStack {
ForEach(0..<elementsPerRow) { elementIndex in
if ((rowIndex * elementsPerRow) + elementIndex < self.totalElements) {
Button(action: {
print("\((rowIndex * elementsPerRow) + elementIndex)")
}) {
Text("\( (rowIndex * elementsPerRow) + elementIndex)")
.frame(maxWidth: self.elementWidth, maxHeight: self.elementWidth)
.background(Color.purple)
.foregroundColor(Color.white)
.clipShape(Circle())
}
}
}
}
}
}
)
}
}
}