// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/testing/protocol_fake.h"
#import <objc/runtime.h>
#import "base/logging.h"
#import "base/strings/sys_string_conversions.h"
namespace {
// Opaque value to use as an associated object key.
char kAssociatedProtocolNameKey;
} // namespace
@interface NSInvocation (Description)
// Returns a string description of the invocation consisting of the selector
// name interspersed with argument values.
- (NSString*)crsc_description;
@end
@interface ProtocolFake () {
NSSet<Protocol*>* _protocols;
// Selectors for which no logging should be done.
NSMutableSet<NSValue*>* _ignoredSelectors;
// Count of selector calls.
NSMutableDictionary<NSValue*, NSNumber*>* _callCounts;
}
@end
@implementation ProtocolFake
@synthesize baseViewController = _baseViewController;
- (instancetype)initWithProtocols:(NSArray<Protocol*>*)protocols {
if (!protocols) {
return nil;
}
// NSProxy isn't a subclass of NSObject, and has no superclass, so
// there's no [super init] to call.
_protocols = [[NSSet<Protocol*> alloc] initWithArray:protocols];
_ignoredSelectors = [NSMutableSet set];
_callCounts = [NSMutableDictionary dictionary];
// Log by default.
_logs = YES;
return self;
}
- (NSInteger)callCountForSelector:(SEL)sel {
return _callCounts[[NSValue valueWithPointer:sel]].integerValue;
}
- (void)ignoreSelector:(SEL)sel {
[_ignoredSelectors addObject:[NSValue valueWithPointer:sel]];
}
#pragma mark - NSProxy
- (NSMethodSignature*)methodSignatureForSelector:(SEL)sel {
for (Protocol* protocol in _protocols) {
const BOOL kYesAndNoArray[] = {YES, NO};
for (BOOL required : kYesAndNoArray) {
struct objc_method_description method = protocol_getMethodDescription(
protocol, sel, required, YES /* an instance method */);
if (method.name != NULL) {
NSMethodSignature* signature =
[NSMethodSignature signatureWithObjCTypes:method.types];
// Tag the method signature with the protocol name.
objc_setAssociatedObject(signature, &kAssociatedProtocolNameKey,
NSStringFromProtocol(protocol),
OBJC_ASSOCIATION_COPY_NONATOMIC);
return signature;
}
}
}
return nil;
}
- (void)forwardInvocation:(NSInvocation*)invocation {
NSValue* selectorValue = [NSValue valueWithPointer:invocation.selector];
if ([_ignoredSelectors containsObject:selectorValue]) {
return;
}
NSNumber* count = _callCounts[selectorValue];
if (count) {
_callCounts[selectorValue] = @(count.integerValue + 1);
} else {
_callCounts[selectorValue] = @1;
}
// Instead of actually doing anything the protocol method would normally
// do, instead just generate a title and description and display an alert or
// log a message.
NSString* protocolName = objc_getAssociatedObject(
[self methodSignatureForSelector:invocation.selector],
&kAssociatedProtocolNameKey);
NSString* description = [invocation crsc_description];
if (self.alerts && self.baseViewController) {
[self showAlertWithTitle:protocolName message:description];
}
if (self.logs) {
VLOG(0) << "Alerter -- protocol:" << base::SysNSStringToUTF8(protocolName);
VLOG(0) << "Alerter -- invocation:" << base::SysNSStringToUTF8(description);
}
}
#pragma mark - NSObject
- (BOOL)conformsToProtocol:(Protocol*)aProtocol {
for (Protocol* protocol in _protocols) {
// Handle protocols that conform to other protocols.
if (protocol_conformsToProtocol(protocol, aProtocol)) {
return YES;
}
}
return NO;
}
- (BOOL)respondsToSelector:(SEL)aSelector {
return [self methodSignatureForSelector:aSelector] != nil;
}
#pragma mark - Private
// Helper to show a simple alert.
- (void)showAlertWithTitle:(NSString*)title message:(NSString*)message {
UIAlertController* alertController =
[UIAlertController alertControllerWithTitle:title
message:message
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* action =
[UIAlertAction actionWithTitle:@"Done"
style:UIAlertActionStyleCancel
handler:nil];
[action setAccessibilityLabel:@"protocol_alerter_done"];
[alertController addAction:action];
[self.baseViewController presentViewController:alertController
animated:YES
completion:nil];
}
@end
@implementation NSInvocation (Description)
- (NSString*)crsc_description {
NSInteger arguments = self.methodSignature.numberOfArguments;
NSString* selector = NSStringFromSelector(self.selector);
// NSInvocation's first two arguments are `self` and `_cmd`; if they are the
// only ones, then the invocation has no actual arguments.
if (arguments == 2) {
return selector;
}
// Get the parts of the selector name by splitting on /:/, and dropping the
// last (empty) part.
NSArray* keywords = [[selector componentsSeparatedByString:@":"]
subarrayWithRange:NSMakeRange(0, arguments - 2)];
NSMutableString* description = [[NSMutableString alloc] init];
NSInteger argumentIndex = 2;
for (NSString* keyword in keywords) {
// Insert a space before each keyword after the first one.
if (description.length) {
[description appendString:@" "];
}
[description appendString:keyword];
[description appendString:@":"];
[description
appendString:[self crsc_argumentDescriptionAtIndex:argumentIndex]];
argumentIndex++;
}
return description;
}
// Return a string describing the argument value at `index`.
// (`index` is in NSInvocation's argument array).
- (NSString*)crsc_argumentDescriptionAtIndex:(NSInteger)index {
const char* type = [self.methodSignature getArgumentTypeAtIndex:index];
switch (*type) {
case '@':
return [self crsc_objectDescriptionAtIndex:index];
case 'q':
return [self crsc_int64DescriptionAtIndex:index];
case 'Q':
return [self crsc_unsignedInt64DescriptionAtIndex:index];
// Add cases as needed here.
default:
return [NSString stringWithFormat:@"<Unknown Type:%s>", type];
}
}
// Return a string describing an argument at `index` that's known to be an
// objective-C object.
- (NSString*)crsc_objectDescriptionAtIndex:(NSInteger)index {
// This is one of the few cases where __unsafe_unretained is correct; this
// allocates memory for an empty generic object that `getArgument:` will
// write in to.
__unsafe_unretained id object;
[self getArgument:&object atIndex:index];
if (!object) {
return @"nil";
}
NSString* description = [object description];
NSString* className = NSStringFromClass([object class]);
if (!description) {
return
[NSString stringWithFormat:@"<%@ object, no description>", className];
}
// Wrap strings in @" ... ".
if ([object isKindOfClass:[NSString class]]) {
return [NSString stringWithFormat:@"@\"%@\"", description];
}
// Remove the address of objects from their descriptions, so (for example):
// <NSObject: 0xc00lf0ccac1a>
// becomes just:
// <NSObject>
NSRange range = NSMakeRange(0, description.length);
NSString* classPlusAddress =
[className stringByAppendingString:@": 0x[0-9a-c]+"];
return [description
stringByReplacingOccurrencesOfString:classPlusAddress
withString:className
options:NSRegularExpressionSearch
range:range];
}
// Returns a string describing an argument at `index` that is known to be a
// 64-bit integer (a "long long").
- (NSString*)crsc_int64DescriptionAtIndex:(NSInteger)index {
int64_t value;
[self getArgument:&value atIndex:index];
return [NSString stringWithFormat:@"%lld", value];
}
// Returns a string describing an argument at `index` that is known to be an
// unsigned 64-bit integer (an "unsigned long long").
- (NSString*)crsc_unsignedInt64DescriptionAtIndex:(NSInteger)index {
uint64_t value;
[self getArgument:&value atIndex:index];
return [NSString stringWithFormat:@"%llu", value];
}
@end