swiftunit-testingconcurrencyxctestxmppframework

Why is XMPPFramework blocking?


We are trying to get minimal example for XMPPFramework to run.

Consider this simple class:

import Foundation
import XMPPFramework

class Connection: NSObject, XMPPStreamDelegate {
    let stream: XMPPStream

    override init() {
        self.stream = XMPPStream()!
    }

    func connectToServer(timeout: TimeInterval) {
        stream.addDelegate(self, delegateQueue: DispatchQueue.main)
        stream.myJID = XMPPJID(string: "myuser")
        stream.hostName = "myserver.tld"
        stream.hostPort = 5222

        do {
            try stream.connect(withTimeout: timeout)
        }
        catch {
            print(error)
        }
    }

    func xmppStreamWillConnect(_ sender: XMPPStream!) {
        print("will connect")
    }

    func xmppStreamDidConnect(_ sender: XMPPStream!) {
        print("did connect")
        self.stream.disconnect()
    }
}

And this simple test class:

import Foundation
import XCTest
@testable import MyModule

class ConnectionTests: XCTestCase {
    func testConnect() {
        let connection = Connection()

        print("Try to connect")
        let expectation = self.expectation(description: "connect")
        connection.connectToServer(timeout: 3)
        self.waitForExpectations(timeout: 5)
    }
}

I expect this output:

Try to connect
will connect
did connect
<5s timeout since I don't fulfill>

If, by accident, my XMPP server did not react favorably to my request, I'd like to see:

Try to connect
will connect
<some error message>

If the XMPP server did not reply (quickly), I'd expect:

Try to connect
will connect
<3s timeout reached>

However, I'm getting none of these but instead:

Try to connect
will connect
<5s timeout since I don't fulfill>

What is going on?

Here is what I gather.

So I don't understand who blocks whom here, and how to prevent it from happening.


Solution

  • The problem is three-fold:

    1. The expectation times out because it's never fulfilled.
    2. We never see "did connect" because it never got that far.
    3. We didn't understand enough of XMPP(Framework) to understand where to plug in.

    We implement some additional delegate methods:

    func xmppStream(_ sender: XMPPStream!, socketDidConnect socket: GCDAsyncSocket!) {
        print("socket did connect")
    }
    
    func xmppStreamDidStartNegotiation(_ sender: XMPPStream!) {
        print("negotiation started")
    }
    
    ...
    
    func xmppStream(_ sender: XMPPStream!, didReceiveError error: DDXMLElement!) {
        print("error: \(error)")
    }
    
    func xmppStreamDidDisconnect(_ sender: XMPPStream!, withError error: Error!) {
        print("disconnected")
        self.afterConnection()
    }
    

    Here, self.afterConnection is a function passed to connectToServer as additional parameter.

    And we change in the test:

    connection.connectToServer(timeout: 3, then: { expectation.fulfill() })
    

    Now the test terminates in an orderly fashion with this output:

    Try to connect
    will connect
    socket did connect
    negotiation started
    disconnected
    error: Optional(<stream:error xmlns:stream="http://etherx.jabber.org/streams"><host-unknown xmlns="urn:ietf:params:xml:ns:xmpp-streams"/></stream:error>)
    

    So it's a problem with the server, after all. We needed to implement the correct delegates to diagnose the issue; we'd have expected stream.connect to throw an error in such a case!