问题
I have code of this form:
func myFunction(<...>, completionHandler: (ResponseType) -> Void) {
<prepare parameters>
mySessionManager.upload(multipartFormData: someClosure,
to: saveUrl, method: .post, headers: headers) { encodingResult in
// encodingCompletion
switch encodingResult {
case .failure(let err):
completionHandler(.error(err))
case .success(let request, _, _):
request.response(queue: self.asyncQueue) { response in
// upload completion
<extract result>
completionHandler(.success(result))
}
}
}
}
And testing code like this:
func testMyFunction() {
<prepare parameters>
var error: Error? = nil
var result: MyResultType? = nil
let sem = DispatchSemaphore(value: 0)
var ran = false
myFunction(<...>) { response in
if ran {
error = "ran twice"
return
}
defer {
ran = true
sem.signal()
}
switch response {
case .error(let err): error = err
case .success(let res): result = res
}
}
sem.wait()
XCTAssertNil(error, "Did not want to see this error: \(error!)")
<test response>
}
I use a semaphore to block the main thread until the request is processed asynchronously; this works fine for all my other Alamofire requests -- but not this one. The test hangs.
(Note bene: Using active waiting does not change things.)
Using the debugger, I figured out that
- all code that executes does so just fine but
- encodingCompletion is never called.
Now my best guess is that DispatchQueue.main.async
says, "execute this on the main thread when it has time" -- which it never will, since my test code is blocking there (and will run further tests, anyway).
I replaced it with self.queue.async
and upload.delegate.queue.addOperation
, two other queueing operations found in the same function. Then the test runs through but yields unexpected errors; my guess is that then, encodingCompletion
is called too early.
There are several questions to ask here; an answer to any can solve my problem.
- Can I test such code differently so that
DispatchQueue.main
can get to other tasks? - How can I use the debugger to find out which thread runs when?
- How can I adapt Alamofire at the critical position so that it does not require the main queue?
回答1:
As explained here, this is a bad "solution" as it introduces the possibility for deadlocks when requests are nested. I'm leaving this here for instructional purposes.
Changing
DispatchQueue.main.async {
let encodingResult = MultipartFormDataEncodingResult.success(
request: upload,
streamingFromDisk: true,
streamFileURL: fileURL
)
encodingCompletion?(encodingResult)
}
in SessionManager.swift to
self.queue.sync {
...
}
solves (read: works around) the problem.
I have no idea if this is a robust fix or anything; I have filed an issue.
回答2:
We should not block the main thread. XCTest has its own solution for waiting on asynchronous computations:
let expectation = self.expectation(description: "Operation should finish.")
operation(...) { response in
...
expectation.fulfill()
}
waitForExpectations(timeout: self.timeout)
From the documentation:
Runs the run loop while handling events until all expectations are fulfilled or the timeout is reached. Clients should not manipulate the run loop while using this API.
Outside of XCTest, we can use a similar mechanism as XCTestCase.waitForExpectations() does:
var done = false
operation(...) { response in
...
done = true
}
repeat {
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1))
} while !done
Note: This assumes that operation
sends its work to the same queue itself is executed on. If it uses another queue, this won't work; but then the approach using DispatchSemaphore
(see the question) does not cause a deadlock and can be used.
The implementation in XCTest does a lot more (multiple expectations, timeout, configurable sleep interval, etc.) but this is the basic mechanism.
来源:https://stackoverflow.com/questions/42644111/alamofire-never-calls-encodingcompletion-for-upload-with-multipartformdata-when