chromium/ios/testing/protocol_fake.mm

// 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