blob: 838dbf9964883234c5bbde0fd9e5199fb579e55c [file] [log] [blame]
//
// Copyright 2019 Google LLC.
//
// 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 "Service/Tests/FunctionalTests/EDOServiceUIBaseTest.h"
#include <objc/runtime.h>
#import "Channel/Sources/EDOHostPort.h"
#import "Channel/Sources/EDOSocket.h"
#import "Channel/Sources/EDOSocketChannel.h"
#import "Service/Sources/EDOClientService.h"
#import "Service/Sources/EDOHostNamingService.h"
#import "Service/Sources/EDOHostService.h"
#import "Service/Sources/EDORemoteException.h"
#import "Service/Sources/EDOServiceError.h"
#import "Service/Sources/NSObject+EDOBlacklistedType.h"
#import "Service/Sources/NSObject+EDOValueObject.h"
#import "Service/Tests/FunctionalTests/EDOTestDummyInTest.h"
#import "Service/Tests/TestsBundle/EDOTestClassDummy.h"
#import "Service/Tests/TestsBundle/EDOTestDummy.h"
#import "Service/Tests/TestsBundle/EDOTestProtocol.h"
#import "Service/Tests/TestsBundle/EDOTestProtocolInApp.h"
#import "Service/Tests/TestsBundle/EDOTestProtocolInTest.h"
static NSString *const kTestServiceName = @"com.google.edo.testService";
@interface EDOUITestAppUITests : EDOServiceUIBaseTest
@end
@implementation EDOUITestAppUITests
- (void)tearDown {
EDOSetClientErrorHandler(nil);
[super tearDown];
}
- (void)testServiceNonExist {
[self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:5];
EDOTestDummy *remoteDummy = [EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT];
// The temporary service is created and self is wrapped as a remote object being sent
// to the application process without the service created locally here.
XCTAssertEqualObjects([remoteDummy returnClassNameWithObject:self], @"EDOObject");
}
- (void)testValueAndIdOutParameter {
[self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:7];
EDOHostService *service =
[EDOHostService serviceWithPort:2234
rootObject:[[EDOTestDummyInTest alloc] initWithValue:9]
queue:dispatch_get_main_queue()];
EDOTestDummy *remoteDummy = [EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT];
XCTAssertEqualObjects([remoteDummy class], NSClassFromString(@"EDOObject"));
NSError *errorOut;
XCTAssertFalse([remoteDummy returnBoolWithError:nil]);
XCTAssertTrue([remoteDummy returnBoolWithError:&errorOut]);
XCTAssertEqual(errorOut.code, 7);
EDOTestDummy *dummyOut;
XCTAssertThrows([remoteDummy voidWithOutObject:nil]);
[remoteDummy voidWithOutObject:&dummyOut];
XCTAssertEqualObjects([dummyOut class], NSClassFromString(@"EDOObject"));
XCTAssertEqual(dummyOut.value, 12);
EDOTestDummyInTest *dummyInTestOut;
// 11 + 7 + 12
XCTAssertEqual(
[dummyOut selWithOutEDO:&dummyInTestOut dummy:[[EDOTestDummyInTest alloc] initWithValue:11]],
30);
// dummyInTestOut is created in this process and need to be unwrapped to the local address
// as an out parameter.
XCTAssertEqualObjects([dummyInTestOut class], [EDOTestDummyInTest class]);
XCTAssertEqual(dummyInTestOut.value.intValue, 30);
EDOTestDummyInTest *dummyInTestOutRet = [dummyOut selWithInOutEDO:&dummyInTestOut];
XCTAssertEqual(dummyInTestOut.value.intValue, 30);
XCTAssertEqual(dummyInTestOutRet.value.intValue, 49); // 12 + 7 + 30
EDOTestDummyInTest *dummyInNil;
XCTAssertNil([dummyOut selWithInOutEDO:&dummyInNil]);
XCTAssertThrows([dummyOut selWithInOutEDO:nil]);
[service invalidate];
}
- (void)testProtocolParameter {
[self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:10];
EDOTestDummy *remoteDummy = [EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT];
// The protocol is loaded in the both sides so it shouldn't throw an exception.
XCTAssertNoThrow([remoteDummy voidWithProtocol:@protocol(EDOTestProtocol)]);
// This protocol isn't loaded on the app-side.
XCTAssertThrowsSpecificNamed([remoteDummy voidWithProtocol:@protocol(EDOTestProtocolInTest)],
NSException, NSInternalInconsistencyException);
// Calling a method from the protocol
XCTAssertTrue([[remoteDummy protocolName] isEqualToString:@"EDOTestProtocolInApp"]);
// Getting a protocol that wasn't loaded on the test side
XCTAssertThrowsSpecificNamed([remoteDummy returnWithProtocolInApp], NSException,
NSInternalInconsistencyException);
}
- (void)testBlacklistedParameter {
[self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:0];
EDOBlacklistedTestDummyInTest *testDummy =
[[EDOBlacklistedTestDummyInTest alloc] initWithValue:0];
EDOHostService *service = [EDOHostService serviceWithPort:2234
rootObject:testDummy
queue:dispatch_get_main_queue()];
EDOTestDummy *remoteDummy = [EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT];
XCTAssertNoThrow([remoteDummy callBackToTest:testDummy withValue:0]);
XCTAssertNoThrow([remoteDummy createEDOWithPort:2234]);
XCTAssertNoThrow([remoteDummy selWithInOutEDO:&testDummy]);
[EDOBlacklistedTestDummyInTest edo_disallowRemoteInvocation];
XCTAssertThrows([remoteDummy callBackToTest:testDummy withValue:0]);
XCTAssertThrows([remoteDummy createEDOWithPort:2234]);
XCTAssertThrows([remoteDummy selWithInOutEDO:&testDummy]);
EDOTestDummyInTest *plainTestDummy = [[EDOTestDummyInTest alloc] initWithValue:0];
XCTAssertNoThrow([remoteDummy callBackToTest:plainTestDummy withValue:0]);
[service invalidate];
}
- (void)testTwoWayAndMultiplexInvocation {
[self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:5];
EDOHostService *service =
[EDOHostService serviceWithPort:2234
rootObject:[[EDOTestDummyInTest alloc] initWithValue:9]
queue:dispatch_get_main_queue()];
EDOTestDummy *remoteDummy = [EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT];
XCTAssertEqualObjects([remoteDummy class], NSClassFromString(@"EDOObject"));
EDOTestDummyInTest *dummy = [[EDOTestDummyInTest alloc] initWithValue:8];
// Test: [A callBackToTest:B withValue:7]
// App: A-> [B callTestDummy:A]
// Test: B-> [A selWithIdReturn:10]
// App: -> A.value = A.value(5) + value(10) * 2 = 25
// App: -> new A(A.value(5) + 10).value
// App: = 15
// Test: B-> (15) + B.value(8) + 3
// Test: = 26
// App: (26) + value(7) + A.value(25)
// Test: = 58
XCTAssertEqual([remoteDummy callBackToTest:dummy withValue:7], 58);
[service invalidate];
}
- (void)testMultiplexInvocationStressfully {
[self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:5];
EDOTestDummy *remoteDummy = [EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT];
XCTAssertEqualObjects([remoteDummy class], NSClassFromString(@"EDOObject"));
const int kStressTestAttempts = 5;
const int kConcurrentNumber = 15;
EDOTestDummyInTest *dummy = [[EDOTestDummyInTest alloc] initWithValue:8];
for (int i = 0; i < kStressTestAttempts; ++i) {
XCTestExpectation *nestedCallExpectation =
[self expectationWithDescription:@"nested invocation completed."];
// The inner dispatch_apply performs actual stressful execution, and the test considers deadlock
// will happen if dispatch_apply cannot complete within the timeout.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_apply(kConcurrentNumber, DISPATCH_APPLY_AUTO, ^(size_t iteration) {
[remoteDummy callBackToTest:dummy withValue:7];
});
[nestedCallExpectation fulfill];
});
[self waitForExpectations:@[ nestedCallExpectation ] timeout:5.f];
}
}
- (void)testDispatchAsyncEarlyReturn {
[self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:5];
EDOHostService *service =
[EDOHostService serviceWithPort:2234
rootObject:[[EDOTestDummyInTest alloc] initWithValue:9]
queue:dispatch_get_main_queue()];
EDOTestDummy *remoteDummy = [EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT];
EDOTestDummyInTest *dummy = [[EDOTestDummyInTest alloc] initWithValue:7];
dispatch_semaphore_t waitLock = dispatch_semaphore_create(0L);
XCTestExpectation *expectsInvoke = [self expectationWithDescription:@"Invoked the block."];
dummy.block = ^{
dispatch_semaphore_wait(waitLock, DISPATCH_TIME_FOREVER);
[expectsInvoke fulfill];
};
// Test: [A returnPlus10AndAsyncExecuteBlock:B]
// App: dispatch_async: [B invokeBlock]
// Test: acquire waitLock
// -> 5 + 10
// Test: = 15
// release waitLock
// App: dispatch_async: finish
XCTAssertEqual([remoteDummy returnPlus10AndAsyncExecuteBlock:dummy], 5 + 10);
dispatch_semaphore_signal(waitLock);
[self waitForExpectationsWithTimeout:1 handler:nil];
[service invalidate];
}
- (void)testDispatchAsyncManyTimes {
[self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:8];
NS_VALID_UNTIL_END_OF_SCOPE dispatch_queue_t backgroundQueue =
dispatch_queue_create("com.google.edo.uitest", DISPATCH_QUEUE_SERIAL);
EDOTestDummyInTest *rootDummy = [[EDOTestDummyInTest alloc] initWithValue:9];
EDOHostService *service = [EDOHostService serviceWithPort:2234
rootObject:rootDummy
queue:backgroundQueue];
const int totalOfInvokes = 10;
XCTestExpectation *expectsTen = [self expectationWithDescription:@"Invoked many times."];
expectsTen.expectedFulfillmentCount = totalOfInvokes;
rootDummy.block = ^{
// This block is dispatched to a background queue.
XCTAssertFalse([NSThread isMainThread]);
[expectsTen fulfill];
};
EDOTestDummy *remoteDummy = [EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT];
// Dispatch to the background queue because we need to wrap dummy into the remote object, the
// current main queue doesn't have a host service to wrap it.
dispatch_async(backgroundQueue, ^{
for (int i = 0; i < totalOfInvokes; ++i) {
XCTAssertEqual([remoteDummy returnPlus10AndAsyncExecuteBlock:rootDummy], 8 + 10);
}
});
[self waitForExpectationsWithTimeout:5 handler:nil];
[service invalidate];
}
- (void)testServiceInvalidErrorHandling {
// Terminate the app if other tests launched it.
[[[XCUIApplication alloc] init] terminate];
NSString *exceptionName = @"UITest Exception";
__block NSError *currentError;
EDOSetClientErrorHandler(^(NSError *error) {
currentError = error;
[NSException raise:exceptionName format:@"%ld", (long)error.code]; // NOLINT
});
XCTAssertThrowsSpecificNamed([EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT],
NSException, exceptionName);
XCTAssertEqualObjects(currentError.domain, EDOServiceErrorDomain);
XCTAssertEqual(currentError.code, EDOServiceErrorCannotConnect);
}
- (void)testRemoteObjectShouldFailAfterServiceTerminated {
NSString *exceptionName = @"UITest Exception";
__block NSError *currentError;
EDOSetClientErrorHandler(^(NSError *error) {
currentError = error;
[NSException raise:exceptionName format:@"%ld", (long)error.code]; // NOLINT
});
[self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:8];
EDOTestDummy *remoteDummy = [EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT];
XCTAssertEqual(remoteDummy.value, 8);
[remoteDummy invalidateService];
// The remote object should fail after the remote service is down.
XCTAssertThrowsSpecificNamed([remoteDummy voidWithValuePlusOne], NSException, exceptionName);
XCTAssertEqualObjects(currentError.domain, EDOServiceErrorDomain);
XCTAssertEqual(currentError.code, EDOServiceErrorCannotConnect);
currentError = nil;
// The new service is created on the same port and the cached remote object from the previous
// service should fail.
// TODO(haowoo): Refactor this after moving to the new exception/error handler logic.
[self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:8];
XCTAssertThrowsSpecificNamed([remoteDummy voidWithValuePlusOne], NSException,
NSDestinationInvalidException);
}
- (void)testAllocAndClassMethod {
[self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:5];
// Create a local service so it can wrap and deref the any returned EDOObjects.
EDOHostService *service = [EDOHostService serviceWithPort:2234
rootObject:[[EDOTestDummyInTest alloc] init]
queue:dispatch_get_main_queue()];
// @see EDOTestClassDummyStub.m
XCTAssertEqual([EDOTestClassDummy classMethodWithInt:7], 16);
XCTAssertEqual([EDOTestClassDummy classMethodWithIdReturn:8].value, 8);
EDOTestClassDummy *dummy = [EDOTestClassDummy classMethodWithIdReturn:8];
XCTAssertEqualObjects([dummy class], NSClassFromString(@"EDOObject"));
[service invalidate];
// Validate the class methods don't exist locally.
unsigned int methodCount = 0;
Class dummyMeta = object_getClass([EDOTestClassDummy class]);
Method *methods = class_copyMethodList(dummyMeta, &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
Method method = methods[i];
char const *selectorName = sel_getName(method_getName(method));
// Make sure those class methods are not defined locally.
XCTAssertTrue(strcmp(selectorName, sel_getName(@selector(classMethodWithInt:))) != 0);
XCTAssertTrue(strcmp(selectorName, sel_getName(@selector(classMethodWithIdReturn:))) != 0);
}
free(methods);
}
/**
* Verifies `isKindOfClass:` returns true only if the class object belongs to the callee process.
*/
- (void)testIsKindOfClassOnlyResolvesInSameProcess {
[self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:5];
// Create a local service so it can wrap and deref the any returned EDOObjects.
EDOHostService *service = [EDOHostService serviceWithPort:2234
rootObject:[[EDOTestDummyInTest alloc] init]
queue:dispatch_get_main_queue()];
EDOTestDummy *remoteDummy = [EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT];
NSArray<NSNumber *> *remoteArray = [remoteDummy returnArray];
EDOHostPort *hostPort = [EDOHostPort hostPortWithLocalPort:EDOTEST_APP_SERVICE_PORT];
Class remoteArrayClass = [EDOClientService classObjectWithName:@"NSArray" hostPort:hostPort];
XCTAssertTrue([remoteArray isKindOfClass:remoteArrayClass]);
XCTAssertFalse([remoteArray isKindOfClass:[NSArray class]]);
[service invalidate];
}
- (void)testRemoteObjectCopy {
[self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:5];
EDOTestDummy *dummy = [EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT];
NSArray *remoteArray = [dummy returnArray];
NSArray *remoteArrayCopy;
XCTAssertNoThrow(remoteArrayCopy = [remoteArray copy]);
XCTAssertEqual(remoteArray, remoteArrayCopy);
}
- (void)testRemoteObjectMutableCopy {
[self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:5];
EDOTestDummy *dummy = [EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT];
NSArray *remoteArray = [dummy returnArray];
NSMutableArray *remoteArrayCopy = [remoteArray mutableCopy];
XCTAssertNotEqual(remoteArray, remoteArrayCopy);
XCTAssertEqualObjects(remoteArray, remoteArrayCopy);
[remoteArrayCopy addObject:@"test"];
XCTAssertEqualObjects([remoteArrayCopy lastObject], @"test");
}
- (void)testInsertRemoteObjectToDictionary {
[self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:5];
EDOTestDummy *dummy = [EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT];
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
XCTAssertNoThrow([dict setObject:dummy forKey:@"key"]);
}
- (void)testBrokenChannelAfterServiceClosed {
XCUIApplication *app = [self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:5];
EDOTestDummy *remoteDummy = [EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT];
XCTAssertEqualObjects([remoteDummy class], NSClassFromString(@"EDOObject"));
[app terminate];
app = [self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:5];
EDOTestDummy *newRemoteDummy = [EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT];
XCTAssertNotEqualObjects(remoteDummy, newRemoteDummy);
__block Class clazz;
XCTestExpectation *expectsReturn = [self expectationWithDescription:@"Returned in time."];
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
clazz = [newRemoteDummy class];
[expectsReturn fulfill];
});
[self waitForExpectationsWithTimeout:15 handler:nil];
XCTAssertEqualObjects(clazz, NSClassFromString(@"EDOObject"));
XCTAssertThrowsSpecificNamed([remoteDummy returnInt], NSException, NSDestinationInvalidException);
}
/**
* Tests requesting service ports info of the application process, and verifies the port info with
* service name.
*/
- (void)testFetchServicePortsInfo {
[self launchApplicationWithServiceName:kTestServiceName initValue:5];
EDOHostNamingService *service;
XCTAssertNoThrow(service =
[EDOClientService rootObjectWithPort:EDOHostNamingService.namingServerPort]);
XCTAssertFalse([service portForServiceWithName:kTestServiceName] == 0);
}
/** Tests running multiple naming services in the same host, and verifies that exception happens. */
- (void)testStartMultipleNamingServiceObject {
[self launchApplicationWithServiceName:kTestServiceName initValue:5];
EDOHostNamingService *localService = EDOHostNamingService.sharedService;
XCTAssertFalse([localService start]);
}
/** Tests passing local object to remote dummy using pass by value. */
- (void)testPassByValueWithLocalObject {
[self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:5];
EDOTestDummy *dummy = [EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT];
NSArray<NSString *> *localArray = @[ @"foo" ];
dummy.valueObject = [localArray passByValue];
NSArray<NSString *> *remoteArray = (NSArray *)dummy.valueObject;
XCTAssertEqualObjects(localArray[0], remoteArray[0]);
NSString *remoteClassName = [dummy returnClassNameWithObject:remoteArray];
// Ensure that in the remote side, the object is passed by value instead of EDOObject.
XCTAssertNotEqualObjects(remoteClassName, @"EDOObject");
NSString *localClassName = NSStringFromClass(object_getClass(remoteArray));
// Ensure that in the local side, the object is fetched by reference as normal.
XCTAssertEqualObjects(localClassName, @"EDOObject");
}
/** Tests local object is released on the execution queue if it is released by EDOHostService. */
- (void)testLocalObjectReleaseOnHostExecutionQueue {
[self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:5];
EDOHostService *service =
[EDOHostService serviceWithPort:2234
rootObject:[[EDOTestDummyInTest alloc] initWithValue:9]
queue:dispatch_get_main_queue()];
__block EDOTestDummyInTest *dummy = [[EDOTestDummyInTest alloc] init];
__weak EDOTestDummyInTest *weakDummy = dummy;
EDOTestDummy *remoteDummy = [EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT];
XCTestExpectation *expectation = [self expectationWithDescription:@"dummy is released on main."];
dummy.deallocHandlerBlock = ^{
XCTAssertTrue([NSThread isMainThread]);
[expectation fulfill];
};
dummy.block = ^{
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dummy = nil;
});
};
[remoteDummy returnPlus10AndAsyncExecuteBlock:dummy];
[self waitForExpectations:@[ expectation ] timeout:2.0f];
XCTAssertNil(weakDummy);
[service invalidate];
}
- (void)testRemoteExceptionRevealsInformation {
[self launchApplicationWithPort:EDOTEST_APP_SERVICE_PORT initValue:5];
EDOHostService *service =
[EDOHostService serviceWithPort:2234
rootObject:[[EDOTestDummyInTest alloc] initWithValue:9]
queue:dispatch_get_main_queue()];
EDOTestDummy *dummy = [EDOClientService rootObjectWithPort:EDOTEST_APP_SERVICE_PORT];
EDORemoteException *remoteException;
@try {
[dummy selWithThrow];
} @catch (EDORemoteException *exception) {
remoteException = exception;
}
XCTAssertNotNil(remoteException);
XCTAssertEqualObjects(remoteException.name, @"Dummy Just Throw 5");
XCTAssertEqualObjects(remoteException.reason, @"Just Throw");
NSString *callStackSymbols = [remoteException.callStackSymbols componentsJoinedByString:@"|"];
XCTAssertTrue([callStackSymbols containsString:@"testRemoteExceptionRevealsInformation"]);
XCTAssertTrue([callStackSymbols containsString:@"[EDOTestDummy selWithThrow]"]);
XCTAssertTrue([callStackSymbols containsString:@"TestsHost"]);
[service invalidate];
}
@end