From 91fdd6d09d6390b67ff3258b6418437dca11d6e1 Mon Sep 17 00:00:00 2001 From: Thomas Van Lenten Date: Fri, 29 Apr 2016 13:47:13 -0400 Subject: Add GTMSynchronizationAsserts.h/m and a unit test file. These macros allow code to assert being in or not in a @sync-protected section, which is important when calling across methods or calling out to other classes or callbacks. --- DebugUtils/GTMSynchronizationAsserts.h | 124 +++++++++++++ DebugUtils/GTMSynchronizationAsserts.m | 93 ++++++++++ DebugUtils/GTMSynchronizationAssertsTest.m | 288 +++++++++++++++++++++++++++++ 3 files changed, 505 insertions(+) create mode 100644 DebugUtils/GTMSynchronizationAsserts.h create mode 100644 DebugUtils/GTMSynchronizationAsserts.m create mode 100644 DebugUtils/GTMSynchronizationAssertsTest.m diff --git a/DebugUtils/GTMSynchronizationAsserts.h b/DebugUtils/GTMSynchronizationAsserts.h new file mode 100644 index 0000000..b026565 --- /dev/null +++ b/DebugUtils/GTMSynchronizationAsserts.h @@ -0,0 +1,124 @@ +// +// GTMSynchronizationAsserts.h +// +// Copyright 2016 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file 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. +// + +#if !__has_feature(objc_arc) +#error "This file needs to be compiled with ARC enabled." +#endif + +#import + +#import "GTMDefines.h" // For _GTMDevAssert. + +// Macros to monitor synchronization blocks in debug builds. +// +// These report problems using _GTMDevAssert, which may be defined by the +// project or by GTMDefines.h +// +// GTMMonitorSynchronized Start monitoring a top-level-only @sync scope. +// Asserts if already inside a monitored @sync scope. +// GTMMonitorRecursiveSynchronized Start monitoring a top-level or recursive @sync +// scope. +// GTMCheckSynchronized Assert that the current execution is inside a monitored @sync +// scope. +// GTMCheckNotSynchronized Assert that the current execution is not inside a monitored +// @sync scope. +// +// Example usage: +// +// - (void)myExternalMethod { +// @synchronized(self) { +// GTMMonitorSynchronized(self) +// +// - (void)myInternalMethod { +// GTMCheckSynchronized(self); +// +// - (void)callMyCallbacks { +// GTMCheckNotSynchronized(self); +// +// GTMCheckNotSynchronized is available for verifying the code isn't +// in a deadlockable @sync state, important when posting notifications and +// invoking callbacks. +// +// Don't use GTMCheckNotSynchronized immediately before a @sync scope; the +// normal recursiveness check of GTMMonitorSynchronized can catch those. + +#if DEBUG + + #define __GTMMonitorSynchronizedVariableInner(varname, counter) \ + varname ## counter + #define __GTMMonitorSynchronizedVariable(varname, counter) \ + __GTMMonitorSynchronizedVariableInner(varname, counter) + + #define GTMMonitorSynchronized(obj) \ + NS_VALID_UNTIL_END_OF_SCOPE id \ + __GTMMonitorSynchronizedVariable(__monitor, __COUNTER__) = \ + [[GTMSyncMonitorInternal alloc] initWithSynchronizationObject:obj \ + allowRecursive:NO \ + functionName:__func__] + + #define GTMMonitorRecursiveSynchronized(obj) \ + NS_VALID_UNTIL_END_OF_SCOPE id \ + __GTMMonitorSynchronizedVariable(__monitor, __COUNTER__) = \ + [[GTMSyncMonitorInternal alloc] initWithSynchronizationObject:obj \ + allowRecursive:YES \ + functionName:__func__] + + #define GTMCheckSynchronized(obj) { \ + _GTMDevAssert( \ + [GTMSyncMonitorInternal functionsHoldingSynchronizationOnObject:obj], \ + @"GTMCheckSynchronized(" #obj ") failed: not sync'd" \ + @" on " #obj " in %s. Call stack:\n%@", \ + __func__, [NSThread callStackSymbols]); \ + } + + #define GTMCheckNotSynchronized(obj) { \ + _GTMDevAssert( \ + ![GTMSyncMonitorInternal functionsHoldingSynchronizationOnObject:obj], \ + @"GTMCheckNotSynchronized(" #obj ") failed: was sync'd" \ + @" on " #obj " in %s by %@. Call stack:\n%@", __func__, \ + [GTMSyncMonitorInternal functionsHoldingSynchronizationOnObject:obj], \ + [NSThread callStackSymbols]); \ + } + +// GTMSyncMonitorInternal is a private class that keeps track of the +// beginning and end of synchronized scopes, relying on ARC to release +// it at the end of a scope. +// +// This class should not be used directly, but only via the +// GTMMonitorSynchronized macro. +@interface GTMSyncMonitorInternal : NSObject { + NSValue *_objectKey; // The synchronize target object. + const char *_functionName; // The function containing the monitored sync block. +} + +- (instancetype)initWithSynchronizationObject:(id)object + allowRecursive:(BOOL)allowRecursive + functionName:(const char *)functionName; +// Return the names of the functions that hold sync on the object, or nil if none. ++ (NSArray *)functionsHoldingSynchronizationOnObject:(id)object; +@end + +#else + + // !DEBUG + #define GTMMonitorSynchronized(obj) do { } while (0) + #define GTMMonitorRecursiveSynchronized(obj) do { } while (0) + #define GTMCheckSynchronized(obj) do { } while (0) + #define GTMCheckNotSynchronized(obj) do { } while (0) + +#endif // DEBUG diff --git a/DebugUtils/GTMSynchronizationAsserts.m b/DebugUtils/GTMSynchronizationAsserts.m new file mode 100644 index 0000000..6c76550 --- /dev/null +++ b/DebugUtils/GTMSynchronizationAsserts.m @@ -0,0 +1,93 @@ +// +// GTMSyncAsserts.m +// +// Copyright 2016 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file 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 "GTMSynchronizationAsserts.h" + +#if DEBUG + +@implementation GTMSyncMonitorInternal + +- (instancetype)initWithSynchronizationObject:(id)object + allowRecursive:(BOOL)allowRecursive + functionName:(const char *)functionName { + self = [super init]; + if (self) { + // In the thread's dictionary, we keep a counted set of the names + // of functions that are synchronizing on the object. + Class threadKey = [GTMSyncMonitorInternal class]; + _objectKey = [NSValue valueWithNonretainedObject:object]; + _functionName = functionName; + + NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary; + NSMutableDictionary *counters = threadDict[threadKey]; + if (counters == nil) { + counters = [NSMutableDictionary dictionary]; + threadDict[(id)threadKey] = counters; + } + NSCountedSet *functionNamesCounter = counters[_objectKey]; + NSUInteger numberOfSyncingFunctions = functionNamesCounter.count; + + if (!allowRecursive) { + BOOL isTopLevelSyncScope = (numberOfSyncingFunctions == 0); + NSArray *stack = [NSThread callStackSymbols]; + _GTMDevAssert(isTopLevelSyncScope, + @"*** Recursive sync on %@ at %s; previous sync at %@\n%@", + [object class], functionName, functionNamesCounter.allObjects, + [stack subarrayWithRange:NSMakeRange(1, stack.count - 1)]); + } + + if (!functionNamesCounter) { + functionNamesCounter = [NSCountedSet set]; + counters[_objectKey] = functionNamesCounter; + } + [functionNamesCounter addObject:@(functionName)]; + } + return self; +} + +- (void)dealloc { + Class threadKey = [GTMSyncMonitorInternal class]; + + NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary; + NSMutableDictionary *counters = threadDict[threadKey]; + NSCountedSet *functionNamesCounter = counters[_objectKey]; + NSString *functionNameStr = @(_functionName); + NSUInteger numberOfSyncsByThisFunction = [functionNamesCounter countForObject:functionNameStr]; + NSArray *stack = [NSThread callStackSymbols]; + _GTMDevAssert(numberOfSyncsByThisFunction > 0, @"Sync not found on %@ at %s\n%@", + [_objectKey.nonretainedObjectValue class], _functionName, + [stack subarrayWithRange:NSMakeRange(1, stack.count - 1)]); + [functionNamesCounter removeObject:functionNameStr]; + if (functionNamesCounter.count == 0) { + [counters removeObjectForKey:_objectKey]; + } +} + ++ (NSArray *)functionsHoldingSynchronizationOnObject:(id)object { + Class threadKey = [GTMSyncMonitorInternal class]; + NSValue *localObjectKey = [NSValue valueWithNonretainedObject:object]; + + NSMutableDictionary *threadDict = [NSThread currentThread].threadDictionary; + NSMutableDictionary *counters = threadDict[threadKey]; + NSCountedSet *functionNamesCounter = counters[localObjectKey]; + return functionNamesCounter.count > 0 ? functionNamesCounter.allObjects : nil; +} + +@end + +#endif // DEBUG diff --git a/DebugUtils/GTMSynchronizationAssertsTest.m b/DebugUtils/GTMSynchronizationAssertsTest.m new file mode 100644 index 0000000..f08fc3e --- /dev/null +++ b/DebugUtils/GTMSynchronizationAssertsTest.m @@ -0,0 +1,288 @@ +/* Copyright (c) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file 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 + +// For testing, force use of the default debug versions of the _GTMDevAssert macro. +#undef _GTMDevAssert +#undef NS_BLOCK_ASSERTIONS +#undef DEBUG +#define DEBUG 1 + +#import "GTMSynchronizationAsserts.h" + +@interface GTMSynchonizationAssertsTest : XCTestCase +@end + +@implementation GTMSynchonizationAssertsTest + +- (void)verifySynchronized { + // Test both GTMCheckSynchronized and GTMCheckNotSynchronized assuming we're in a sync block. + @try { + GTMCheckSynchronized(self); + } @catch (NSException *exception) { + XCTFail(@"shouldn't have thrown"); + } + + @try { + GTMCheckNotSynchronized(self); + XCTFail(@"should have thrown"); + } @catch (NSException *exception) { + } +} + +- (void)verifyNotSynchronized { + // Test both GTMCheckSynchronized and GTMCheckNotSynchronized assuming we're not in a sync block. + @try { + GTMCheckNotSynchronized(self); + } @catch (NSException *exception) { + XCTFail(@"shouldn't have thrown"); + } + + @try { + GTMCheckSynchronized(self); + XCTFail(@"shoul have thrown"); + } @catch (NSException *exception) { + } +} + +- (void)testChecks_SingleMethod { + [self verifyNotSynchronized]; + + @synchronized(self) { + GTMMonitorSynchronized(self); + [self verifySynchronized]; + + @synchronized(self) { + GTMMonitorRecursiveSynchronized(self); + [self verifySynchronized]; + + @synchronized(self) { + GTMMonitorRecursiveSynchronized(self); + [self verifySynchronized]; + } + } + } + [self verifyNotSynchronized]; +} + +- (void)testChecks_AcrossMethods { + [self doIndirectCheckNotSynchronized]; + + @synchronized(self) { + GTMMonitorSynchronized(self); + [self verifySynchronized]; + + @synchronized(self) { + GTMMonitorRecursiveSynchronized(self); + [self doIndirectCheckSynchronized]; + + @synchronized(self) { + GTMMonitorRecursiveSynchronized(self); + [self doIndirectCheckSynchronized]; + } + } + } + [self doIndirectCheckNotSynchronized]; +} + +- (void)doIndirectCheckSynchronized { + // Verify from a separate method. + [self verifySynchronized]; +} + +- (void)doIndirectCheckNotSynchronized { + // Verify from a separate method. + [self verifyNotSynchronized]; +} + +#pragma mark Sync Monitor Tests + +- (void)testNonrecursiveSync { + // Non-recursive monitors. + XCTestExpectation *outer = [self expectationWithDescription:@"outer"]; + XCTestExpectation *inner = [self expectationWithDescription:@"inner"]; + + @try { + @synchronized(self) { + GTMMonitorSynchronized(self); + [outer fulfill]; + + @synchronized(self) { + GTMMonitorSynchronized(self); + XCTFail(@"should have thrown"); + } + } + } @catch (NSException *exception) { + [inner fulfill]; + } + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testRecursiveSync_SingleMethod { + // The inner monitors are recursive. + XCTestExpectation *outer = [self expectationWithDescription:@"outer"]; + XCTestExpectation *inner1 = [self expectationWithDescription:@"inner1"]; + XCTestExpectation *inner2 = [self expectationWithDescription:@"inner2"]; + + @try { + @synchronized(self) { + GTMMonitorSynchronized(self); + [outer fulfill]; + + @synchronized(self) { + GTMMonitorRecursiveSynchronized(self); + [inner1 fulfill]; + + @synchronized(self) { + GTMMonitorRecursiveSynchronized(self); + [inner2 fulfill]; + } + } + } + } @catch (NSException *exception) { + XCTFail(@"shouldn't have thrown"); + } + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testRecursiveSync_AcrossMethods { + // The inner monitors are recursive. + XCTestExpectation *outer = [self expectationWithDescription:@"outer"]; + XCTestExpectation *inner1 = [self expectationWithDescription:@"inner1"]; + XCTestExpectation *inner2 = [self expectationWithDescription:@"inner2"]; + + @try { + @synchronized(self) { + GTMMonitorSynchronized(self); + [outer fulfill]; + + @synchronized(self) { + GTMMonitorRecursiveSynchronized(self); + [inner1 fulfill]; + + [self doInnerRecursiveSync]; + [inner2 fulfill]; + } + } + } @catch (NSException *exception) { + XCTFail(@"shouldn't have thrown"); + } + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)doInnerRecursiveSync { + @synchronized(self) { + GTMMonitorRecursiveSynchronized(self); + } +} + +- (void)testRecursiveThenNonrecursiveSync_SingleMethod { + // The outer monitors are recursive, but the inner one is not and should throw. + XCTestExpectation *outer1 = [self expectationWithDescription:@"outer1"]; + XCTestExpectation *outer2 = [self expectationWithDescription:@"outer2"]; + XCTestExpectation *inner = [self expectationWithDescription:@"inner"]; + + @try { + @synchronized(self) { + GTMMonitorRecursiveSynchronized(self); + [outer1 fulfill]; + + @synchronized(self) { + GTMMonitorRecursiveSynchronized(self); + [outer2 fulfill]; + + @synchronized(self) { + GTMMonitorSynchronized(self); + XCTFail(@"should have thrown"); + } + } + } + } @catch (NSException *exception) { + [inner fulfill]; + } + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testRecursiveThenNonrecursiveSync_AcrossMethods { + // The outer monitors are recursive, but the inner one is not and should throw. + XCTestExpectation *outer1 = [self expectationWithDescription:@"outer1"]; + XCTestExpectation *outer2 = [self expectationWithDescription:@"outer2"]; + XCTestExpectation *inner = [self expectationWithDescription:@"inner"]; + + @try { + @synchronized(self) { + GTMMonitorRecursiveSynchronized(self); + [outer1 fulfill]; + + @synchronized(self) { + GTMMonitorRecursiveSynchronized(self); + [outer2 fulfill]; + + [self doInnerNonrecursiveSync]; + } + } + } @catch (NSException *exception) { + [inner fulfill]; + } + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)doInnerNonrecursiveSync { + @synchronized(self) { + GTMMonitorSynchronized(self); + XCTFail(@"should have thrown"); + } +} + +- (void)testSyncOnSeparateObjects { + // Verify that monitoring works for distinct sync objects. + XCTestExpectation *outer = [self expectationWithDescription:@"outer"]; + XCTestExpectation *innerA = [self expectationWithDescription:@"innerA"]; + XCTestExpectation *innerB = [self expectationWithDescription:@"innerB"]; + + id obj1 = [[NSObject alloc] init]; + id obj2 = [[NSObject alloc] init]; + + @try { + @synchronized(obj1) { + GTMMonitorSynchronized(obj1); + [outer fulfill]; + + @synchronized(obj2) { + GTMMonitorSynchronized(obj2); + [innerA fulfill]; + + @synchronized(obj1) { + GTMMonitorSynchronized(obj1); + XCTFail(@"should have thrown"); + } + } + } + } @catch (NSException *exception) { + [innerB fulfill]; + } + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +@end -- cgit v1.2.3