aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Thomas Van Lenten <thomasvl@google.com>2016-04-29 13:47:13 -0400
committerGravatar Thomas Van Lenten <thomasvl@google.com>2016-04-29 13:47:13 -0400
commit91fdd6d09d6390b67ff3258b6418437dca11d6e1 (patch)
treeacf04a347085046a74a1ab14b8c1ee33b314b8fa
parentc485d79b9289a4f6ff1402babbf44b6fba0aa6e7 (diff)
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.
-rw-r--r--DebugUtils/GTMSynchronizationAsserts.h124
-rw-r--r--DebugUtils/GTMSynchronizationAsserts.m93
-rw-r--r--DebugUtils/GTMSynchronizationAssertsTest.m288
3 files changed, 505 insertions, 0 deletions
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 <Foundation/Foundation.h>
+
+#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 <XCTest/XCTest.h>
+
+#import <objc/runtime.h>
+
+// 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