From 861bfc86deda59260e5a01e81ccb7e31de3207a9 Mon Sep 17 00:00:00 2001 From: Dave MacLachlan Date: Fri, 15 Jan 2021 17:41:27 -0800 Subject: [PATCH] Allow injection of macro state class into OCM macros We have a need to inject a replacement for `OCMMacroState` for when we are doing cross process testing using a library like https://github.com/google/EarlGrey/tree/earlgrey2. This simple expansion to the basic macros will give us the flexibility we need to allow us to use OCMock v3 syntax instead of falling back to OCMock v2 syntax when working with EarlGrey tests. The modification to andReturn macro allows us to move objects across the process boundary instead of "hiding" an object instead of an NSValue. The "hack" of using NSArray as a tuple could be replaced by a real object, or an NSDictionary, but I went with the simple stupid approach to start. --- Source/OCMock/OCMStubRecorder.h | 34 +++++++++++++++----- Source/OCMock/OCMStubRecorder.m | 21 ++++-------- Source/OCMock/OCMockMacros.h | 57 ++++++++++++++++++++------------- 3 files changed, 68 insertions(+), 44 deletions(-) diff --git a/Source/OCMock/OCMStubRecorder.h b/Source/OCMock/OCMStubRecorder.h index 383d9b45..7ba2040e 100644 --- a/Source/OCMock/OCMStubRecorder.h +++ b/Source/OCMock/OCMStubRecorder.h @@ -18,6 +18,7 @@ #import #import +#import #if !TARGET_OS_WATCH @class XCTestExpectation; @@ -42,15 +43,32 @@ @interface OCMStubRecorder (Properties) -#define andReturn(aValue) _andReturn(({ \ - __typeof__(aValue) _val = (aValue); \ - NSValue *_nsval = [NSValue value:&_val withObjCType:@encode(__typeof__(_val))]; \ - if (OCMIsObjectType(@encode(__typeof(_val)))) { \ - objc_setAssociatedObject(_nsval, "OCMAssociatedBoxedValue", *(__unsafe_unretained id *) (void *) &_val, OBJC_ASSOCIATION_RETAIN); \ - } \ - _nsval; \ +// Used to autoselect `return` vs `returnValue` based on type. +// Gets complicated because NSValue cannot be NSCoded if it contains a type +// of void*, and typeof(nil) is void* in ObjC (but not ObjC++) code. +// Also we want to avoid undefined behaviours by casting _val into an id +// if it isn't a pointer type. +#define andReturn(aValue) _andReturn(({ \ + __typeof__(aValue) _val = (aValue); \ + const char *_encoding = @encode(__typeof(aValue)); \ + const void *_nilPtr = nil; \ + BOOL _objectOrNil = OCMIsObjectType(_encoding) || \ + (strcmp(_encoding, @encode(void *)) == 0 && \ + memcmp((void*)&_val, &_nilPtr, sizeof(void*)) == 0); \ + id _retVal; \ + if(_objectOrNil) \ + { \ + __unsafe_unretained id _unsafeId; \ + memcpy(&_unsafeId, (void*)&_val, sizeof(id)); \ + _retVal = _unsafeId; \ + } \ + else \ + { \ + _retVal = [NSValue valueWithBytes:&_val objCType:_encoding]; \ + } \ + [NSArray arrayWithObjects:@(_objectOrNil), _retVal, nil]; \ })) -@property (nonatomic, readonly) OCMStubRecorder *(^ _andReturn)(NSValue *); +@property (nonatomic, readonly) OCMStubRecorder *(^ _andReturn)(NSArray *); #define andThrow(anException) _andThrow(anException) @property (nonatomic, readonly) OCMStubRecorder *(^ _andThrow)(NSException *); diff --git a/Source/OCMock/OCMStubRecorder.m b/Source/OCMock/OCMStubRecorder.m index 2c2448b1..09dbeed0 100644 --- a/Source/OCMock/OCMStubRecorder.m +++ b/Source/OCMock/OCMStubRecorder.m @@ -126,21 +126,14 @@ @implementation OCMStubRecorder (Properties) @dynamic _andReturn; -- (OCMStubRecorder * (^)(NSValue *))_andReturn -{ - id (^theBlock)(id) = ^(NSValue *aValue) { - if(OCMIsObjectType([aValue objCType])) - { - id objValue = nil; - [aValue getValue:&objValue]; // TODO: deprecated but replacement available in 10.13 only - return [self andReturn:objValue]; - } - else - { - return [self andReturnValue:aValue]; - } +- (OCMStubRecorder *(^)(NSArray *))_andReturn +{ + id (^theBlock)(NSArray *) = ^ (NSArray *aTuple) + { + id value = (aTuple.count == 1) ? nil : aTuple[1]; + return [aTuple[0] boolValue] ? [self andReturn:value] : [self andReturnValue:value]; }; - return (id)[[theBlock copy] autorelease]; + return [[theBlock copy] autorelease]; } diff --git a/Source/OCMock/OCMockMacros.h b/Source/OCMock/OCMockMacros.h index 22535572..f0155849 100644 --- a/Source/OCMock/OCMockMacros.h +++ b/Source/OCMock/OCMockMacros.h @@ -34,68 +34,76 @@ #define OCMObserverMock() [OCMockObject observerMock] -#define OCMStub(invocation) \ +#define OCMStubWithStateClass(MacroStateClass, invocation) \ ({ \ _OCMSilenceWarnings( \ - [OCMMacroState beginStubMacro]; \ + [MacroStateClass beginStubMacro]; \ OCMStubRecorder *recorder = nil; \ @try{ \ invocation; \ }@catch(...){ \ - [[OCMMacroState globalState] setInvocationDidThrow:YES]; \ + [[MacroStateClass globalState] setInvocationDidThrow:YES]; \ /* NOLINTNEXTLINE(google-objc-avoid-throwing-exception) */ \ @throw; \ }@finally{ \ - recorder = [OCMMacroState endStubMacro]; \ + recorder = [MacroStateClass endStubMacro]; \ } \ recorder; \ ); \ }) -#define OCMExpect(invocation) \ +#define OCMStub(invocation) OCMStubWithStateClass(OCMMacroState, invocation) + +#define OCMExpectWithStateClass(MacroStateClass, invocation) \ ({ \ _OCMSilenceWarnings( \ - [OCMMacroState beginExpectMacro]; \ + [MacroStateClass beginExpectMacro]; \ OCMStubRecorder *recorder = nil; \ @try{ \ invocation; \ }@catch(...){ \ - [[OCMMacroState globalState] setInvocationDidThrow:YES]; \ + [[MacroStateClass globalState] setInvocationDidThrow:YES]; \ /* NOLINTNEXTLINE(google-objc-avoid-throwing-exception) */ \ @throw; \ }@finally{ \ - recorder = [OCMMacroState endExpectMacro]; \ + recorder = [MacroStateClass endExpectMacro]; \ } \ recorder; \ ); \ }) -#define OCMReject(invocation) \ +#define OCMExpect(invocation) OCMExpectWithStateClass(OCMMacroState, invocation) + +#define OCMRejectWithStateClass(MacroStateClass, invocation) \ ({ \ _OCMSilenceWarnings( \ - [OCMMacroState beginRejectMacro]; \ + [MacroStateClass beginRejectMacro]; \ OCMStubRecorder *recorder = nil; \ @try{ \ invocation; \ }@catch(...){ \ - [[OCMMacroState globalState] setInvocationDidThrow:YES]; \ + [[MacroStateClass globalState] setInvocationDidThrow:YES]; \ /* NOLINTNEXTLINE(google-objc-avoid-throwing-exception) */ \ @throw; \ }@finally{ \ - recorder = [OCMMacroState endRejectMacro]; \ + recorder = [MacroStateClass endRejectMacro]; \ } \ recorder; \ ); \ }) +#define OCMReject(invocation) OCMRejectWithStateClass(OCMMacroState, invocation) -#define OCMClassMethod(invocation) \ + +#define OCMClassMethodWithStateClass(MacroStateClass, invocation) \ _OCMSilenceWarnings( \ - [[OCMMacroState globalState] switchToClassMethod]; \ + [[MacroStateClass globalState] switchToClassMethod]; \ invocation; \ ); +#define OCMClassMethod(invocation) OCMClassMethodWithStateClass(OCMMacroState, invocation) + #ifndef OCM_DISABLE_SHORT_SYNTAX #define ClassMethod(invocation) OCMClassMethod(invocation) @@ -106,38 +114,43 @@ #define OCMVerifyAllWithDelay(mock, delay) [(OCMockObject *)mock verifyWithDelay:delay atLocation:OCMMakeLocation(self, __FILE__, __LINE__)] -#define _OCMVerify(invocation) \ +#define _OCMVerifyWithStateClass(MacroStateClass, invocation) \ ({ \ _OCMSilenceWarnings( \ - [OCMMacroState beginVerifyMacroAtLocation:OCMMakeLocation(self, __FILE__, __LINE__)]; \ + [MacroStateClass beginVerifyMacroAtLocation:OCMMakeLocation(self, __FILE__, __LINE__)]; \ @try{ \ invocation; \ }@catch(...){ \ - [[OCMMacroState globalState] setInvocationDidThrow:YES]; \ + [[MacroStateClass globalState] setInvocationDidThrow:YES]; \ /* NOLINTNEXTLINE(google-objc-avoid-throwing-exception) */ \ @throw; \ }@finally{ \ - [OCMMacroState endVerifyMacro]; \ + [MacroStateClass endVerifyMacro]; \ } \ ); \ }) -#define _OCMVerifyWithQuantifier(quantifier, invocation) \ +#define _OCMVerify(invocation) _OCMVerifyWithStateClass(OCMMacroState, invocation) + + +#define _OCMVerifyWithQuantifierAndStateClass(MacroStateClass, quantifier, invocation) \ ({ \ _OCMSilenceWarnings( \ - [OCMMacroState beginVerifyMacroAtLocation:OCMMakeLocation(self, __FILE__, __LINE__) withQuantifier:quantifier]; \ + [MacroStateClass beginVerifyMacroAtLocation:OCMMakeLocation(self, __FILE__, __LINE__) withQuantifier:quantifier]; \ @try{ \ invocation; \ }@catch(...){ \ - [[OCMMacroState globalState] setInvocationDidThrow:YES]; \ + [[MacroStateClass globalState] setInvocationDidThrow:YES]; \ /* NOLINTNEXTLINE(google-objc-avoid-throwing-exception) */ \ @throw; \ }@finally{ \ - [OCMMacroState endVerifyMacro]; \ + [MacroStateClass endVerifyMacro]; \ } \ ); \ }) +#define _OCMVerifyWithQuantifier(quantifier, invocation) _OCMVerifyWithQuantifierAndStateClass(OCMMacroState, quantifier, invocation) + // explanation for macros below here: https://stackoverflow.com/questions/3046889/optional-parameters-with-c-macros #define _OCMVerify_1(A) _OCMVerify(A)