-
Notifications
You must be signed in to change notification settings - Fork 263
Asynchronous testing API [AsyncXCTest 6/6] #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Symbolication is a great solution for getting the file and line numbers in Objective-C, where default parameters aren't available. However in very large codebases, I've seen symbolication take over 90 seconds to complete. I believe the private API In a Swift-only API, however, I think |
Capturing the last (or all) locations of un-awaited expectations seems like a good alternative solution without having to desymbolicate. 👍 Several observations on this implementation vs Objective-C XCTest (for which Nimble's is a similar to):
I think Just my 2 cents. |
Thanks for the comments, @jeffh! 👏
This is actually what Objective-C XCTest does as well! Running the following will report a single failure on the line where the last expectation is created: func test_expectationsUnawaited() {
let _ = self.expectationWithDescription("foo")
let _ = self.expectationWithDescription("bar") // Failure reported here in Objective-C XCTest
}
Nice catch--I'll update the code in this RFC. 👍 Thanks for bringing up (1), (2), and (4)--they'll be important to keep in mind once we've decided which dependencies to take on, and are ready to focus on implementation. |
I agree with @jeffh that using the Pulling in Corelibs Foundation for |
We have that circular dependency for testing Foundation with XCTest on Darwin as well, and it's never bothered me. I figure if we break |
@mike-ferris-apple should still comment on the actual substance of this RFC though. |
@briancroom I am of the opinion that testing something with itself only makes sense when the project is very mature. NSRunLoop is very new to swift-corelibs-foundation and I'm certain that we have at least one or two bugs still lurking around there. If we can help it I'd vote in favor of avoiding a circular graph if at all possible. Perhaps a conditional inclusion for upper level frameworks/apps that depend on Foundation might be interesting. But as @parkera mentioned @mike-ferris-apple should probably be the point of inquiry on that. |
Just a quick reply saying that I see this and will take a look when I get a chance. May be a couple days at this point, though. |
On point (A) in the original pull request, I think this seems like a reasonable solution. Writing source compatible tests is still very much possible, and mostly this will be transparent to folks. Point (B) says that there's a dummy implementation, from what I am seeing it looks like the current code would simply not compile on Linux? @briancroom commented that using Foundation to get access to NSError and NSRunLoop might be preferable and I would tend to agree. But I also agree with @phausler that establishing a dependency of Foundation (and in particular on NSRunLoop) is worrying given the current state of maturity of the corelibs Foundation. I have my doubts about how practically useful this API will be as long as the wait call is always guaranteed to wait the full timeout. I know from experience that when we have had bugs in Xcode's implementation that yielded the same behavior it winds up making tests run unacceptably slow. |
Yup! To be clear: the current implementation only works on OS X, and is meant purely as a placeholder. My plan was to add a cross-platform implementation (that also satisfied the expectations before the timeout when possible), just as soon as we reached a consensus on whether to depend upon swift-corelibs-libdispatch or swift-corelibs-foundation. It sounds like while there are worries about the maturity of swift-corelibs-foundation, no one is outright opposed to the idea of swift-corelibs-xctest depending upon swift-corelibs-foundation. I agree with this sentiment. As a next step, I'd like to amend this pull request with an implementation that depends upon swift-corelibs-foundation. Integrating swift-corelibs-foundation into this project and the build script may end up being a good chunk of work, so if anyone's 100% opposed to this idea, let me know ASAP, and save me a weekend of free time! 😉 |
Let's separate the question slightly... @parkera and @phausler, I'd love to here your thoughts.
@modocache I would suggest that if we're OK with #1 in general, then this is worth pursuing, and that perhaps we can structure the waiting implementation internally to possibly allow for alternate implementations to be tried out... I am not terribly sure what a non-libdispach/non-Foundation implementation of this would look like, but we could at least keep that door open. |
No objection from me on using Foundation (with the caveat that we know a big renaming effort is under way so you may have to update soon). The run loop implementation in corelibs Foundation is actually pretty close to mature. It's 95% the same as our other platforms and the Linux-specific stuff looks very reasonable to me. |
dce290d
to
61112cf
Compare
} | ||
|
||
// Otherwise, wait another fraction of a second. | ||
runLoop.runUntilDate(NSDate(timeIntervalSinceNow: 0.01)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@parkera @phausler Here I've implemented asynchronous tests in XCTest using SwiftFoundation's NSRunLoop
. Unfortunately, this doesn't exactly work--unlike when I use Objective-C Foundation, the SwiftFoundation runloop here does not run until the specified date.
In order to get this to work as it does with Objective-C Foundation, I had to make the following change to SwiftFoundation:
diff --git a/Foundation/NSRunLoop.swift b/Foundation/NSRunLoop.swift
index 4434429..6288242 100644
--- a/Foundation/NSRunLoop.swift
+++ b/Foundation/NSRunLoop.swift
@@ -95,10 +95,6 @@ extension NSRunLoop {
return false
}
let modeArg = mode._cfObject
- if _CFRunLoopFinished(_cfRunLoop, modeArg) {
- return false
- }
-
let limitTime = limitDate.timeIntervalSinceReferenceDate
let ti = limitTime - CFAbsoluteTimeGetCurrent()
CFRunLoopRunInMode(modeArg, ti, true)
I examined the implementation of _CFRunLoopFinished
to see what was amiss, but couldn't figure it out. Any ideas?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like the above works perfectly fine on Linux. Perhaps swift-corelibs-xctest should depend upon Objective-C Foundation when being built for OS X, instead of SwiftFoundation?
I updated the commit to use SwiftFoundation. The project builds on OS X, and its tests pass if I make the change to I use the same technique swift-corelibs-foundation does to specify a dependency. swift-corelibs-xctest now assumes For Linux, I plan on massaging the build script and |
51dec01
to
3eac171
Compare
OK! I pushed up a few changes to This now works perfectly on Linux. It works fine on OS X, aside from that weird @mike-ferris-apple, you'll be happy to hear that There are some changes to how this project is built: see my changes to the README for details. These changes will cause
@jeffh, I still haven't added addressed this comment, but I will! 😝 |
By the way, @briancroom: an interesting aspect of the new dependency on Foundation is that we can use |
As of swiftlang/swift-corelibs-xctest#43, swift-corelibs-xctest is dependent upon swift-corelibs-foundation. As such, it must be built after Foundation, and its build script must be passed a reference to the Foundation build directory. Update the build script in order to get XCTest building after the changes in swiftlang/swift-corelibs-xctest#43. In addition, this change also allows XCTest to be tested via `utils/build-script --xctest --test`. This has been possible since swiftlang/swift-corelibs-xctest#46.
Here's how the build script will need to change once these changes are merged: modocache/swift@a17c05b. That commit allows me to build and test swift-corelibs-xctest from OS X and Linux. |
@modocache I think it's fine in this case to use the Swift version of Foundation as a dependency on OS X. My thinking is this: the reason that swift-corelibs-foundation and swift-corelibs-xctest run on OS X at all is as a convenience for us to develop them in an environment that most of us are already familiar with (OS X, Xcode, etc). The deployment target of these two libraries is currently just Linux though (you'll notice that they are not present in the packages created for OS X). Plus, swift-corelibs-foundation already uses swift-corelibs-xctest as its test harness on OS X instead of the system one. So we have a kind of precedent there. |
It does?! Wow, this is news to me. I had assumed that, like swift-package-manager, it conditionally used either Objective-C XCTest or swift-corelibs-xctest, based on the host platform. It's very cool that it uses swift-corelibs-xctest on all platforms! 💯
Awesome, great to hear. Any ideas on the behavior discrepancy around |
If you can, let's open a PR on swift-corelibs-foundation to discuss the required change for NSRunLoop. |
@parkera Sure thing: swiftlang/swift-corelibs-foundation#258 Thanks! |
3eac171
to
37766ac
Compare
XCTAllExpectationFailures.append(failure) | ||
} | ||
|
||
/// Creates and returns an expectation associated with the test case. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice documentation on these!
Thanks for all the persistent work on this @modocache! It was really cool to see it all working on both platforms when I checked it out today. I've gone through and left a bunch of comments, some nits, some larger discussion points, but none of which I would consider to be merge blockers on this. |
Lots of activity here, trying to make sense of it all. @modocache , on your checklist above, 1, 2 and 3 look good to me. Since there's been a bit of discussion about what parameters should be required in the pm build script after this change, it would probably be a good idea to get buy in from @mxcl or @ddunbar for that PR. @briancroom had a bunch of interested comments/suggestions above. I agree with him that most changes from his suggestions could be addressed as later refinements if you want (though see below for one possible exception). On the meat of the PR, I made a couple comments on individual parts of the commit. Overall this looks good! Thanks for your persistence in putting this all together! The one thing that seems possibly worth addressing is the business about keeping the expectation failures out of band with the regular failure stream. Brian, once any final tweaks are made and once the pm guys sign off, would you like to do the actual merging here since it will need several steps to be staged? If testing is needed in the middle of the sequence, ping me and I can kick it off. |
swiftlang/swift-corelibs-xctest#43 will add a dependency between swift-corelibs-xctest and swift-corelibs-foundation. This will necessitate the path to a Foundation build be passed to the SwiftPM bootstrap script. In order to prevent CI from breaking, we'll modify the Swift build script to pass the correct parameters *before* those parameters are actually used. To do that, this commit ensures the bootstrap script accepts the new parameter: "--foundation".
b7890d8
to
22f1d24
Compare
Thanks for the excellent reviews, @briancroom and @mike-ferris-apple! 💯 I believe I've addressed all the comments. @mike-ferris-apple, as you suggested, I'll wait for @mxcl or @ddunbar to review swiftlang/swift-package-manager#176 and swiftlang/swift-package-manager#177. If those look good, I'll merge everything in the order I specified (except for the SwiftPM pull requests, I don't have commit access for those). As for CI: @mike-ferris-apple, could you "@swift-ci please test" the following three pull requests? |
Mirror the Objective-C XCTest API to add asynchronous testing support, primarily via two methods: - `XCTestCase.expectationWithDescription()` - `XCTestCase.waitForExpectationsWithTimeout()` These methods' APIs are identical to their Objective-C SDK overlay counterparts, with the following exceptions: 1. If expectations are created but not waited for, Objective-C XCTest generates a test failure. In order to display a failure on the exact line of the last expectation that was created, Objective-C XCTest symbolicates the call stack. It does so using a private framework called `CoreSymbolication.framework`. We don't have this at our disposal for swift-corelibs-xctest, so instead we take `file` and `line` as parameters, defaulting to `__FILE__` and `__LINE__`. By doing so, we provide the same failure highlighting functionality while also maintaining a (nearly) identical API. 2. If `waitForExpectationsWithTimeout()` fails, Objective-C XCTest generates a test failure on the exact line that method was called, using the same technique from (1). For the same reason, this method also takes `file` and `line` parameters in swift-corelibs-xctest. 3. `waitForExpectationsWithTimeout()` takes a handler callback, which in Objective-C XCTest is passed an instance of `NSError`. swift-corelibs-xctest doesn't depend upon swift-corelibs-foundation, and so does not have access to `NSError`. Instead, it defines a shim, `XCTError`. In order to implement the asynchronous APIs, swift-corelibs-xctest now has a dependency upon swift-corelibs-foundation. Bump minimum deployment targets for XCTest to the same required by Foundation.
22f1d24
to
df734de
Compare
@mike-ferris-apple Everything's in place for this to be merged. Could you ask @swift-ci to please test? If the tests go well on Linux I'll merge. (I assume the CI still doesn't work for OS X, I'll test that locally prior to merging.) |
@swift-ci please test |
I'll double check this works on OS X, then merge this within the next few hours. |
Asynchronous testing API [AsyncXCTest 6/6]
We did it!! 🎉 Thanks for all the awesome reviews and the amazing troubleshooting everyone. I'll send an email to the corelibs-dev mailing list to let people know about the new Foundation dependency. |
Yeaaaah! So exciting to see this go in. |
Great! Thanks for your perseverance on this one! |
What's in this pull request?
An attempt to add asynchronous testing support, mirroring the Objective-C XCTest API. This is done primarily via two methods:
XCTestCase.expectationWithDescription()
XCTestCase.waitForExpectationsWithTimeout()
The tests in the pull request pass on Darwin, but not on Linux. Still, I've encountered a few issues that I'd like to get feedback on.
What's worth discussing about this pull request?
The methods added are identical to their Objective-C SDK overlay counterparts, with the following exceptions:
CoreSymbolication.framework
. We don't have this at our disposal for swift-corelibs-xctest, so instead this pull request has the method takefile
andline
as parameters, defaulting to__FILE__
and__LINE__
. By doing so, we provide the same failure highlighting functionality while also maintaining a (nearly) identical API.waitForExpectationsWithTimeout()
fails, Objective-C XCTest generates a test failure on the exact line that method was called, using the same technique from (1). For the same reason, this method also takesfile
andline
parameters in this pull request.waitForExpectationsWithTimeout()
takes a handler callback, which in Objective-C XCTest is passed an instance ofNSError
. swift-corelibs-xctest doesn't depend upon swift-corelibs-foundation, and so does not have access toNSError
. Instead, it defines a shim,XCTError
.I'd like to discuss the following:
(A) Without using something like
CoreSymbolication.framework
, we cannot emit failures for the correct file or line while maintaining an identical API. This pull request is a sort of middle ground; the use of the API looks identical:However, they're not totally identical. The following is only possible in swift-corelibs-xctest:
Is this an acceptable solution?
(B) The code in this pull request has a dummy implementation to wait for expectations to be fulfilled. For a real implementation, I think we'll need to add a dependency to swift-corelibs-xctest, and I think that should either be swift-corelibs-libdispatch or swift-corelibs-foundation.
I think swift-corelibs-foundation would be ideal: it includes both:
NSRunLoop
, which we can use to implement the waiting logic, and...NSError
, which is used by the callback towaitForExpectationsWithTimeout()
.However, swift-corelibs-foundation uses swift-corelibs-xctest for its own unit test suite. So while the dependency isn't exactly cyclical (Foundation's tests depend on XCTest, which depends on Foundation--no cycle), it's still a little murky as to whether it's a good idea.
Should swift-corelibs-xctest depend on swift-corelibs-foundation? Should it depend on swift-corelibs-libdispatch? Or should it depend on neither--and if so, how can we implement asynchronous testing?
/cc @briancroom @jeffh