Skip to content
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

OCMockObject is not thread safe #235

Merged
merged 1 commit into from
Mar 23, 2016
Merged

OCMockObject is not thread safe #235

merged 1 commit into from
Mar 23, 2016

Conversation

ian-twilightcoder
Copy link
Contributor

Make OCMockObject thread safe.

Synchronize on the four mutable array instance variables during access to ensure their consistency.
Retain everything retrieved from the arrays if the retrieved objects ever get used outside of the synchronized chunks.

Retain captured invocation arguments since the arguments could otherwise be deallocated at any time. The invocation can't outright retain its arguments since that would cause a retain cycle, so use a customized method to retain arguments that filters out self.
Add all the reference counting methods (-retainCount, -retain, -release, -autorelease) to OCPartialMockObject's selector black list. They're a recipe for stack overflow since partial mocks set up a forwarder for -retain. The forwarder would capture the call to -retain as an NSInvocation, OCMockObject now retains the invocation arguments, which calls -retain on the original object again, which hits the forwarder method again, and so on and so forth.

Make OCMMacroState thread safe as well.

Having a global macro state doesn't work. With locking it doesn't crash, but if two threads are recording at the same time then one of the macro states gets stepped on. Change the global state to be a per-thread state.
Avoid a use-after-dealloc race by not trying to clear the global state in -dealloc and making the macros exception safe such that the +endXXXMacro calls always happen even if the captured invocation throws an exception.

Make OCObserverMockObject thread safe.

Synchronize on the two mutable array instance variables during access to ensure their consistency.

Tested by building all schemes in the Xcode project, running the static analyzer on all of them, and running the unit tests on all of them.

Used a built copy of OCMock in my OS X project and verified that all of my project's unit tests completed successfully against the built copy of OCMock.

#171

@@ -90,8 +98,7 @@ - (id)initWithRecorder:(OCMRecorder *)aRecorder
- (void)dealloc
{
[recorder release];
if(globalState == self)
globalState = nil;
NSAssert([NSThread currentThread].threadDictionary[OCMGlobalStateKey] != self, @"Unexpected dealloc while set as the global state");
[super dealloc];
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why introduce an assert here? And shouldn't the object for OCMGlobalStateKey be removed from the thread dictionaory at this point?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the object for OCMGlobalStateKey should always be removed from the thread dictionary at this point, the assert was a "just in case" thing. I don't mind removing it.

@ian-twilightcoder ian-twilightcoder changed the title OCMockObject is not thread safe #171 OCMockObject is not thread safe Sep 24, 2015
@erikdoe
Copy link
Owner

erikdoe commented Oct 1, 2015

In case you are wondering, this PR is good and could be merged. However, the performance issues with iOS 9 are now confirmed (see #253), and the only workaround I can think of would impact threading. So, I think I'll postpone merging this one until we have a bit more clarity about the runtime issue.

@ian-twilightcoder
Copy link
Contributor Author

Thanks for the update, would love to see this in a release soon so I can stop building my own version of OCMock. ;-)

@erikdoe
Copy link
Owner

erikdoe commented Oct 13, 2015

It just occurred to me that there are other places in OCMock that aren't thread-safe. Consider this code from OCClassMockObject.m:

- (void)stopMocking
{
    if(originalMetaClass != nil)
        [self restoreMetaClass];
    [super stopMocking];
}

- (void)restoreMetaClass
{
    OCMSetAssociatedMockForClass(nil, mockedClass);
    object_setClass(mockedClass, originalMetaClass);
    originalMetaClass = nil;
}

When two threads hit this at the same time, it's possible that one thread sets originalMetaClass to nil when the second thread just enters restoreMetaClass. In that case object_setClass could be called with nil as the second argument, and this would be bad, right?

Just wondering whether we really want to make OCMock completely thread safe. Or could we describe a "level" of thread-safety we're aiming for?

@ian-twilightcoder
Copy link
Contributor Author

I would say that it's reasonable to say that you're only allowed to call -stopMocking once, kind of like -[NSTimer invalidate] and similar methods, in which case I don't think you need to worry about the threading. Although -restoreMetaClass is called from more than just -stopMocking, so yeah maybe mockedClass and originalMetaClass need similar guards. I admit that I didn't really do an exhaustive check over all the classes in OCMock, just the ones that were involved when I hit crashes mocking user defaults in a multithreaded app.

I suppose we could say that OCMock is thread safe enough such that the mocks can be used on multiple threads, but their creation and destruction is generally intended to be done on a single thread in +setUp, +tearDown, -setUp, -tearDown, and the various -testXXX methods? Or I can try and find more time to check all the other classes if you prefer?

@dwabyick
Copy link

Is this by any chance related to the following error? We've had some periodic failures during unit tests on OSX. I thought that unit tests are run on the main thread, so I'm not sure what the issue is.


2015-10-19 11:23:58.219 Project[63509:912285] An uncaught exception was raised
2015-10-19 11:23:58.219 Project[63509:912285] *** Collection <__NSArrayM: 0x628000245640> was mutated while being enumerated.
2015-10-19 11:23:58.219 Project[63509:912285] (
    0   CoreFoundation                      0x00007fff8c3e366c __exceptionPreprocess + 172
    1   libobjc.A.dylib                     0x00007fff9215076e objc_exception_throw + 43
    2   CoreFoundation                      0x00007fff8c3e2f05 __NSFastEnumerationMutationHandler + 309
    3   OCMock                              0x000000010227cec7 -[OCMockObject verifyInvocation:atLocation:] + 149
    4   OCMock                              0x000000010227b03d -[OCMVerifier forwardInvocation:] + 116

@ian-twilightcoder
Copy link
Contributor Author

Unit test methods are always invoked on the main thread, but they can spawn threads like anything else. (XCTestExpectation supports this). That said, the implication here is that -[OCMInvocationMatcher matchesInvocation:] is somehow modifying the the OCMockObject's invocations array on the same thread. (Since the access is locked, it's impossible for another thread to be doing the modification.) That should be an existing bug as far as I can tell.

Also this hasn't even been merged in to master yet, I'm still waiting for input from Erik on that.

@dwabyick
Copy link

@iangithubusername, thanks for the answer. Just for clarification, I was curious if your PR would address these types of issues. It sounds like this is a separate issue.

@ian-twilightcoder
Copy link
Contributor Author

It depends, I'd have to see the full crash log. If you have another thread in some OCMock methods, then this change has a good chance of addressing your issue. In my case I was seeing crashes like this too, mostly from a partial mock I'd made on +[NSUserDefaults standardUserDefaults] which gets accessed from pretty much every thread in my project. After this change all my crashes went away, however if you're seeing this in strictly single threaded code then this change shouldn't make a difference.

@benasher44
Copy link
Contributor

+1 A lot of objects that we mock are themselves thread-safe, which other code takes advantage of, but then the thread-safety guarantees of those objects fall apart when being mocked. Really looking forward to this PR being merged.

@ian-twilightcoder
Copy link
Contributor Author

@erikdoe What do you think about this one? Do you think it's good enough to say that tear down has to happen from only one thread, and only after everything is done using the mock? Other than that caveat I think this change does make the mock objects thread safe.

@benasher44
Copy link
Contributor

@iangithubusername I noticed in testing that OCObserverMockObject does not have any @synchronized blocks guarding the recorders or centers arrays. To make OCMock thread safe, I think this type of mock object should be considered as well.

@ian-twilightcoder
Copy link
Contributor Author

Good point, updated the PR. Although there's another problem with a retain cycle, I'm awaiting feedback from Erik as to how he wants that handled.

@ian-twilightcoder
Copy link
Contributor Author

@carllindberg I tried just nil'ing out the invocation targets but those are used by a couple different things like partial mocks to find their original object. What I ended up doing instead was hanging onto a copy of the invocation with self removed and arguments retained. That seems to work out alright and lets me remove the blacklist for reference counting methods too.

@carllindberg
Copy link
Contributor

I forgot that -retainArguments makes copies of c-strings as well -- that is the reason for avoiding retainArguments on methods which have char pointers, since the pointer address will change, breaking pointer comparisons needed by -anyPointer etc. (and it would crash if it tried dereferencing those pointers).

I think we may be best off writing our own NSInvocation category method "retainObjectArguments". That can go through the arguments (avoiding the target and return value), and for each one which is an object, add it to an NSArray. (If the argument is a block, make sure to call -copy first.) Then, add that array as an associated object to the NSInvocation. This approach should avoid the char * and target problem, but still make sure that object arguments are retained.

@ian-twilightcoder
Copy link
Contributor Author

I considered doing that too, just grabbing all the objects out of the invocation and adding them to a counted set. I thought it would be good to keep each invocation distinct and having them line up with the original invocation, just in case something ever removes things out of the invocation list then it would be easy to also remove from the retained invocations. Since the retained invocation copy isn't used for anything but to retain the arguments, the char * thing doesn't really matter. I suppose I could've dropped all of the non-object things out of the invocation too.

I don't know, there didn't really seem to be an obvious way to solve the problem. This one seems to work pretty well but if anyone really wants something else that's fine too, I just want to get this change integrated.

@carllindberg
Copy link
Contributor

If you just add the set or array as an associated object on the invocation instance, then the arguments are retained and they will be released naturally whenever the invocation gets dealloced. In other words, it's just a better version of -retainArguments for our purposes (which just adds objects to an internal NSArray). It would also fix the other place which currently needs to avoid -retainArguments.

Your version should work most of the time, though it will fail matching if "self" happens to be an argument other than the target (since it won't be copied over).

@ian-twilightcoder
Copy link
Contributor Author

My version isn't affecting the matching at all. The original invocation is untouched, and the new sidecar invocation is used solely for retaining the target, arguments, and return value while not retaining self to avoid the retain cycle.

Personally I'm not a fan of associated objects, they're too non-obvious to me and I only really use them in a last resort situation. What I could do is change invocations to be a map table whose keys are the invocations and whose values are a retained collection of just the object target/arguments/return value that aren't self.

@carllindberg
Copy link
Contributor

Ah, OK. Associated objects aren't bad at all; they are best used by category methods which give a better API but they are far far better than other mechanisms (like an external map table, the older approach). So, what you have is basically a more complicated version using an NSInvocation to retain the arguments instead of an NSArray, and additional storage on the copied invocations, but the effect would be the same. Adding it as an associated object would be pretty clean and requires no cleanup code anywhere else, and that method would also be appropriate to use in the other place which avoids retaining arguments, but whatever Erik prefers really.

@ian-twilightcoder
Copy link
Contributor Author

They're not bad, I just find them to be kind of non-obvious. It's a little clearer to me to use a map table since there's an obvious place to keep it (OCMockObject). Also "-retainObjectArgumentsExcludingObject:" seems like a kind of funky API to add onto NSInvocation. But let me look at the other place, maybe it's not as clean there.

@carllindberg
Copy link
Contributor

I think they are reasonably obvious when self-contained in a category method. It'd be funky API to add to a general-purpose category, of course, but for an OCMock-specific category it is what is needed ;-)

Oh... might be a good idea to copy block objects instead of just retaining them, in case they are a stack block.

@ian-twilightcoder
Copy link
Contributor Author

Oh yeah, that's why I was manually setting up a copy of the invocation in the first place; I didn't want to mess around figuring out what all needed to be copied or retained. In the interest of second guessing NSInvocation behavior as little as possible and letting -retainArguments just do its thing, I think I prefer the way it is right now and leaving OCMInvocationMatcher alone. We'll see what Erik says though.

@carllindberg
Copy link
Contributor

OK, that is interesting. You could also just store the copied invocation as an associated object on the original invocation then, and it will be cleaned up at the appropriate time without need to store them separately. Hopefully we would not run into a situation where a stack block was left on the original NSInvocation.

If you make the other place which is avoiding char * retainArguments use your method, that could also fix problems with method which have both char * and object arguments.

Determining a block isn't too hard though...

BOOL IsBlockObject(id anObject)
{
    static Class blockSuperclass = Nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        id block = ^{};
        Class blockClass = [block class];
        while ([blockClass superclass] != [NSObject class] && [blockClass superclass] != Nil)
            blockClass = [blockClass superclass];
        blockSuperclass = blockClass;
    });

    if (blockSuperclass != Nil)
        return [anObject isKindOfClass:blockSuperclass]);
    return NO;
}

@ian-twilightcoder
Copy link
Contributor Author

Merged up to master and updated.

@benasher44
Copy link
Contributor

@iangithubusername @erikdoe so, I've spent some time investigating the performance issues with OCMock, which this branch somehow exacerbates. The tests run fine when run from Xcode, but they slow down significantly when run using xctool (which many of us use, including travis-ci by default). It turns out that the issue is that xctool's otest-shim is injected to the test bundle by xctool, when you use it to run the tests. For its own purposes, it defines hash and isEqual: for NSInvocation in a category. Their implementation appears to be much slower, and the trace ending with isEqual: bubbles straight to the top in instruments when you profile the tests running with xctool. OCMock implicitly uses isEqual: in -[OCMockObject handleInvocation:] on the line that does [invocations setObject:selflessInvocation forKey:anInvocation];. This call subsequently calls into the isEqual: method defined in the NSInvocation category in otest-shim in xctool. If you run the tests just using xcodebuild, the performance issue seems to disappear. I could find out more, as I'm still in the early stages of re-implementing our test-runner to use xcodebuild without xctool, but I thought I'd report these early findings here. If there's another way to implement the currently functionality without using NSInvocation's isEqual, then that should be an easy win.

@erikdoe
Copy link
Owner

erikdoe commented Dec 15, 2015

Just a quick heads up. I'm not ignoring this thread and I do want this functionality to end up in OCMock. Unfortunately, though, I simply haven't found the time to look into something as complex as this. I'm expecting to spend time on OCMock over the holiday season.

@ian-twilightcoder
Copy link
Contributor Author

Thanks for checking in, did you get a chance to have a look at this?

@erikdoe
Copy link
Owner

erikdoe commented Jan 9, 2016

So, I finally had some time to look into this and, unfortunately, this is not a straight merge at the moment. In detail:

  1. There are a number of style issues. These are not big problems and I'm happy to fix them at a later stage myself.

  2. The use of a "shadow" invocation to retain the arguments. On this topic I'm with Carl and I think this should be handled inside the invocation itself. It feels like better design and I think the current code even supports this feeling. Pushing the responsibility to manage a second invocation to the outside code makes it neccessary to handle the copy in different places in different ways. The mock has to keep the copies in a map and the invocation matcher has to have a separate instance variable. In addition, looking at @benasher44's comment, it seems that using invocations this way creates a performance issue.

  3. When merging this PR into the current master a number of tests crash badly. Commenting out the newly introduced disposing of the dynamically created subclasses (Remove mocked subclasses from the runtime once the mocked object is d… #272) fixes the crashes, but of course, that means the subclasses keep lingering around, which the change was meant to fix. I wasn't able to get reasonable stack traces in Xcode but in AppCode I see traces like this one:

* thread #1: tid = 0x294b0c, 0x00007fff8b2614dd libobjc.A.dylib`objc_msgSend + 29, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x7fb743b0790)
    frame #0: 0x00007fff8b2614dd libobjc.A.dylib`objc_msgSend + 29
    frame #1: 0x00007fff9b5198ad CoreFoundation`-[__NSArrayM dealloc] + 205
    frame #2: 0x00007fff9b55cbb6 CoreFoundation`-[NSInvocation dealloc] + 118
    frame #3: 0x00007fff8b266b3b libobjc.A.dylib`(anonymous namespace)::AutoreleasePoolPage::pop(void*) + 477
    frame #4: 0x0000000107e315a0 XCTest`__24-[XCTestCase invokeTest]_block_invoke_2 + 167
    frame #5: 0x0000000107e6502e XCTest`-[XCTestContext performInScope:] + 184
    frame #6: 0x0000000107e314e8 XCTest`-[XCTestCase invokeTest] + 169
    frame #7: 0x0000000107e31983 XCTest`-[XCTestCase performTest:] + 443
    frame #8: 0x0000000107e2f654 XCTest`-[XCTestSuite performTest:] + 377
    frame #9: 0x0000000107e2f654 XCTest`-[XCTestSuite performTest:] + 377
    frame #10: 0x0000000107e2f654 XCTest`-[XCTestSuite performTest:] + 377
    frame #11: 0x0000000107e42a1b XCTest`-[XCTestObservationCenter _observeTestExecutionForBlock:] + 611
    frame #12: 0x0000000107e66447 XCTest`_XCTestMain + 1052

Notice frame 2. An NSInvocation is involved. Obviously I don't have the source code for it so I have no idea what array could be causing the problem. My hope is that not using these "shadow" invocations, and retaining the arguments like Carl suggested could avoid this issue.

@ian-twilightcoder
Copy link
Contributor Author

  1. Sorry, I really tried to follow the existing style in OCMock! If you want to point them out I'll fix them.

  2. OK, I used an associated array on the invocation instead (and since arrays don't check for equality on insertion that should take care of the performance issue as well).

  3. Ah I see what's going on. Invocations are being made for class methods, and this change calls -retainArguments on the invocations. It looks like -retainArguments eventually causes +release to be called on the class objects when the invocation deallocates. Remove mocked subclasses from the runtime once the mocked object is d… #272 made it so that -stopMocking disposes the Class object that's getting captured in the invocation. If the invocation outlives the OCPartialMockObject (which seems quite common due to autorelease pools, though interestingly it's not 100%), then when the invocation deallocates and calls +release on the dynamically created subclass that's already been disposed, you get a crash since the Class object is effectively a zombie. I fixed it by making the retain arguments thing not retain Class objects. They don't appear to actually be reference counted anyway (as far as I can tell from playing with +retainCount, +retain, and +release in the debugger), so it's not doing anyone any good to try to retain them.

Make OCMockObject thread safe.

Synchronize on the four mutable array instance variables during access to ensure their consistency.
Retain everything retrieved from the arrays if the retrieved objects ever get used outside of the synchronized chunks.

Retain captured invocation arguments since the arguments could otherwise be deallocated at any time. The invocation can't outright retain its arguments since that would cause a retain cycle, so use a customized method to retain arguments that filters out self.
Add all the reference counting methods (-retainCount, -retain, -release, -autorelease) to OCPartialMockObject's selector black list. They're a recipe for stack overflow since partial mocks set up a forwarder for -retain. The forwarder would capture the call to -retain as an NSInvocation, OCMockObject now retains the invocation arguments, which calls -retain on the original object again, which hits the forwarder method again, and so on and so forth.

Make OCMMacroState thread safe as well.

Having a global macro state doesn't work. With locking it doesn't crash, but if two threads are recording at the same time then one of the macro states gets stepped on. Change the global state to be a per-thread state.
Avoid a use-after-dealloc race by not trying to clear the global state in -dealloc and making the macros exception safe such that the +endXXXMacro calls always happen even if the captured invocation throws an exception.

Make OCObserverMockObject thread safe.

Synchronize on the two mutable array instance variables during access to ensure their consistency.

#171
// is a block though (the argument type at index 0 is always "@" even if
// the target is a Class or block), and in practice it's OK since you
// can't mock a block.
[retainedArguments addObject:target];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gave an implementation to check whether an object is a block in the comments to this pull request... it's probably safer to test the object directly rather than rely on the types, I'd guess.

Might be able to make a helper function / macro to reduce some of the repetitive code, but looks good.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought I'd wrote this last week... But if I were to guess I'd say that the different block types are more likely to share an encoding type than they are to share a common non-NSObject superclass. Though I'm sure either way it's relying on implementation detail. I'll see what @erikdoe says, hopefully we can close this out soon once and for all.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if you have code compiled with non-ARC blocks can be very different encoded types. Even with ARC I have seen some differences, I think. Maybe they have become more consistent on newer compilers but I have seen inconsistencies in the past (and OCMock could be running on lots of setups). I think dispatch_block_t may be encoded very differently, and it's also easily possible to pass a block into a method argument which is typed to accept any object. The common non-NSObject block superclass, while an implementation detail, is highly unlikely to change, and is a runtime check of the actual instance -- it should be much safer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried in ARC and non-ARC, all block types encode as @? even dispatch_block_t. So as far as I can tell the code as written is solid.

I actually pulled the identifying code from OCMIsObjectType, I just added another check for a case I saw where block arguments were included as part of the type. So I think this is stylistically the right thing to do, it positively identifies the argument type even if the actual passed argument is nil, and it seems to work in all cases. Of course I'll defer to @erikdoe but I think what's in the pull request is good.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about older versions of Xcode? OCMock gets used in lots of development environments.

And also, the type check will fail if an argument is typed as "id", but a block happens to be passed into it (say addObject: on an NSArray, if code creates an array of blocks, say).

But yes, it's up to Erik ;-)

@ian-twilightcoder
Copy link
Contributor Author

Hi @erikdoe what's the next step for this one?

@erikdoe erikdoe merged commit 982c6f7 into erikdoe:master Mar 23, 2016
@erikdoe
Copy link
Owner

erikdoe commented Mar 23, 2016

So, I finally, finally merged this. Many apologies. I've had a few crazy months... The changes I made on top of the PR are:

  1. Smaller stylistic changes. By the way, the idea is that the framework itself uses the OCMFunctionsPrivate header while users of the framework use OCMFunctions. The private header is not meant for private functions only. It may be a bit weird but I wasn't totally sure about the use of extern when using the functions from the same binary.

  2. Given that we decided in this thread to make OCMock only thread-safe as far as use of the mocks is concerned I have removed (hopefully all) code that dealt with making operations relation to setup and verify thread-safe. This includes, for example, the use of the thread dictionary in OCMMacroState. I know I mentioned that this wasn't thread safe earlier, but that was before the decision not to go for full thread-safety. Please tell me if you feel strongly about the try/finally in OCMock.h.

@ian-twilightcoder
Copy link
Contributor Author

Thanks for merging this. However without the OCMMacroState thread dictionary changes and try/finally cleanup, anything that uses the OCMock 3 macros to create mocks that get used in multiple threads will very quickly crash.

@erikdoe
Copy link
Owner

erikdoe commented Mar 23, 2016

Not sure I understand. As far as I can tell the use of the thread dictionary makes the actual use of the macro thread-safe, it ensures that when two threads in parallel use the stub/expect/verify macros, the macros still work. If we use these macros only from one thread, then there should be no problem, not?

@ian-twilightcoder
Copy link
Contributor Author

I don't remember the exact details, just that things accessing +[OCMMacroState globalState] were getting the wrong state object which was causing messages to go to the wrong mock.

@ian-twilightcoder
Copy link
Contributor Author

From -[OCMockObject forwardingTargetForSelector:] as I recall. I think there was a background thread in that method, and the main thread was in the middle of setting up a different mock object.

@erikdoe
Copy link
Owner

erikdoe commented Mar 23, 2016

Hmm. Let's assume we do this:

OCMStub([someMock foo]);
OCMStub([someMock bar]);

These two calls should result in two invocations of forwardingTargetForSelector:, one for the foo method and another for the bar method. The implementation of forwardingTargetForSelector: needs to check whether it's invoked as a real stub (use of the mock) or whether it's during setup. For that it checks the global state. If there is global state then it's a setup, otherwise the stub needs to be used.

@erikdoe
Copy link
Owner

erikdoe commented Mar 23, 2016

Okay, thinking about it again, do you have the following scenario in mind?

OCMStub([someMock foo]);
[anotherObject doStuff];
OCMStub([someMock bar]);

If doStuff triggers another thread and that thread then uses someMock while we're trying to set up the stub for bar, then I could see how that thread, if it's unlucky, would see the global state and erroneously assume it's in setup mode.

If that's the case then the other @synchronised I removed in the addStub: method etc. would also be needed, right?

Could you test whether the version as is does work in your scenarios? I'm asking because the code for OCMock is quite complex already and I only want to add stuff that's absolutely necessary to implement the features. I'm not asking for a unit test. Just a quick heads up whether current master would work for you.

@ian-twilightcoder
Copy link
Contributor Author

That does appear to be the case. 60c7b46 works fine, but 729c99b crashes.

erikdoe added a commit that referenced this pull request Apr 12, 2016
@erikdoe erikdoe mentioned this pull request Apr 14, 2016
@ian-twilightcoder ian-twilightcoder deleted the Issue171 branch April 20, 2016 22:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants