blob: a454d33688ec4273589bbbaa8215edb94d3096a8 [file] [log] [blame]
//
// Copyright 2018 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>
#import "Channel/Sources/EDOHostPort.h"
#import "Service/Sources/EDOClassMessage.h"
#import "Service/Sources/EDOClientService.h"
#import "Service/Sources/EDOHostService+Private.h"
#import "Service/Sources/EDOHostService.h"
#import "Service/Sources/EDOInvocationMessage.h"
#import "Service/Sources/EDOMethodSignatureMessage.h"
#import "Service/Sources/EDOObject+Private.h"
#import "Service/Sources/EDOObject.h"
#import "Service/Sources/EDOObjectMessage.h"
#import "Service/Sources/EDOParameter.h" // IWYU pragma: keep
#import "Service/Sources/EDORemoteException.h"
#import "Service/Sources/EDOServicePort.h"
#import "Service/Tests/TestsBundle/EDOTestDummy.h"
@interface EDOMessageTest : XCTestCase
@end
@implementation EDOMessageTest
// To make the compiler be aware of this selector's exsitence.
- (void)nonExistMethod {
}
/**
* Check if the @c class implements +supportsSecureCoding on its own.
*
* @note @c NSSecureCoding requires the subclasses also implements +supportsSecureCoding, so it
* needs to manually to look up the methods in the class itself.
*/
- (BOOL)implementsSecureCoding:(Class)class {
unsigned int methodCount = 0;
Class dummyMeta = object_getClass(class);
// Only iterates over the class methods defined in this class only.
Method *methods = class_copyMethodList(dummyMeta, &methodCount);
const char *secureCodingSelectorName = sel_getName(@selector(supportsSecureCoding));
BOOL found = NO;
for (unsigned int i = 0; i < methodCount; i++) {
Method method = methods[i];
char const *selectorName = sel_getName(method_getName(method));
if (strcmp(selectorName, secureCodingSelectorName) == 0) {
found = YES;
break;
}
}
free(methods);
return found;
}
/** Tests that all serializable EDO classes conform to NSSecureCoding. */
- (void)testAllEDOClassesSupportNSSecureCoding {
int numClasses = objc_getClassList(NULL, 0);
Class *classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses);
numClasses = objc_getClassList(classes, numClasses);
for (int i = 0; i < numClasses; i++) {
Class klass = classes[i];
// Only check classes that start with EDO and conforms to NSCoding.
// TODO(haowoo): We need to skip NSException as it's not securely coded.
if (memcmp(class_getName(klass), "EDO", 3) != 0 ||
![klass conformsToProtocol:@protocol(NSCoding)] ||
strcmp(class_getName(klass), "EDOTestDummyException") == 0) {
continue;
}
XCTAssertTrue([klass conformsToProtocol:@protocol(NSSecureCoding)],
@"class %s doesn't conform to NSSecureCoding", class_getName(klass));
XCTAssertTrue([self implementsSecureCoding:classes[i]],
@"class %s doesn't implement supportsSecureCoding", class_getName(klass));
XCTAssertTrue([classes[i] supportsSecureCoding],
@"supportsSecureCoding returns NO for class %s", class_getName(klass));
}
free(classes);
}
- (void)testObjectRequestHandler {
XCTestExpectation *blockExecuted = [self expectationWithDescription:@"Executed the test block."];
id dummyLocal = [[EDOTestDummy alloc] init];
[self
edo_createQueueAndServiceWithRootObject:dummyLocal
block:^(EDOHostService *service) {
EDOObjectResponse *response =
(EDOObjectResponse *)EDOObjectRequest.requestHandler(
[EDOObjectRequest
requestWithHostPort:service.port.hostPort],
service);
XCTAssertEqualObjects([response class],
[EDOObjectResponse class]);
EDOObject *object = response.object;
XCTAssertEqual(object.remoteAddress,
(EDOPointerType)dummyLocal);
XCTAssertEqual(object.remoteClass,
(EDOPointerType)[dummyLocal class]);
[blockExecuted fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testClassRequestHandler {
XCTestExpectation *blockExecuted = [self expectationWithDescription:@"Executed the test block."];
id dummyLocal = [[EDOTestDummy alloc] init];
Class dummyMeta = object_getClass([dummyLocal class]);
[self
edo_createQueueAndServiceWithRootObject:dummyLocal
block:^(EDOHostService *service) {
{
EDOServiceRequest *request = [EDOClassRequest
requestWithClassName:@"EDOTestDummy"
hostPort:service.port.hostPort];
EDOClassResponse *response =
(EDOClassResponse *)EDOClassRequest.requestHandler(
request, service);
XCTAssertEqualObjects([response class],
[EDOClassResponse class]);
EDOObject *object = response.object;
XCTAssertTrue(object.remoteAddress ==
(EDOPointerType)[dummyLocal class]);
XCTAssertTrue(object.remoteClass ==
(EDOPointerType)dummyMeta);
}
{
EDOServiceRequest *request = [EDOClassRequest
requestWithClassName:@"NonExistTestClass"
hostPort:service.port.hostPort];
EDOClassResponse *response =
(EDOClassResponse *)EDOClassRequest.requestHandler(
request, service);
XCTAssertNotNil(response);
XCTAssertNil(response.object);
}
[blockExecuted fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testNoArgsInvocationHandler {
XCTestExpectation *blockExecuted = [self expectationWithDescription:@"Executed the test block."];
EDOTestDummy *dummyLocal = [[EDOTestDummy alloc] init];
[self edo_createQueueAndServiceWithRootObject:dummyLocal
block:^(EDOHostService *service) {
EDOInvocationResponse *response =
[self edo_runInvocationWithService:service
target:dummyLocal
selector:@selector
(voidWithValuePlusOne)
arguments:@[]];
XCTAssertNil(response.exception);
XCTAssertNil(response.returnValue);
XCTAssertNil(response.outValues);
[blockExecuted fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testInvocationWithArgsHandler {
XCTestExpectation *blockExecuted = [self expectationWithDescription:@"Executed the test block."];
EDOTestDummy *dummyLocal = [[EDOTestDummy alloc] initWithValue:5];
[self
edo_createQueueAndServiceWithRootObject:dummyLocal
block:^(EDOHostService *service) {
{
EDOInvocationResponse *response = [self
edo_runInvocationWithService:service
target:dummyLocal
selector:@selector(voidWithInt:)
arguments:@[ [self
edo_intValue:8] ]];
XCTAssertNil(response.exception);
XCTAssertNil(response.returnValue);
XCTAssertNil(response.outValues);
}
{
EDOInvocationResponse *response =
[self edo_runInvocationWithService:service
target:dummyLocal
selector:@selector
(voidWithNumber:)
arguments:@[
[self edo_numberValue:@9]
]];
XCTAssertNil(response.exception);
XCTAssertNil(response.returnValue);
XCTAssertNil(response.outValues);
}
[blockExecuted fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testInvocationWithReturnHandler {
XCTestExpectation *blockExecuted = [self expectationWithDescription:@"Executed the test block."];
EDOTestDummy *dummyLocal = [[EDOTestDummy alloc] initWithValue:100];
[self
edo_createQueueAndServiceWithRootObject:dummyLocal
block:^(EDOHostService *service) {
{
EDOInvocationResponse *response = [self
edo_runInvocationWithService:service
target:dummyLocal
selector:@selector(returnInt)
arguments:@[]];
int returnValue = 0;
XCTAssert(strcmp(response.returnValue.objCType,
@encode(int)) == 0);
[response.returnValue getValue:&returnValue];
XCTAssertEqual(returnValue, 100);
XCTAssertNil(response.exception);
XCTAssertNil(response.outValues);
}
{
EDOInvocationResponse *response = [self
edo_runInvocationWithService:service
target:dummyLocal
selector:@selector(returnNumber)
arguments:@[]];
NSNumber __unsafe_unretained *returnValue;
XCTAssert(strcmp(response.returnValue.objCType,
@encode(NSNumber *)) == 0);
[response.returnValue getValue:&returnValue];
XCTAssertEqualObjects(returnValue,
[NSNumber numberWithInteger:100]);
XCTAssertNil(response.exception);
XCTAssertNil(response.outValues);
}
{
EDOInvocationResponse *response = [self
edo_runInvocationWithService:service
target:dummyLocal
selector:@selector(returnIdNil)
arguments:@[]];
id __unsafe_unretained returnValue;
XCTAssert(strcmp(response.returnValue.objCType,
@encode(id)) == 0);
[response.returnValue getValue:&returnValue];
XCTAssert(returnValue == nil);
XCTAssertNil(response.exception);
XCTAssertNil(response.outValues);
}
[blockExecuted fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testInvocationWithArgsAndReturnHandler {
XCTestExpectation *blockExecuted = [self expectationWithDescription:@"Executed the test block."];
EDOTestDummy *dummyLocal = [[EDOTestDummy alloc] initWithValue:50];
[self
edo_createQueueAndServiceWithRootObject:dummyLocal
block:^(EDOHostService *service) {
{
EDOInvocationResponse *response =
[self edo_runInvocationWithService:service
target:dummyLocal
selector:@selector
(returnIdWithInt:)
arguments:@[
[self edo_intValue:10]
]];
EDOTestDummy __unsafe_unretained *returnValue;
XCTAssert(strcmp(response.returnValue.objCType,
@encode(id)) == 0);
[response.returnValue getValue:&returnValue];
XCTAssertEqual(
[returnValue class], [EDOObject class],
@"Non-value-type should be wrapped as a EDOObject");
XCTAssertNil(response.exception);
XCTAssertNil(response.outValues);
}
{
EDOInvocationResponse *response = [self
edo_runInvocationWithService:service
target:dummyLocal
selector:@selector
(returnNumberWithInt:value:)
arguments:@[
[self edo_intValue:8],
[self edo_numberValue:@9]
]];
NSNumber __unsafe_unretained *returnValue;
XCTAssert(strcmp(response.returnValue.objCType,
@encode(NSNumber *)) == 0);
[response.returnValue getValue:&returnValue];
// 50(dummyInitValue) + 10(selWithIdReturn:) * 2 + (8 +
// 9)(selWithReturnAndArg:value:)
XCTAssertEqualObjects(returnValue,
[NSNumber numberWithInteger:87]);
XCTAssertNil(response.exception);
XCTAssertNil(response.outValues);
}
[blockExecuted fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testInvocationWitOutParameters {
XCTestExpectation *blockExecuted = [self expectationWithDescription:@"Executed the test block."];
EDOTestDummy *dummyLocal = [[EDOTestDummy alloc] initWithValue:8];
[self edo_createQueueAndServiceWithRootObject:dummyLocal
block:^(EDOHostService *service) {
{
EDOBoxedValueType *nullValue = [EDOBoxedValueType
parameterForDoublePointerNullValue];
EDOInvocationResponse *response = [self
edo_runInvocationWithService:service
target:dummyLocal
selector:@selector
(returnBoolWithError:)
arguments:@[ nullValue ]];
XCTAssertNil(response.exception);
XCTAssertEqual(response.outValues.count, 1U);
BOOL retValue = YES;
[response.returnValue getValue:&retValue];
XCTAssertFalse(retValue);
}
{
// nil as a pointer pointing to nil
EDOBoxedValueType *nilValue =
[EDOBoxedValueType parameterForNilValue];
EDOInvocationResponse *response =
[self edo_runInvocationWithService:service
target:dummyLocal
selector:@selector
(returnBoolWithError:)
arguments:@[ nilValue ]];
BOOL retValue = NO;
XCTAssertNil(response.exception);
[response.returnValue getValue:&retValue];
XCTAssertTrue(retValue);
XCTAssertEqual(response.outValues.count, 1U);
// Only copy the pointer address out of the
// EDOBoxedValueType w/o retain/release.
NSError __unsafe_unretained *errorOut;
[response.outValues[0] getValue:&errorOut];
// NSError is not a value type.
XCTAssertEqualObjects([errorOut class],
[EDOObject class]);
XCTAssertEqual(errorOut.code, 8);
}
[blockExecuted fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testInvocationWithThrowHandler {
XCTestExpectation *blockExecuted = [self expectationWithDescription:@"Executed the test block."];
EDOTestDummy *dummyLocal = [[EDOTestDummy alloc] initWithValue:50];
[self
edo_createQueueAndServiceWithRootObject:dummyLocal
block:^(EDOHostService *service) {
XCTAssertNotNil(
[self edo_runInvocationWithService:service
target:dummyLocal
selector:@selector
(selWithThrow)
arguments:@[]]
.exception);
{
EDOBoxedValueType *nilValue =
[EDOBoxedValueType parameterForNilValue];
EDOInvocationResponse *response = [self
edo_runInvocationWithService:service
target:dummyLocal
selector:@selector(voidWithId:)
arguments:@[ nilValue ]];
XCTAssertEqualObjects(response.exception.reason,
@"NilArg");
}
{
id nonNilArg = @"NonNil";
EDOBoxedValueType *nonNilValue =
[EDOBoxedValueType parameterWithBytes:&nonNilArg
objCType:@encode(id)];
EDOInvocationResponse *response = [self
edo_runInvocationWithService:service
target:dummyLocal
selector:@selector(voidWithId:)
arguments:@[ nonNilValue ]];
XCTAssertEqualObjects(response.exception.reason,
@"NonNilArg");
}
[blockExecuted fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testClassMethodInvocationHandler {
XCTestExpectation *blockExecuted = [self expectationWithDescription:@"Executed the test block."];
EDOTestDummy *dummyLocal = [[EDOTestDummy alloc] init];
[self edo_createQueueAndServiceWithRootObject:dummyLocal
block:^(EDOHostService *service) {
EDOInvocationResponse *response = [self
edo_runInvocationWithService:service
target:[dummyLocal class]
selector:@selector
(classMethodWithNumber:)
arguments:@[
[self edo_numberValue:@(10)]
]];
id __unsafe_unretained returnValue;
XCTAssert(strcmp(response.returnValue.objCType,
@encode(id)) == 0);
[response.returnValue getValue:&returnValue];
XCTAssertEqualObjects([returnValue class],
[EDOObject class]);
XCTAssertNil(response.exception);
XCTAssertNil(response.outValues);
[blockExecuted fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testArgsMismatchedInvocationHandler {
XCTestExpectation *blockExecuted = [self expectationWithDescription:@"Executed the test block."];
EDOTestDummy *dummyLocal = [[EDOTestDummy alloc] initWithValue:50];
[self
edo_createQueueAndServiceWithRootObject:dummyLocal
block:^(EDOHostService *service) {
XCTAssertNotNil(
[self edo_runInvocationWithService:service
target:dummyLocal
selector:@selector
(returnIdWithInt:)
arguments:@[]]
.exception);
XCTAssertNotNil(
[self edo_runInvocationWithService:service
target:dummyLocal
selector:@selector
(nonExistMethod)
arguments:@[]]
.exception);
XCTAssertNotNil(
[self edo_runInvocationWithService:service
target:dummyLocal
selector:@selector
(voidWithValuePlusOne)
arguments:@[
[self edo_intValue:10]
]]
.exception);
XCTAssertNotNil(
[self edo_runInvocationWithService:service
target:dummyLocal
selector:@selector
(voidWithInt:)
arguments:@[
[self edo_numberValue:@10]
]]
.exception);
[blockExecuted fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testMethodSignatureRequestHandler {
NS_VALID_UNTIL_END_OF_SCOPE EDOTestDummy *dummyLocal = [[EDOTestDummy alloc] initWithValue:50];
void *remoteAddress = (__bridge void *)dummyLocal;
EDOHostService *service = [EDOHostService serviceWithPort:0
rootObject:self
queue:dispatch_get_main_queue()];
[EDOTestDummy enumerateSelector:^(SEL selector) {
EDOMethodSignatureRequest *request =
[EDOMethodSignatureRequest requestWithObject:(EDOPointerType)remoteAddress
port:service.port
selector:selector];
EDOMethodSignatureResponse *response =
(EDOMethodSignatureResponse *)EDOMethodSignatureRequest.requestHandler(request, service);
XCTAssertEqualObjects(response.signature, [self selectorSignature:selector],
@"the signature for %@ is not matched.", NSStringFromSelector(selector));
}];
EDOMethodSignatureRequest *request =
[EDOMethodSignatureRequest requestWithObject:(EDOPointerType)remoteAddress
port:service.port
selector:@selector(nonExistMethod)];
EDOMethodSignatureResponse *response =
(EDOMethodSignatureResponse *)EDOMethodSignatureRequest.requestHandler(request, service);
XCTAssertNil(response.signature, @"the non-exist signature should be nil.");
[service invalidate];
}
- (void)testMethodSignatureForward {
id dummyLocal = [[EDOTestDummy alloc] init];
NS_VALID_UNTIL_END_OF_SCOPE dispatch_queue_t queue =
dispatch_queue_create("com.google.edo.service.test", DISPATCH_QUEUE_SERIAL);
EDOHostService *service = [EDOHostService serviceWithPort:0 rootObject:dummyLocal queue:queue];
[EDOTestDummy enumerateSelector:^(SEL sel) {
EDOTestDummy *object = [EDOClientService rootObjectWithPort:service.port.hostPort.port];
NSMethodSignature *sig1 = [object methodSignatureForSelector:sel];
NSMethodSignature *sig2 = [dummyLocal methodSignatureForSelector:sel];
XCTAssertEqualObjects(sig1, sig2, @"The remote signature is not matched %@.",
NSStringFromSelector(sel));
}];
{
EDOTestDummy *object = [EDOClientService rootObjectWithPort:service.port.hostPort.port];
NSMethodSignature *sig1 = [object methodSignatureForSelector:@selector(nonExistMethod)];
NSMethodSignature *sig2 = [dummyLocal methodSignatureForSelector:@selector(nonExistMethod)];
XCTAssertEqual(sig1, sig2);
XCTAssertNil(sig1, @"The non-exist signature should be nil.");
}
{
id dummyClazz = [service distantObjectForLocalObject:[dummyLocal class]
hostPort:service.port.hostPort];
NSMethodSignature *sig1 =
[dummyClazz methodSignatureForSelector:@selector(classMethodWithNumber:)];
NSMethodSignature *sig2 =
[EDOTestDummy methodSignatureForSelector:@selector(classMethodWithNumber:)];
XCTAssertEqualObjects(sig1, sig2, @"The class method signature is not matched.");
}
[service invalidate];
}
#pragma mark - Helper methods
- (dispatch_queue_t)edo_createQueueAndServiceWithRootObject:(id)obj
block:(void (^)(EDOHostService *))block {
NSString *queueName = [NSString stringWithFormat:@"com.google.edo.service.%@", self.name];
dispatch_queue_t queue = dispatch_queue_create(queueName.UTF8String, DISPATCH_QUEUE_SERIAL);
EDOHostService *service = [EDOHostService serviceWithPort:0 rootObject:obj queue:queue];
block(service);
XCTAssertNotNil(queue);
[service invalidate];
return queue;
}
- (EDOInvocationResponse *)edo_runInvocationWithService:(EDOHostService *)service
target:(id)target
selector:(SEL)selector
arguments:(NSArray *)arguments {
EDOPointerType targetPointer = (EDOPointerType)(__bridge void *)target;
EDOInvocationRequest *request = [EDOInvocationRequest requestWithTarget:targetPointer
selector:selector
arguments:arguments
hostPort:service.port.hostPort
returnByValue:NO];
return (EDOInvocationResponse *)EDOInvocationRequest.requestHandler(request, service);
}
- (NSString *)selectorSignature:(SEL)selector {
Method method = class_getInstanceMethod([EDOTestDummy class], selector);
NSMethodSignature *signature =
[NSMethodSignature signatureWithObjCTypes:method_getTypeEncoding(method)];
NSMutableString *encoding = [NSMutableString stringWithUTF8String:signature.methodReturnType];
for (NSUInteger i = 0; i < signature.numberOfArguments; ++i) {
[encoding appendFormat:@"%s", [signature getArgumentTypeAtIndex:i]];
}
return encoding;
}
- (EDOBoxedValueType *)edo_intValue:(int)value {
return [EDOBoxedValueType parameterWithBytes:&value objCType:@encode(int)];
}
- (EDOBoxedValueType *)edo_numberValue:(NSNumber *)value {
return [EDOBoxedValueType parameterWithBytes:&value objCType:@encode(NSNumber *)];
}
@end