From c000f2f9e1b807f4fc5477a7f3ae0d58214ec6af Mon Sep 17 00:00:00 2001 From: Dave MacLachlan Date: Tue, 26 May 2020 12:49:34 -0700 Subject: [PATCH] Add support for having warnings vs errors A basic framework for dealing with issues that users may want to treat as either warnings or errors. It supports setting a default from compile time, modifying it at runtime using an environment variable, and push/popping states off of a stack. It also changes some exceptions that were being thrown to issues specifically because those exceptions are making it very difficult for me to transition my codebase from OCMock 3.4.1 to OCMock 3.6, and I know it will also be an issue for https://github.com/TextureGroup/Texture moving to OCMock 3.6. --- OCMock.podspec | 3 +- Source/OCMock.xcodeproj/project.pbxproj | 30 ++++ Source/OCMock/OCMIssueReporter.h | 61 +++++++ Source/OCMock/OCMIssueReporter.m | 166 ++++++++++++++++++ Source/OCMock/OCMock.h | 1 + Source/OCMock/OCMockObject.m | 24 ++- Source/OCMockTests/OCMIssueReporterTests.m | 105 +++++++++++ .../OCMockObjectClassMethodMockingTests.m | 5 +- .../OCMockTests/OCMockObjectInternalTests.m | 8 +- Source/OCMockTests/OCMockObjectTests.m | 9 +- .../OCMockObjectVerifyAfterRunTests.m | 4 + 11 files changed, 404 insertions(+), 12 deletions(-) create mode 100644 Source/OCMock/OCMIssueReporter.h create mode 100644 Source/OCMock/OCMIssueReporter.m create mode 100644 Source/OCMockTests/OCMIssueReporterTests.m diff --git a/OCMock.podspec b/OCMock.podspec index ef7d7cbd..15c2d8a1 100644 --- a/OCMock.podspec +++ b/OCMock.podspec @@ -29,7 +29,8 @@ Pod::Spec.new do |s| s.public_header_files = ["OCMock.h", "OCMockObject.h", "OCMArg.h", "OCMConstraint.h", "OCMLocation.h", "OCMMacroState.h", "OCMRecorder.h", "OCMStubRecorder.h", "NSNotificationCenter+OCMAdditions.h", - "OCMFunctions.h", "OCMVerifier.h", "OCMQuantifier.h" ] + "OCMFunctions.h", "OCMVerifier.h", "OCMQuantifier.h", + "OCMIssueReporter.h"] .map { |file| "Source/OCMock/" + file } end diff --git a/Source/OCMock.xcodeproj/project.pbxproj b/Source/OCMock.xcodeproj/project.pbxproj index dfaa8cbf..29a7f704 100644 --- a/Source/OCMock.xcodeproj/project.pbxproj +++ b/Source/OCMock.xcodeproj/project.pbxproj @@ -283,8 +283,20 @@ 8B11D4B82448E2F400247BE2 /* OCMCPlusPlus98Tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8B11D4B62448E2E900247BE2 /* OCMCPlusPlus98Tests.mm */; settings = {COMPILER_FLAGS = "-std=gnu++98"; }; }; 8B11D4BA2448E53600247BE2 /* OCMCPlusPlus11Tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8B11D4B92448E53600247BE2 /* OCMCPlusPlus11Tests.mm */; settings = {COMPILER_FLAGS = "-std=gnu++11"; }; }; 8B11D4BB2448E53600247BE2 /* OCMCPlusPlus11Tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8B11D4B92448E53600247BE2 /* OCMCPlusPlus11Tests.mm */; settings = {COMPILER_FLAGS = "-std=gnu++11"; }; }; + 8B911C8C247B97A500A3581F /* OCMIssueReporterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B911C8B247B97A500A3581F /* OCMIssueReporterTests.m */; }; + 8B911C8D247B97A500A3581F /* OCMIssueReporterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B911C8B247B97A500A3581F /* OCMIssueReporterTests.m */; }; 8BF73E53246CA75E00B9A52C /* OCMNoEscapeBlockTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF73E52246CA75E00B9A52C /* OCMNoEscapeBlockTests.m */; settings = {COMPILER_FLAGS = "-Xclang -fexperimental-optimized-noescape"; }; }; 8BF73E54246CA75E00B9A52C /* OCMNoEscapeBlockTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF73E52246CA75E00B9A52C /* OCMNoEscapeBlockTests.m */; settings = {COMPILER_FLAGS = "-Xclang -fexperimental-optimized-noescape"; }; }; + 8BF74028247AE3F100B9A52C /* OCMIssueReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BF74026247AE3F100B9A52C /* OCMIssueReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8BF74029247AE3F100B9A52C /* OCMIssueReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BF74026247AE3F100B9A52C /* OCMIssueReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8BF7402A247AE3F100B9A52C /* OCMIssueReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BF74026247AE3F100B9A52C /* OCMIssueReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8BF7402B247AE3F100B9A52C /* OCMIssueReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BF74026247AE3F100B9A52C /* OCMIssueReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8BF7402C247AE3F100B9A52C /* OCMIssueReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BF74026247AE3F100B9A52C /* OCMIssueReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8BF7402D247AE3F100B9A52C /* OCMIssueReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF74027247AE3F100B9A52C /* OCMIssueReporter.m */; }; + 8BF7402E247AE3F100B9A52C /* OCMIssueReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF74027247AE3F100B9A52C /* OCMIssueReporter.m */; }; + 8BF7402F247AE3F100B9A52C /* OCMIssueReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF74027247AE3F100B9A52C /* OCMIssueReporter.m */; }; + 8BF74030247AE3F100B9A52C /* OCMIssueReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF74027247AE3F100B9A52C /* OCMIssueReporter.m */; }; + 8BF74031247AE3F100B9A52C /* OCMIssueReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF74027247AE3F100B9A52C /* OCMIssueReporter.m */; }; 8DE97C5522B43EE60098C63F /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159E146333BF0052CD09 /* OCMockObject.m */; }; 8DE97C5622B43EE60098C63F /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158C146333BF0052CD09 /* OCClassMockObject.m */; }; 8DE97C5722B43EE60098C63F /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315AA146333BF0052CD09 /* OCPartialMockObject.m */; }; @@ -577,7 +589,10 @@ 817EB1621BD765130047E85A /* OCMock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8B11D4B62448E2E900247BE2 /* OCMCPlusPlus98Tests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = OCMCPlusPlus98Tests.mm; sourceTree = ""; }; 8B11D4B92448E53600247BE2 /* OCMCPlusPlus11Tests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = OCMCPlusPlus11Tests.mm; sourceTree = ""; }; + 8B911C8B247B97A500A3581F /* OCMIssueReporterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMIssueReporterTests.m; sourceTree = ""; }; 8BF73E52246CA75E00B9A52C /* OCMNoEscapeBlockTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMNoEscapeBlockTests.m; sourceTree = ""; }; + 8BF74026247AE3F100B9A52C /* OCMIssueReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMIssueReporter.h; sourceTree = ""; }; + 8BF74027247AE3F100B9A52C /* OCMIssueReporter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMIssueReporter.m; sourceTree = ""; }; 8DE97CA022B43EE60098C63F /* OCMock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A02926811CA0725A00594AAF /* TestObjects.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TestObjects.xcdatamodel; sourceTree = ""; }; D31108AD1828DB8700737925 /* OCMockLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OCMockLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -759,6 +774,7 @@ 03B316211463350E0052CD09 /* OCMConstraintTests.m */, 8B11D4B62448E2E900247BE2 /* OCMCPlusPlus98Tests.mm */, 8B11D4B92448E53600247BE2 /* OCMCPlusPlus11Tests.mm */, + 8B911C8B247B97A500A3581F /* OCMIssueReporterTests.m */, 2FA28EDBF243639C57F88A1B /* OCMArgTests.m */, 036865631D3571A8005E6BEE /* OCMQuantifierTests.m */, 03B316291463350E0052CD09 /* OCObserverMockObjectTests.m */, @@ -794,6 +810,8 @@ 037ECD5618FB0D2E00AF0E4C /* Helper */ = { isa = PBXGroup; children = ( + 8BF74026247AE3F100B9A52C /* OCMIssueReporter.h */, + 8BF74027247AE3F100B9A52C /* OCMIssueReporter.m */, 03E98D4918F308B400522D42 /* OCMLocation.h */, 03E98D4A18F308B400522D42 /* OCMLocation.m */, 2FA28006D043CBDBBAEF6E3F /* OCMMacroState.h */, @@ -974,6 +992,7 @@ 0368656D1D357317005E6BEE /* OCMQuantifier.h in Headers */, 3C0FF06A1BAA3FD10021AD20 /* OCMFunctionsPrivate.h in Headers */, 03B31613146333C00052CD09 /* OCProtocolMockObject.h in Headers */, + 8BF74028247AE3F100B9A52C /* OCMIssueReporter.h in Headers */, 2FA287ACE547BB41937BDEC3 /* NSObject+OCMAdditions.h in Headers */, 2FA28641AAD0AC2F876C9E48 /* OCMInvocationMatcher.h in Headers */, 0322DA6919118B4600CACAF1 /* OCMVerifier.h in Headers */, @@ -1015,6 +1034,7 @@ 03B315FB146333C00052CD09 /* OCMRealObjectForwarder.h in Headers */, 03DCED6F183406DA0059089E /* NSObject+OCMAdditions.h in Headers */, 0368656E1D357318005E6BEE /* OCMQuantifier.h in Headers */, + 8BF74029247AE3F100B9A52C /* OCMIssueReporter.h in Headers */, 817EB1661BD7674D0047E85A /* OCMFunctionsPrivate.h in Headers */, 03B31605146333C00052CD09 /* OCObserverMockObject.h in Headers */, 03B3160A146333C00052CD09 /* OCPartialMockObject.h in Headers */, @@ -1069,6 +1089,7 @@ 817EB15A1BD765130047E85A /* NSValue+OCMAdditions.h in Headers */, 817EB15B1BD765130047E85A /* OCMFunctions.h in Headers */, 817EB15C1BD765130047E85A /* OCMBlockArgCaller.h in Headers */, + 8BF7402B247AE3F100B9A52C /* OCMIssueReporter.h in Headers */, 817EB15D1BD765130047E85A /* OCMArgAction.h in Headers */, 2FA28806443827E286F12F6F /* OCMNonRetainingObjectReturnValueProvider.h in Headers */, ); @@ -1105,6 +1126,7 @@ 8DE97C9122B43EE60098C63F /* OCMFunctionsPrivate.h in Headers */, 8DE97C9222B43EE60098C63F /* OCObserverMockObject.h in Headers */, 8DE97C9322B43EE60098C63F /* OCMObserverRecorder.h in Headers */, + 8BF7402C247AE3F100B9A52C /* OCMIssueReporter.h in Headers */, 8DE97C9422B43EE60098C63F /* OCMPassByRefSetter.h in Headers */, 8DE97C9522B43EE60098C63F /* NSInvocation+OCMAdditions.h in Headers */, 8DE97C9622B43EE60098C63F /* NSMethodSignature+OCMAdditions.h in Headers */, @@ -1157,6 +1179,7 @@ F0B951491B00810C00942C38 /* NSValue+OCMAdditions.h in Headers */, F0B9514A1B00810C00942C38 /* OCMFunctions.h in Headers */, 2FA28B7BDB3319A499E90525 /* OCMBlockArgCaller.h in Headers */, + 8BF7402A247AE3F100B9A52C /* OCMIssueReporter.h in Headers */, 2FA280E60213BA09F007C173 /* OCMArgAction.h in Headers */, 2FA28AFBD67EAB9DD1F23BF5 /* OCMNonRetainingObjectReturnValueProvider.h in Headers */, ); @@ -1412,6 +1435,7 @@ 03B315B6146333BF0052CD09 /* NSMethodSignature+OCMAdditions.m in Sources */, 03618D85195B553400389166 /* OCMRecorder.m in Sources */, 03C7BF0C195DA2F200A545DD /* OCMInvocationStub.m in Sources */, + 8BF7402D247AE3F100B9A52C /* OCMIssueReporter.m in Sources */, 03B315BB146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.m in Sources */, 0322DA6B19118B4600CACAF1 /* OCMVerifier.m in Sources */, 03B315C0146333BF0052CD09 /* OCClassMockObject.m in Sources */, @@ -1454,6 +1478,7 @@ 03B315B8146333BF0052CD09 /* NSMethodSignature+OCMAdditions.m in Sources */, 03618D86195B553400389166 /* OCMRecorder.m in Sources */, 03C7BF0D195DA2F200A545DD /* OCMInvocationStub.m in Sources */, + 8BF7402E247AE3F100B9A52C /* OCMIssueReporter.m in Sources */, 03B315BD146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.m in Sources */, 0322DA6C19118B4600CACAF1 /* OCMVerifier.m in Sources */, 03B315C2146333BF0052CD09 /* OCClassMockObject.m in Sources */, @@ -1500,6 +1525,7 @@ 03E98D5018F310EE00522D42 /* OCMockObjectMacroTests.m in Sources */, 03565A4A18F05721003AE91E /* OCObserverMockObjectTests.m in Sources */, 03565A4318F05721003AE91E /* OCMockObjectClassMethodMockingTests.m in Sources */, + 8B911C8C247B97A500A3581F /* OCMIssueReporterTests.m in Sources */, 03565A4918F05721003AE91E /* OCMArgTests.m in Sources */, 036865641D3571A8005E6BEE /* OCMQuantifierTests.m in Sources */, 0322DA65191188D100CACAF1 /* OCMockObjectVerifyAfterRunTests.m in Sources */, @@ -1527,6 +1553,7 @@ 817EB1181BD765130047E85A /* OCClassMockObject.m in Sources */, 817EB1191BD765130047E85A /* OCPartialMockObject.m in Sources */, 817EB11A1BD765130047E85A /* OCProtocolMockObject.m in Sources */, + 8BF74030247AE3F100B9A52C /* OCMIssueReporter.m in Sources */, 817EB11B1BD765130047E85A /* OCMRecorder.m in Sources */, 817EB11C1BD765130047E85A /* OCMStubRecorder.m in Sources */, 817EB11D1BD765130047E85A /* OCMExpectationRecorder.m in Sources */, @@ -1569,6 +1596,7 @@ 8DE97C5622B43EE60098C63F /* OCClassMockObject.m in Sources */, 8DE97C5722B43EE60098C63F /* OCPartialMockObject.m in Sources */, 8DE97C5822B43EE60098C63F /* OCProtocolMockObject.m in Sources */, + 8BF74031247AE3F100B9A52C /* OCMIssueReporter.m in Sources */, 8DE97C5922B43EE60098C63F /* OCMRecorder.m in Sources */, 03A1CC9F23F8A045005ADA04 /* OCMQuantifier.m in Sources */, 8DE97C5A22B43EE60098C63F /* OCMStubRecorder.m in Sources */, @@ -1615,6 +1643,7 @@ D31108C51828DBD600737925 /* OCMockObjectClassMethodMockingTests.m in Sources */, D31108C71828DBD600737925 /* OCMConstraintTests.m in Sources */, D31108C91828DBD600737925 /* OCObserverMockObjectTests.m in Sources */, + 8B911C8D247B97A500A3581F /* OCMIssueReporterTests.m in Sources */, 037ECD5518FAD84100AF0E4C /* OCMInvocationMatcherTests.m in Sources */, D31108C41828DBD600737925 /* OCMockObjectPartialMocksTests.m in Sources */, D31108C81828DBD600737925 /* OCMStubRecorderTests.m in Sources */, @@ -1642,6 +1671,7 @@ F0B9510D1B0080EC00942C38 /* OCClassMockObject.m in Sources */, F0B9510E1B0080EC00942C38 /* OCPartialMockObject.m in Sources */, F0B9510F1B0080EC00942C38 /* OCProtocolMockObject.m in Sources */, + 8BF7402F247AE3F100B9A52C /* OCMIssueReporter.m in Sources */, F0B951101B0080EC00942C38 /* OCMRecorder.m in Sources */, F0B951111B0080EC00942C38 /* OCMStubRecorder.m in Sources */, F0B951121B0080EC00942C38 /* OCMExpectationRecorder.m in Sources */, diff --git a/Source/OCMock/OCMIssueReporter.h b/Source/OCMock/OCMIssueReporter.h new file mode 100644 index 00000000..0a314f63 --- /dev/null +++ b/Source/OCMock/OCMIssueReporter.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMFunctions.h" + +// Use this define to be able to control the default issue treatment from a build. +// It can be overridden by using `OCMIssueTreatmentDefaultEnvironmentVariable`. +#ifndef OCMIssueTreatmentDefault +#define OCMIssueTreatmentDefault OCMIssueTreatmentWarnings +#endif + +// Use this to control the default issue treatment through the environment. +// It can be set to 0 for warnings or 1 for errors. +#define OCMIssueTreatmentDefaultEnvironmentVariable @"OCMIssueTreatmentDefault" + +// The name of NSExceptions thrown by default when an issue it treated as an error. +OCMOCK_EXTERN NSExceptionName const OCMIssueException; + +typedef NS_ENUM(NSUInteger, OCMIssueTreatment) +{ + // Warnings are printed to stderr. + OCMIssueTreatmentWarnings = 0, + // Errors are thrown an NSExceptions. + OCMIssueTreatmentErrors, +}; + +@interface OCMIssueReporter : NSObject +{ + NSMutableArray *issueTreatmentStack; +} + ++ (instancetype)defaultReporter; + +- (void)reportIssueInFile:(const char *)file line:(NSUInteger)line format:(NSString *)format, ... NS_FORMAT_FUNCTION(3,4); +- (void)reportIssueInFilev:(const char *)file line:(NSUInteger)line format:(NSString *)format arguments:(va_list)args NS_FORMAT_FUNCTION(3,0); +- (void)reportIssueInFile:(const char *)file line:(NSUInteger)line exceptionName:(NSExceptionName)name reason:(NSString *)reason; + +// Pushes/Pops an issue treatment on the stack. Push and Pop can only be called from the main thread and must be balanced. +- (void)pushIssueTreatment:(OCMIssueTreatment)treatment; +- (void)popIssueTreatment; + +// The current issue treatment on the top of the stack. +- (OCMIssueTreatment)issueTreatment; + +@end + +#define OCM_REPORT_ISSUE(_format, ...) ([[OCMIssueReporter defaultReporter] reportIssueInFile:__FILE__ line:__LINE__ format:_format, __VA_ARGS__ ]) diff --git a/Source/OCMock/OCMIssueReporter.m b/Source/OCMock/OCMIssueReporter.m new file mode 100644 index 00000000..c956f9e7 --- /dev/null +++ b/Source/OCMock/OCMIssueReporter.m @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMIssueReporter.h" + +NSExceptionName const OCMIssueException = @"OCMIssueException"; + +@implementation OCMIssueReporter + ++ (instancetype)defaultReporter +{ + static dispatch_once_t onceToken; + static OCMIssueReporter *defaultReporter; + dispatch_once(&onceToken, ^{ + defaultReporter = [[OCMIssueReporter alloc] init]; + atexit_b(^{ + if([defaultReporter->issueTreatmentStack count] != 1) + { + [NSException raise:NSInternalInconsistencyException format:@"Unmatched push/pops on OCMIssueRecorder"]; + } + }); + }); + return defaultReporter; +} + +- (instancetype)init +{ + if((self = [super init])) + { + NSDictionary *environment = [[NSProcessInfo processInfo] environment]; + OCMIssueTreatment value; + NSString *stringValue = [environment objectForKey:OCMIssueTreatmentDefaultEnvironmentVariable]; + if(stringValue) + { + value = [stringValue integerValue]; + } + else + { + value = OCMIssueTreatmentDefault; + } + if(value > OCMIssueTreatmentErrors) + { + [NSException raise:NSInvalidArgumentException format:@"OCMIssueReporter has invalid default issue treatment: %d", (int)value]; + } + issueTreatmentStack = [[NSMutableArray alloc] init]; + [issueTreatmentStack addObject:[NSNumber numberWithInteger:value]]; + } + return self; +} + +- (void)dealloc +{ + [issueTreatmentStack release]; + [super dealloc]; +} + +- (void)reportIssueInFile:(const char *)file line:(NSUInteger)line format:(NSString *)format, ... +{ + va_list arguments; + va_start(arguments, format); + [self reportIssueInFilev:file line:line format:format arguments:arguments]; + va_end(arguments); +} + +- (void)reportIssueInFilev:(const char *)file line:(NSUInteger)line format:(NSString *)format arguments:(va_list)arguments +{ + NSString *description = [[NSString alloc] initWithFormat:format arguments:arguments]; + [self reportIssueInFile:file line:line exceptionName:OCMIssueException reason:description]; + [description release]; +} + +- (void)reportIssueInFile:(const char *)file line:(NSUInteger)line exceptionName:(NSExceptionName)name reason:(NSString *)reason +{ + OCMIssueTreatment treatment = [self issueTreatment]; + NSString *fullDescription; + NSString *type; + switch(treatment) + { + case OCMIssueTreatmentWarnings: + type = @"warning"; + break; + + case OCMIssueTreatmentErrors: + type = @"error"; + break; + + default: + [NSException raise:NSInternalInconsistencyException format:@"Unknown issue treatment: %d", (int)treatment]; + break; + } + if(file) + { + fullDescription = [[NSString alloc] initWithFormat:@"%s:%d:0: %@: %@", file, (int)line, type, reason]; + } + else + { + fullDescription = [[NSString alloc] initWithFormat:@"%@: %@", type, reason]; + } + switch(treatment) + { + case OCMIssueTreatmentWarnings: + fprintf(stderr, "%s\n", [fullDescription UTF8String]); + break; + + case OCMIssueTreatmentErrors: + [NSException raise:name format:@"%@", fullDescription]; + break; + + default: + [NSException raise:NSInternalInconsistencyException format:@"Unknown issue treatment: %d", (int)treatment]; + break; + } +} + +- (void)pushIssueTreatment:(OCMIssueTreatment)treatment +{ + if(![NSThread isMainThread]) + { + [NSException raise:NSInternalInconsistencyException format:@"pushIssueTreatment can only be called on main thread"]; + } + @synchronized(self) + { + [issueTreatmentStack addObject:[NSNumber numberWithInteger:treatment]]; + } +} + +- (void)popIssueTreatment +{ + if(![NSThread isMainThread]) + { + [NSException raise:NSInternalInconsistencyException format:@"popIssueTreatment can only be called on main thread"]; + } + + @synchronized(self) + { + if([issueTreatmentStack count] == 1) + { + [NSException raise:NSInternalInconsistencyException format:@"unbalanced calls to pushIssueTreatment/popIssueTreatment"]; + } + [issueTreatmentStack removeLastObject]; + } +} + +- (OCMIssueTreatment)issueTreatment +{ + OCMIssueTreatment treatment; + @synchronized(self) + { + treatment = [[issueTreatmentStack lastObject] integerValue]; + } + return treatment; +} +@end diff --git a/Source/OCMock/OCMock.h b/Source/OCMock/OCMock.h index 548e2147..d94e0d03 100644 --- a/Source/OCMock/OCMock.h +++ b/Source/OCMock/OCMock.h @@ -25,6 +25,7 @@ #import #import #import +#import #ifdef OCM_DISABLE_SHORT_SYNTAX diff --git a/Source/OCMock/OCMockObject.m b/Source/OCMock/OCMockObject.m index 26090ee2..237cf566 100644 --- a/Source/OCMock/OCMockObject.m +++ b/Source/OCMock/OCMockObject.m @@ -29,7 +29,7 @@ #import "OCMInvocationExpectation.h" #import "OCMExceptionReturnValueProvider.h" #import "OCMExpectationRecorder.h" - +#import "OCMIssueReporter.h" @implementation OCMockObject @@ -127,7 +127,10 @@ - (NSString *)description - (void)addStub:(OCMInvocationStub *)aStub { - [self assertInvocationsArrayIsPresent]; + if(![self checkThatInvocationsArrayIsPresent]) + { + return; + } @synchronized(stubs) { [stubs addObject:aStub]; @@ -142,12 +145,15 @@ - (void)addExpectation:(OCMInvocationExpectation *)anExpectation } } -- (void)assertInvocationsArrayIsPresent +- (BOOL)checkThatInvocationsArrayIsPresent { if(invocations == nil) { - [NSException raise:NSInternalInconsistencyException format:@"** Cannot use mock object %@ at %p. This error usually occurs when a mock object is used after stopMocking has been called on it. In most cases it is not necessary to call stopMocking. If you know you have to, please make sure that the mock object is not used afterwards.", [self description], (void *)self]; + NSString *reason = [NSString stringWithFormat:@"** Cannot use mock object %@ at %p. This issue usually occurs when a mock object is used after stopMocking has been called on it. In most cases it is not necessary to call stopMocking. If you know you have to, please make sure that the mock object is not used afterwards.", [self description], (void *)self]; + [[OCMIssueReporter defaultReporter] reportIssueInFile:__FILE__ line:__LINE__ exceptionName:NSInternalInconsistencyException reason:reason]; + return NO; } + return YES; } @@ -283,7 +289,10 @@ - (void)verifyInvocation:(OCMInvocationMatcher *)matcher atLocation:(OCMLocation - (void)verifyInvocation:(OCMInvocationMatcher *)matcher withQuantifier:(OCMQuantifier *)quantifier atLocation:(OCMLocation *)location { NSUInteger count = 0; - [self assertInvocationsArrayIsPresent]; + if(![self checkThatInvocationsArrayIsPresent]) + { + return; + } @synchronized(invocations) { for(NSInvocation *invocation in invocations) @@ -373,7 +382,10 @@ - (void)forwardInvocation:(NSInvocation *)anInvocation - (BOOL)handleInvocation:(NSInvocation *)anInvocation { - [self assertInvocationsArrayIsPresent]; + if(![self checkThatInvocationsArrayIsPresent]) + { + return NO; + } @synchronized(invocations) { // We can't do a normal retain arguments on anInvocation because its target/arguments/return diff --git a/Source/OCMockTests/OCMIssueReporterTests.m b/Source/OCMockTests/OCMIssueReporterTests.m new file mode 100644 index 00000000..3ec6011b --- /dev/null +++ b/Source/OCMockTests/OCMIssueReporterTests.m @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2013-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import +#import "OCMIssueReporter.h" +#import "OCMock.h" +#import "OCClassMockObject.h" + +@interface OCMIssueReporterTests : XCTestCase +@end + + +@implementation OCMIssueReporterTests + +- (void)testPushPopOfIssueTreatment +{ + // Default Settings + OCMIssueReporter *reporter = [OCMIssueReporter defaultReporter]; + XCTAssertEqual([reporter issueTreatment], OCMIssueTreatmentWarnings); + [reporter pushIssueTreatment:OCMIssueTreatmentWarnings]; + XCTAssertEqual([reporter issueTreatment], OCMIssueTreatmentWarnings); + [reporter popIssueTreatment]; + XCTAssertEqual([reporter issueTreatment], OCMIssueTreatmentWarnings); + [reporter pushIssueTreatment:OCMIssueTreatmentErrors]; + [reporter pushIssueTreatment:OCMIssueTreatmentErrors]; + XCTAssertEqual([reporter issueTreatment], OCMIssueTreatmentErrors); + [reporter popIssueTreatment]; + XCTAssertEqual([reporter issueTreatment], OCMIssueTreatmentErrors); + [reporter popIssueTreatment]; + XCTAssertEqual([reporter issueTreatment], OCMIssueTreatmentWarnings); +} + +- (void)testPushPopOfIssueTreatmentOnThreadThrows +{ + XCTestExpectation *finishedThreadExpectation = [self expectationWithDescription:@"Waiting On Thread"]; + [NSThread detachNewThreadWithBlock:^{ + OCMIssueReporter *reporter = [OCMIssueReporter defaultReporter]; + XCTAssertThrowsSpecificNamed([reporter pushIssueTreatment:OCMIssueTreatmentErrors], NSException, NSInternalInconsistencyException); + XCTAssertThrowsSpecificNamed([reporter popIssueTreatment], NSException, NSInternalInconsistencyException); + [finishedThreadExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testDefaultIssueReporterThrowsOnOverPop +{ + OCMIssueReporter *reporter = [OCMIssueReporter defaultReporter]; + XCTAssertThrowsSpecificNamed([reporter popIssueTreatment], NSException, NSInternalInconsistencyException); +} + +- (void)testRepectsEnvironmentSettings +{ + id processInfoMock = OCMClassMock([NSProcessInfo class]); + OCMStub([processInfoMock processInfo]).andReturn(processInfoMock); + OCMExpect([processInfoMock environment]).andReturn(@{OCMIssueTreatmentDefaultEnvironmentVariable: @(OCMIssueTreatmentErrors)}); + + OCMIssueReporter *reporter = [[OCMIssueReporter alloc] init]; + XCTAssertEqual([reporter issueTreatment], OCMIssueTreatmentErrors); +} + +- (void)testThrowsWithBadEnvironmentSettings +{ + id processInfoMock = OCMClassMock([NSProcessInfo class]); + OCMStub([processInfoMock processInfo]).andReturn(processInfoMock); + OCMExpect([processInfoMock environment]).andReturn(@{OCMIssueTreatmentDefaultEnvironmentVariable: @(5)}); + + XCTAssertThrowsSpecificNamed([[OCMIssueReporter alloc] init], NSException, NSInvalidArgumentException); + OCMVerifyAll(processInfoMock); +} + +- (void)testThrowsWhenIssuesAreErrors +{ + OCMIssueReporter *reporter = [OCMIssueReporter defaultReporter]; + [reporter pushIssueTreatment:OCMIssueTreatmentErrors]; + id stub = OCMClassMock([NSString class]); + [stub stopMocking]; + XCTAssertThrowsSpecificNamed([stub lowercaseString], NSException, NSInternalInconsistencyException); + [reporter popIssueTreatment]; +} + +- (void)testLogsWhenIssuesAreWarnings +{ + OCMIssueReporter *reporter = [OCMIssueReporter defaultReporter]; + [reporter pushIssueTreatment:OCMIssueTreatmentWarnings]; + id stub = OCMClassMock([NSString class]); + [stub stopMocking]; + XCTAssertNoThrow([stub lowercaseString]); + [reporter popIssueTreatment]; +} + +@end diff --git a/Source/OCMockTests/OCMockObjectClassMethodMockingTests.m b/Source/OCMockTests/OCMockObjectClassMethodMockingTests.m index 7ea2ebf2..1469c9ba 100644 --- a/Source/OCMockTests/OCMockObjectClassMethodMockingTests.m +++ b/Source/OCMockTests/OCMockObjectClassMethodMockingTests.m @@ -18,7 +18,7 @@ #import #import "OCClassMockObject.h" #import "OCPartialMockObject.h" - +#import "OCMIssueReporter.h" #pragma mark Helper classes @@ -370,9 +370,12 @@ - (void)testArgumentsGetReleasedAfterStopMocking - (void)testThrowsWhenAttemptingToStubClassMethodOnStoppedMock { + OCMIssueReporter *reporter = [OCMIssueReporter defaultReporter]; + [reporter pushIssueTreatment:OCMIssueTreatmentErrors]; id mock = [OCClassMockObject mockForClass:[TestClassWithClassMethods class]]; [mock stopMocking]; XCTAssertThrowsSpecificNamed([[[mock stub] andReturn:@"hello"] foo], NSException, NSInternalInconsistencyException); + [reporter popIssueTreatment]; } @end diff --git a/Source/OCMockTests/OCMockObjectInternalTests.m b/Source/OCMockTests/OCMockObjectInternalTests.m index e8000dc0..f4710819 100644 --- a/Source/OCMockTests/OCMockObjectInternalTests.m +++ b/Source/OCMockTests/OCMockObjectInternalTests.m @@ -16,7 +16,7 @@ #import #import - +#import "OCMIssueReporter.h" #pragma mark Helper classes @@ -201,6 +201,8 @@ - (void)testArgumentsGetReleasedAfterStopMocking - (void)testRaisesWhenAttemptingToVerifyInvocationsAfterStopMocking { + OCMIssueReporter *reporter = [OCMIssueReporter defaultReporter]; + [reporter pushIssueTreatment:OCMIssueTreatmentErrors]; id mock = OCMClassMock([TestClassForInternalTests class]); [mock title]; @@ -213,10 +215,13 @@ - (void)testRaisesWhenAttemptingToVerifyInvocationsAfterStopMocking XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); XCTAssertTrue([ex.reason containsString:[mock description]]); } + [reporter popIssueTreatment]; } - (void)testRaisesWhenAttemptingToUseAfterStopMocking { + OCMIssueReporter *reporter = [OCMIssueReporter defaultReporter]; + [reporter pushIssueTreatment:OCMIssueTreatmentErrors]; id mock = OCMClassMock([TestClassForInternalTests class]); [mock stopMocking]; @@ -228,6 +233,7 @@ - (void)testRaisesWhenAttemptingToUseAfterStopMocking XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); XCTAssertTrue([ex.reason containsString:[mock description]]); } + [reporter popIssueTreatment]; } diff --git a/Source/OCMockTests/OCMockObjectTests.m b/Source/OCMockTests/OCMockObjectTests.m index 3a9b8dcb..66d8f6bf 100644 --- a/Source/OCMockTests/OCMockObjectTests.m +++ b/Source/OCMockTests/OCMockObjectTests.m @@ -17,7 +17,7 @@ #import #import #import "OCMBoxedReturnValueProvider.h" - +#import "OCMIssueReporter.h" #pragma mark Helper classes and protocols for testing @@ -475,8 +475,11 @@ - (void)testBlocksAreNotConsideredNonObjectArguments - (void)testThrowsWhenAttemptingToStubMethodOnStoppedMock { - [mock stopMocking]; - XCTAssertThrowsSpecificNamed([[mock stub] rangeOfString:@"foo" options:0], NSException, NSInternalInconsistencyException); + OCMIssueReporter *reporter = [OCMIssueReporter defaultReporter]; + [reporter pushIssueTreatment:OCMIssueTreatmentErrors]; + [mock stopMocking]; + XCTAssertThrowsSpecificNamed([[mock stub] rangeOfString:@"foo" options:0], NSException, NSInternalInconsistencyException); + [reporter popIssueTreatment]; } diff --git a/Source/OCMockTests/OCMockObjectVerifyAfterRunTests.m b/Source/OCMockTests/OCMockObjectVerifyAfterRunTests.m index be0899f1..e6e2cea9 100644 --- a/Source/OCMockTests/OCMockObjectVerifyAfterRunTests.m +++ b/Source/OCMockTests/OCMockObjectVerifyAfterRunTests.m @@ -17,6 +17,7 @@ #import #import "OCMockObject.h" #import "OCMStubRecorder.h" +#import "OCMIssueReporter.h" @interface TestBaseClassForVerifyAfterRun : NSObject @@ -119,6 +120,8 @@ - (void)testThrowsWhenClassMethodWasNotInvoked - (void)testThrowsWhenVerificationIsAttemptedAfterStopMocking { + OCMIssueReporter *reporter = [OCMIssueReporter defaultReporter]; + [reporter pushIssueTreatment:OCMIssueTreatmentErrors]; id mock = [OCMockObject niceMockForClass:[TestBaseClassForVerifyAfterRun class]]; [TestBaseClassForVerifyAfterRun classMethod1]; @@ -134,6 +137,7 @@ - (void)testThrowsWhenVerificationIsAttemptedAfterStopMocking XCTAssertEqualObjects([e name], NSInternalInconsistencyException); XCTAssertTrue([[e reason] containsString:@"after stopMocking has been called"]); } + [reporter popIssueTreatment]; }