// 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 #include #include namespace { void PrintUsage() { fprintf( stderr, "Usage: iossim [-d device] [-s sdk_version] \n" " where is the path to the .app directory and " "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"]; NSString* frameworkPath = @"__PLATFORMS__/iPhoneSimulator.platform/Developer/Library/Frameworks"; [testingEnvironmentVariables setValue:frameworkPath forKey:@"DYLD_FRAMEWORK_PATH"]; NSString* libraryPath = @"__PLATFORMS__/iPhoneSimulator.platform/Developer/Library"; [testingEnvironmentVariables setValue:libraryPath forKey:@"DYLD_LIBRARY_PATH"]; if (xctest_path) { [testTargetName setValue:xctest_path forKey:@"TestBundlePath"]; NSString* inject = @"__PLATFORMS__/iPhoneSimulator.platform/Developer/Library/" "PrivateFrameworks/IDEBundleInjection.framework/IDEBundleInjection"; [testingEnvironmentVariables setValue:inject forKey:@"DYLD_INSERT_LIBRARIES"]; } 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; }