OCMockObject is not thread safe #235

Merged
merged 1 commit into from Mar 23, 2016

Projects

None yet

6 participants

@iangithubusername
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

@erikdoe erikdoe commented on the diff Sep 20, 2015
Source/OCMock/OCMMacroState.m
@@ -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];
@erikdoe
erikdoe Sep 20, 2015 Owner

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

@iangithubusername
iangithubusername Sep 20, 2015 Contributor

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.

@erikdoe erikdoe commented on the diff Sep 20, 2015
Source/OCMock/OCMock.h
@@ -42,8 +42,13 @@
({ \
_OCMSilenceWarnings( \
[OCMMacroState beginStubMacro]; \
- invocation; \
- [OCMMacroState endStubMacro]; \
+ OCMStubRecorder *recorder = nil; \
+ @try{ \
+ invocation; \
+ }@finally{ \
+ recorder = [OCMMacroState endStubMacro]; \
+ } \
+ recorder; \
); \
@erikdoe
erikdoe Sep 20, 2015 Owner

Why is this now wrapped in a try/finally? Can we not emulate the previous version which took care of the deallocation more elegantly?

@iangithubusername
iangithubusername Sep 20, 2015 Contributor

Because invocation can throw an exception which would cause +endStubMacro to not get called. Now that the previously-global->now-per-thread macro state is held strongly by the thread dictionary, we can no longer count on the autorelease scope magically causing -dealloc to happen, and thus -dealloc can't do the same cleanup that +endStubMacro did.

@erikdoe erikdoe and 2 others commented on an outdated diff Sep 20, 2015
Source/OCMock/OCMockObject.m
[e raise];
}
}
- (BOOL)handleInvocation:(NSInvocation *)anInvocation
{
- [invocations addObject:anInvocation];
-
+ @synchronized(invocations)
+ {
+ // When the method has a char* argument we do not retain the arguments. This makes it possible
+ // to match char* args literally and with anyPointer. Not retaining the argument means that
+ // in these cases tests that use their own autorelease pools may fail unexpectedly.
+ if(![anInvocation hasCharPointerArgument])
+ [anInvocation retainArguments];
+ [invocations addObject:anInvocation];
+ }
+
@erikdoe
erikdoe Sep 20, 2015 Owner

I think these lines are the reason for the problem reported in #245. Why do you believe this code should be copied here?

@iangithubusername
iangithubusername Sep 20, 2015 Contributor

Yes this change definitely caused #245. -[OCPartialMockObject prepareObjectForInstanceMethodMocking] sets up a "forwarder" for -retain (and every other method on the class). I didn't notice it because -retain isn't usually called in ARC (there's an Obj-C primitive that gets called instead), but if you have a class that implements -retain itself (which apparently NSManagedObjectContext does if you grub around in the IMP) then -retain does get called. Then the "forwarder" causes -[OCMockObject handleInvocation:] to get called with an invocation for -retain, it retains the arguments of the invocation which includes the target (aka the receiver of the original -retain message), -retain gets called again, hits the forwarder again, etc.

It's definitely necessary to retain the invocation arguments though, otherwise they won't be valid from thread-to-thread. Really depending on how the autorelease scope goes they won't even be valid on the same thread in a lot of cases. Something like this was already exploding.

@autoreleasepool {
    [mock stubbedMethodWithArgument:argument];
    // argument deallocates now
}
OCMVerifyAll(mock); // This crashes because the mock has an invocation that has an
// unsafe unretained reference to argument and touching the invocation blows up.

I first just did a plain old -retainArguments, but that caused the unit tests to fail, and copying the char pointer special case made them succeed again.

As for fixing #245 I didn't see much point in capturing the reference counting methods at all, so I added them to the black list in OCPartialMockObject. I could see argument for special casing -retain similar to char pointer arguments though.

@carllindberg
carllindberg Nov 9, 2015 Contributor

Keep in mind that -retainArguments will also retain the target, plus any future target (if this invocation is used with -invoke or -invokeWithTarget:). So, if the target is self (unsure if that is possible in this context) then this is creating a retain loop and the mock won't ever dealloc.

If the target is never used, it might be worthwhile setting the target to nil before calling -retainArguments on any stored NSInvocation.

@iangithubusername
iangithubusername Nov 12, 2015 Contributor

Yes... Yuck. The target is very likely to be self, and it's used too so there's no clearing it. Beargh. The logical place to clear it would be in -stopMocking, but people don't currently call that method typically. It would seem the options are these.

  1. Release note it and change the documentation to say that -stopMocking always has to be called manually, and the object won't deallocate until you do, a la NSPort and NSTimer.
  2. Have people set something in their unit test bundle's Info.plist that says they want some thread safety with OCMock.
  3. Use a global or something like that and make people manually enable thread safe behavior on OCMock as a whole.

Personally I prefer the first option; I don't really want to manually enable thread safety, it should just be there. I also kind of feel like 2 and 3 are jumping through a lot of hoops just to save on a few -stopMocking calls.

@carllindberg
carllindberg Nov 12, 2015 Contributor

Another option would be to clear the target before calling -retainArguments, and when actually needing to use the invocation, make a copy of the invocation and call invokeWithTarget: on that. If we can assume the target is always "self". If not, maybe make the original target an "assign" associated object (original target) so we can get a reference back out later (without having it retained), and hope that argument is not dealloced (targets probably should be naturally retained elsewhere I'd guess during the test).

I did something like this in Collect3/CTAppearance#4 .

And really, arguments need to be retained if any of them are objects, not just if one of them happens to be a char * -- that seems a little odd.

Requiring calls to -stopMocking when there are untold thousands of existing lines of test case code out there written over many years which would suddenly turn into memory leaks without -stopMocking is probably not the best idea.

@iangithubusername
iangithubusername Nov 12, 2015 Contributor

The arguments are retained as long as none of them happen to be a char *. Which as the comment I copied says, doesn't really work. But there's too much matching code elsewhere that breaks if you do retain them. So in the interest of consistency I went with the same system of not retaining char * arguments.

I don't think the invocation is ever actually invoked, it's just kept for matching, mostly in the name of OCMVerify. But that's an interesting idea, a nil target could be considered matching for self. I'll see how tricky that is to implement, it seems like the best of both worlds there.

@carllindberg
carllindberg Nov 12, 2015 Contributor

OCMInvocationMatcher's matchesInvocation: method does not compare the target to determine a matching invocation. Is the target used somewhere else?

@iangithubusername
iangithubusername Nov 13, 2015 Contributor

Just nil'ing the target doesn't work, it breaks andForwardToRealObject on partial mocks (among other things). I'm going to see if anything terrible happens if I make a copy of of the invocation to retain arguments on.

@iangithubusername iangithubusername changed the title from OCMockObject is not thread safe #171 to OCMockObject is not thread safe Sep 24, 2015
@erikdoe
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.

@iangithubusername
Contributor

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

@erikdoe
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?

@iangithubusername
Contributor

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

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
@iangithubusername
Contributor

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

@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.

@iangithubusername
Contributor

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
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.

@iangithubusername
Contributor

@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
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.

@iangithubusername
Contributor

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.

@iangithubusername
Contributor

@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
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.

@iangithubusername
Contributor

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
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).

@iangithubusername
Contributor

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
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.

@iangithubusername
Contributor

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
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.

@iangithubusername
Contributor

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
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;
}
@iangithubusername
Contributor

