| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| #import <Foundation/Foundation.h> |
| #include <getopt.h> |
| #include <string> |
| |
| namespace { |
| |
| void PrintUsage() { |
| fprintf( |
| stderr, |
| "Usage: iossim [-d device] [-s sdk_version] <app_path> <xctest_path>\n" |
| " where <app_path> is the path to the .app directory and <xctest_path> " |
| "is the path to an optional xctest bundle.\n" |
| "Options:\n" |
| " -u Specifies the device udid to use. Will use -d, -s values to get " |
| "devices if not specified.\n" |
| " -d Specifies the device (must be one of the values from the iOS " |
| "Simulator's Hardware -> Device menu. Defaults to 'iPhone 6s'.\n" |
| " -w Wipe the device's contents and settings before running the " |
| "test.\n" |
| " -e Specifies an environment key=value pair that will be" |
| " set in the simulated application's environment.\n" |
| " -t Specifies a test or test suite that should be included in the " |
| "test run. All other tests will be excluded from this run.\n" |
| " -c Specifies command line flags to pass to application.\n" |
| " -p Print the device's home directory, does not run a test.\n" |
| " -s Specifies the SDK version to use (e.g '9.3'). Will use system " |
| "default if not specified.\n"); |
| } |
| |
| // Exit status codes. |
| const int kExitSuccess = EXIT_SUCCESS; |
| const int kExitInvalidArguments = 2; |
| |
| void LogError(NSString* format, ...) { |
| va_list list; |
| va_start(list, format); |
| |
| NSString* message = |
| [[[NSString alloc] initWithFormat:format arguments:list] autorelease]; |
| |
| fprintf(stderr, "iossim: ERROR: %s\n", [message UTF8String]); |
| fflush(stderr); |
| |
| va_end(list); |
| } |
| |
| } |
| |
| // Wrap boiler plate calls to xcrun NSTasks. |
| @interface XCRunTask : NSObject { |
| NSTask* _task; |
| } |
| - (instancetype)initWithArguments:(NSArray*)arguments; |
| - (void)run; |
| - (void)setStandardOutput:(id)output; |
| - (void)setStandardError:(id)error; |
| - (int)getTerminationStatus; |
| @end |
| |
| @implementation XCRunTask |
| |
| - (instancetype)initWithArguments:(NSArray*)arguments { |
| self = [super init]; |
| if (self) { |
| _task = [[NSTask alloc] init]; |
| SEL selector = @selector(setStartsNewProcessGroup:); |
| if ([_task respondsToSelector:selector]) |
| [_task performSelector:selector withObject:nil]; |
| [_task setLaunchPath:@"/usr/bin/xcrun"]; |
| [_task setArguments:arguments]; |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| [_task release]; |
| [super dealloc]; |
| } |
| |
| - (void)setStandardOutput:(id)output { |
| [_task setStandardOutput:output]; |
| } |
| |
| - (void)setStandardError:(id)error { |
| [_task setStandardError:error]; |
| } |
| |
| - (int)getTerminationStatus { |
| return [_task terminationStatus]; |
| } |
| |
| - (void)run { |
| [_task launch]; |
| [_task waitUntilExit]; |
| } |
| |
| - (void)launch { |
| [_task launch]; |
| } |
| |
| - (void)waitUntilExit { |
| [_task waitUntilExit]; |
| } |
| |
| @end |
| |
| // Return array of available iOS runtime dictionaries. Unavailable (old Xcode |
| // versions) or other runtimes (tvOS, watchOS) are removed. |
| NSArray* Runtimes(NSDictionary* simctl_list) { |
| NSMutableArray* runtimes = |
| [[simctl_list[@"runtimes"] mutableCopy] autorelease]; |
| for (NSDictionary* runtime in simctl_list[@"runtimes"]) { |
| if (![runtime[@"identifier"] |
| hasPrefix:@"com.apple.CoreSimulator.SimRuntime.iOS"] || |
| ![runtime[@"availability"] isEqualToString:@"(available)"]) { |
| [runtimes removeObject:runtime]; |
| } |
| } |
| return runtimes; |
| } |
| |
| // Return array of device dictionaries. |
| NSArray* Devices(NSDictionary* simctl_list) { |
| NSMutableArray* devicetypes = |
| [[simctl_list[@"devicetypes"] mutableCopy] autorelease]; |
| for (NSDictionary* devicetype in simctl_list[@"devicetypes"]) { |
| if (![devicetype[@"identifier"] |
| hasPrefix:@"com.apple.CoreSimulator.SimDeviceType.iPad"] && |
| ![devicetype[@"identifier"] |
| hasPrefix:@"com.apple.CoreSimulator.SimDeviceType.iPhone"]) { |
| [devicetypes removeObject:devicetype]; |
| } |
| } |
| return devicetypes; |
| } |
| |
| // Get list of devices, runtimes, etc from sim_ctl. |
| NSDictionary* GetSimulatorList() { |
| XCRunTask* task = [[[XCRunTask alloc] |
| initWithArguments:@[ @"simctl", @"list", @"-j" ]] autorelease]; |
| NSPipe* out = [NSPipe pipe]; |
| [task setStandardOutput:out]; |
| |
| // In the rest of the this file we read from the pipe after -waitUntilExit |
| // (We normally wrap -launch and -waitUntilExit in one -run method). However, |
| // on some swarming slaves this led to a hang on simctl's pipe. Since the |
| // output of simctl is so instant, reading it before exit seems to work, and |
| // seems to avoid the hang. |
| [task launch]; |
| NSData* data = [[out fileHandleForReading] readDataToEndOfFile]; |
| [task waitUntilExit]; |
| |
| NSError* error = nil; |
| return [NSJSONSerialization JSONObjectWithData:data |
| options:kNilOptions |
| error:&error]; |
| } |
| |
| // List supported runtimes and devices. |
| void PrintSupportedDevices(NSDictionary* simctl_list) { |
| printf("\niOS devices:\n"); |
| for (NSDictionary* type in Devices(simctl_list)) { |
| printf("%s\n", [type[@"name"] UTF8String]); |
| } |
| printf("\nruntimes:\n"); |
| for (NSDictionary* runtime in Runtimes(simctl_list)) { |
| printf("%s\n", [runtime[@"version"] UTF8String]); |
| } |
| } |
| |
| // Expand path to absolute path. |
| NSString* ResolvePath(NSString* path) { |
| path = [path stringByExpandingTildeInPath]; |
| path = [path stringByStandardizingPath]; |
| const char* cpath = [path cStringUsingEncoding:NSUTF8StringEncoding]; |
| char* resolved_name = NULL; |
| char* abs_path = realpath(cpath, resolved_name); |
| if (abs_path == NULL) { |
| return nil; |
| } |
| return [NSString stringWithCString:abs_path encoding:NSUTF8StringEncoding]; |
| } |
| |
| // Search |simctl_list| for a udid matching |device_name| and |sdk_version|. |
| NSString* GetDeviceBySDKAndName(NSDictionary* simctl_list, |
| NSString* device_name, |
| NSString* sdk_version) { |
| NSString* sdk = [@"iOS " stringByAppendingString:sdk_version]; |
| NSArray* devices = [simctl_list[@"devices"] objectForKey:sdk]; |
| for (NSDictionary* device in devices) { |
| if ([device[@"name"] isEqualToString:device_name]) { |
| return device[@"udid"]; |
| } |
| } |
| return nil; |
| } |
| |
| bool FindDeviceByUDID(NSDictionary* simctl_list, NSString* udid) { |
| NSDictionary* devices_table = simctl_list[@"devices"]; |
| for (id runtimes in devices_table) { |
| NSArray* devices = devices_table[runtimes]; |
| for (NSDictionary* device in devices) { |
| if ([device[@"udid"] isEqualToString:udid]) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| // Prints the HOME environment variable for a device. Used by the bots to |
| // package up all the test data. |
| void PrintDeviceHome(NSString* udid) { |
| XCRunTask* task = [[[XCRunTask alloc] |
| initWithArguments:@[ @"simctl", @"getenv", udid, @"HOME" ]] autorelease]; |
| [task run]; |
| } |
| |
| // Erase a device, used by the bots before a clean test run. |
| void WipeDevice(NSString* udid) { |
| XCRunTask* shutdown = [[[XCRunTask alloc] |
| initWithArguments:@[ @"simctl", @"shutdown", udid ]] autorelease]; |
| [shutdown setStandardOutput:nil]; |
| [shutdown setStandardError:nil]; |
| [shutdown run]; |
| |
| XCRunTask* erase = [[[XCRunTask alloc] |
| initWithArguments:@[ @"simctl", @"erase", udid ]] autorelease]; |
| [erase run]; |
| } |
| |
| void KillSimulator() { |
| XCRunTask* task = [[[XCRunTask alloc] |
| initWithArguments:@[ @"killall", @"Simulator" ]] autorelease]; |
| [task setStandardOutput:nil]; |
| [task setStandardError:nil]; |
| [task run]; |
| } |
| |
| int RunApplication(NSString* app_path, |
| NSString* xctest_path, |
| NSString* udid, |
| NSMutableDictionary* app_env, |
| NSMutableArray* cmd_args, |
| NSMutableArray* tests_filter) { |
| NSString* tempFilePath = [NSTemporaryDirectory() |
| stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]]; |
| [[NSFileManager defaultManager] createFileAtPath:tempFilePath |
| contents:nil |
| attributes:nil]; |
| |
| NSMutableDictionary* xctestrun = [NSMutableDictionary dictionary]; |
| NSMutableDictionary* testTargetName = [NSMutableDictionary dictionary]; |
| |
| NSMutableDictionary* testingEnvironmentVariables = |
| [NSMutableDictionary dictionary]; |
| [testingEnvironmentVariables setValue:[app_path lastPathComponent] |
| forKey:@"IDEiPhoneInternalTestBundleName"]; |
| |
| [testingEnvironmentVariables |
| setValue: |
| @"__TESTROOT__/Debug-iphonesimulator:__PLATFORMS__/" |
| @"iPhoneSimulator.platform/Developer/Library/Frameworks" |
| forKey:@"DYLD_FRAMEWORK_PATH"]; |
| [testingEnvironmentVariables |
| setValue: |
| @"__TESTROOT__/Debug-iphonesimulator:__PLATFORMS__/" |
| @"iPhoneSimulator.platform/Developer/Library" |
| forKey:@"DYLD_LIBRARY_PATH"]; |
| |
| if (xctest_path) { |
| [testTargetName setValue:xctest_path forKey:@"TestBundlePath"]; |
| NSString* inject = |
| @"__PLATFORMS__/iPhoneSimulator.platform/Developer/Library/" |
| @"PrivateFrameworks/IDEBundleInjection.framework/" |
| @"IDEBundleInjection:__PLATFORMS__/iPhoneSimulator.platform/Developer/" |
| @"usr/lib/libXCTestBundleInject.dylib"; |
| [testingEnvironmentVariables setValue:inject |
| forKey:@"DYLD_INSERT_LIBRARIES"]; |
| [testingEnvironmentVariables |
| setValue:[NSString stringWithFormat:@"__TESTHOST__/%@", |
| [[app_path lastPathComponent] |
| stringByDeletingPathExtension]] |
| forKey:@"XCInjectBundleInto"]; |
| } else { |
| [testTargetName setValue:app_path forKey:@"TestBundlePath"]; |
| } |
| [testTargetName setValue:app_path forKey:@"TestHostPath"]; |
| |
| if ([app_env count]) { |
| [testTargetName setObject:app_env forKey:@"EnvironmentVariables"]; |
| } |
| |
| if ([cmd_args count] > 0) { |
| [testTargetName setObject:cmd_args forKey:@"CommandLineArguments"]; |
| } |
| |
| if ([tests_filter count] > 0) { |
| [testTargetName setObject:tests_filter forKey:@"OnlyTestIdentifiers"]; |
| } |
| |
| [testTargetName setObject:testingEnvironmentVariables |
| forKey:@"TestingEnvironmentVariables"]; |
| [xctestrun setObject:testTargetName forKey:@"TestTargetName"]; |
| |
| NSString* error; |
| NSData* data = [NSPropertyListSerialization |
| dataFromPropertyList:xctestrun |
| format:NSPropertyListXMLFormat_v1_0 |
| errorDescription:&error]; |
| [data writeToFile:tempFilePath atomically:YES]; |
| XCRunTask* task = [[[XCRunTask alloc] initWithArguments:@[ |
| @"xcodebuild", @"-xctestrun", tempFilePath, @"-destination", |
| [@"platform=iOS Simulator,id=" stringByAppendingString:udid], |
| @"test-without-building" |
| ]] autorelease]; |
| |
| if (!xctest_path) { |
| // The following stderr messages are meaningless on iossim when not running |
| // xctests and can be safely stripped. |
| NSArray* ignore_strings = @[ |
| @"IDETestOperationsObserverErrorDomain", @"** TEST EXECUTE FAILED **" |
| ]; |
| NSPipe* stderr_pipe = [NSPipe pipe]; |
| stderr_pipe.fileHandleForReading.readabilityHandler = |
| ^(NSFileHandle* handle) { |
| NSString* log = [[[NSString alloc] initWithData:handle.availableData |
| encoding:NSUTF8StringEncoding] |
| autorelease]; |
| for (NSString* ignore_string in ignore_strings) { |
| if ([log rangeOfString:ignore_string].location != NSNotFound) { |
| return; |
| } |
| } |
| printf("%s", [log UTF8String]); |
| }; |
| [task setStandardError:stderr_pipe]; |
| } |
| [task run]; |
| return [task getTerminationStatus]; |
| } |
| |
| int main(int argc, char* const argv[]) { |
| // When the last running simulator is from Xcode 7, an Xcode 8 run will yeild |
| // a failure to "unload a stale CoreSimulatorService job" message. Sending a |
| // hidden simctl to do something simple (list devices) helpfully works around |
| // this issue. |
| XCRunTask* workaround_task = [[[XCRunTask alloc] |
| initWithArguments:@[ @"simctl", @"list", @"-j" ]] autorelease]; |
| [workaround_task setStandardOutput:nil]; |
| [workaround_task setStandardError:nil]; |
| [workaround_task run]; |
| |
| NSString* app_path = nil; |
| NSString* xctest_path = nil; |
| NSString* udid = nil; |
| NSString* device_name = @"iPhone 6s"; |
| bool wants_wipe = false; |
| bool wants_print_home = false; |
| NSDictionary* simctl_list = GetSimulatorList(); |
| float sdk = 0; |
| for (NSDictionary* runtime in Runtimes(simctl_list)) { |
| sdk = fmax(sdk, [runtime[@"version"] floatValue]); |
| } |
| NSString* sdk_version = [NSString stringWithFormat:@"%0.1f", sdk]; |
| NSMutableDictionary* app_env = [NSMutableDictionary dictionary]; |
| NSMutableArray* cmd_args = [NSMutableArray array]; |
| NSMutableArray* tests_filter = [NSMutableArray array]; |
| |
| int c; |
| while ((c = getopt(argc, argv, "hs:d:u:t:e:c:pwl")) != -1) { |
| switch (c) { |
| case 's': |
| sdk_version = [NSString stringWithUTF8String:optarg]; |
| break; |
| case 'd': |
| device_name = [NSString stringWithUTF8String:optarg]; |
| break; |
| case 'u': |
| udid = [NSString stringWithUTF8String:optarg]; |
| break; |
| case 'w': |
| wants_wipe = true; |
| break; |
| case 'c': { |
| NSString* cmd_arg = [NSString stringWithUTF8String:optarg]; |
| [cmd_args addObject:cmd_arg]; |
| } break; |
| case 't': { |
| NSString* test = [NSString stringWithUTF8String:optarg]; |
| [tests_filter addObject:test]; |
| } break; |
| case 'e': { |
| NSString* envLine = [NSString stringWithUTF8String:optarg]; |
| NSRange range = [envLine rangeOfString:@"="]; |
| if (range.location == NSNotFound) { |
| LogError(@"Invalid key=value argument for -e."); |
| PrintUsage(); |
| exit(kExitInvalidArguments); |
| } |
| NSString* key = [envLine substringToIndex:range.location]; |
| NSString* value = [envLine substringFromIndex:(range.location + 1)]; |
| [app_env setObject:value forKey:key]; |
| } break; |
| case 'p': |
| wants_print_home = true; |
| break; |
| case 'l': |
| PrintSupportedDevices(simctl_list); |
| exit(kExitSuccess); |
| break; |
| case 'h': |
| PrintUsage(); |
| exit(kExitSuccess); |
| break; |
| default: |
| PrintUsage(); |
| exit(kExitInvalidArguments); |
| } |
| } |
| |
| if (udid == nil) { |
| udid = GetDeviceBySDKAndName(simctl_list, device_name, sdk_version); |
| if (udid == nil) { |
| LogError(@"Unable to find a device %@ with SDK %@.", device_name, |
| sdk_version); |
| PrintSupportedDevices(simctl_list); |
| exit(kExitInvalidArguments); |
| } |
| } else { |
| if (!FindDeviceByUDID(simctl_list, udid)) { |
| LogError( |
| @"Unable to find a device with udid %@. Use 'xcrun simctl list' to " |
| @"see valid device udids.", |
| udid); |
| exit(kExitInvalidArguments); |
| } |
| } |
| |
| if (wants_print_home) { |
| PrintDeviceHome(udid); |
| exit(kExitSuccess); |
| } |
| |
| KillSimulator(); |
| if (wants_wipe) { |
| WipeDevice(udid); |
| printf("Device wiped.\n"); |
| exit(kExitSuccess); |
| } |
| |
| // There should be at least one arg left, specifying the app path. Any |
| // additional args are passed as arguments to the app. |
| if (optind < argc) { |
| NSString* unresolved_path = [[NSFileManager defaultManager] |
| stringWithFileSystemRepresentation:argv[optind] |
| length:strlen(argv[optind])]; |
| app_path = ResolvePath(unresolved_path); |
| if (!app_path) { |
| LogError(@"Unable to resolve app_path %@", unresolved_path); |
| exit(kExitInvalidArguments); |
| } |
| |
| if (++optind < argc) { |
| NSString* unresolved_path = [[NSFileManager defaultManager] |
| stringWithFileSystemRepresentation:argv[optind] |
| length:strlen(argv[optind])]; |
| xctest_path = ResolvePath(unresolved_path); |
| if (!xctest_path) { |
| LogError(@"Unable to resolve xctest_path %@", unresolved_path); |
| exit(kExitInvalidArguments); |
| } |
| } |
| } else { |
| LogError(@"Unable to parse command line arguments."); |
| PrintUsage(); |
| exit(kExitInvalidArguments); |
| } |
| |
| int return_code = RunApplication(app_path, xctest_path, udid, app_env, |
| cmd_args, tests_filter); |
| KillSimulator(); |
| return return_code; |
| } |