Merged up to master and updated.

@benasher44
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
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.

@iangithubusername
Contributor

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

@erikdoe
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 (#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.

@iangithubusername
Contributor
  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. #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.

@iangithubusername iangithubusername OCMockObject is not thread safe
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.

erikdoe#171
982c6f7
@carllindberg carllindberg commented on the diff Jan 28, 2016
Source/OCMock/NSInvocation+OCMAdditions.m
{
- const char *argType = OCMTypeWithoutQualifiers([signature getArgumentTypeAtIndex:i]);
- if(strcmp(argType, "*") == 0)
- return YES;
+ NSMutableArray *retainedArguments = [[NSMutableArray alloc] init];
+ NSMethodSignature *methodSignature = self.methodSignature;
+
+ id target = self.target;
+ if((target != nil) && (target != objectToExclude) && !object_isClass(target))
+ {
+ // Bad things will happen if the target is a block since it's not being
+ // copied. There isn't a very good way to tell if an invocation's target
+ // 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];
@carllindberg
carllindberg Jan 28, 2016 Contributor

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.

@iangithubusername
iangithubusername Feb 5, 2016 Contributor

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.

@carllindberg
carllindberg Feb 10, 2016 Contributor

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.

@iangithubusername
iangithubusername Feb 22, 2016 Contributor

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.

@carllindberg
carllindberg Feb 22, 2016 Contributor

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 ;-)

@iangithubusername
Contributor

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

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

1 check passed

continuous-integration/travis-ci/pr The Travis CI build passed
Details
@erikdoe
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.

@iangithubusername
Contributor

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
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?

@iangithubusername
Contributor

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.

@iangithubusername
Contributor

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
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
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.

@iangithubusername
Contributor

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

@erikdoe erikdoe added a commit that referenced this pull request Apr 12, 2016
@erikdoe Added changes made necessary by #235. e90d2cb
@erikdoe erikdoe referenced this pull request Apr 14, 2016
Closed

niceMockForClass crash #292

@toddlee

Even if the argument is not self, it's possible to cause retain cycle. Not sure how to solve the problem though.
Let's say a mock is passed into an object's init. Inside that init, a mock is retained in a property. From anywhere in the object's code, [self.property someMethod:self] is called. Then since the argument isn't the mock itself, this is going to retain the argument (which is the object) causing a retain cycle.

@iangithubusername iangithubusername deleted the iangithubusername:Issue171 branch Apr 20, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment