// Copyright 2012 The Chromium Authors // 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. This is " "incompatible with -i.\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" " -v Be more verbose, showing all the xcrun commands we call\n" " -k When to kill the iOS Simulator : before, after, both, never " "(default: both)\n" " -i Use iossim instead of xcodebuild (disables all xctest " "features). This is incompatible with -t.\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]; NSLog(@"ERROR: %@", message); va_end(list); } } typedef enum { KILL_NEVER = 0, KILL_BEFORE = 1 << 0, KILL_AFTER = 1 << 1, KILL_BOTH = KILL_BEFORE | KILL_AFTER, } SimulatorKill; // See https://stackoverflow.com/a/51895129 and // https://github.com/facebook/xctool/pull/159/files. @interface NSTask (PrivateAPI) - (void)setStartsNewProcessGroup:(BOOL)startsNewProcessGroup; @end // Wrap boiler plate calls to xcrun NSTasks. @interface XCRunTask : NSObject - (instancetype)initWithArguments:(NSArray*)arguments; - (void)run:(bool)verbose; - (void)launch:(bool)verbose; - (void)setStandardOutput:(id)output; - (void)setStandardError:(id)error; - (int)terminationStatus; @end @implementation XCRunTask { NSTask* __strong _task; } - (instancetype)initWithArguments:(NSArray*)arguments { self = [super init]; if (self) { _task = [[NSTask alloc] init]; [_task setStartsNewProcessGroup:NO]; _task.launchPath = @"/usr/bin/xcrun"; _task.arguments = arguments; } return self; } - (void)setStandardOutput:(id)output { _task.standardOutput = output; } - (void)setStandardError:(id)error { _task.standardError = error; } - (int)terminationStatus { return _task.terminationStatus; } - (void)run:(bool)verbose { if (verbose) { NSLog(@"Running xcrun %@", [_task.arguments componentsJoinedByString:@" "]); } [_task launch]; [_task waitUntilExit]; } - (void)launch:(bool)verbose { if (verbose) { NSLog(@"Running xcrun %@", [_task.arguments componentsJoinedByString:@" "]); } [_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]; for (NSDictionary* runtime in simctl_list[@"runtimes"]) { BOOL available = [runtime[@"availability"] isEqualToString:@"(available)"] || runtime[@"isAvailable"]; if (![runtime[@"identifier"] hasPrefix:@"com.apple.CoreSimulator.SimRuntime.iOS"] || !available) { [runtimes removeObject:runtime]; } } return runtimes; } // Return array of device dictionaries. NSArray* Devices(NSDictionary* simctl_list) { NSMutableArray* devicetypes = [simctl_list[@"devicetypes"] mutableCopy]; 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(bool verbose) { XCRunTask* task = [[XCRunTask alloc] initWithArguments:@[ @"simctl", @"list", @"-j" ]]; NSPipe* out = [NSPipe pipe]; task.standardOutput = 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:verbose]; 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.UTF8String; char* resolved_name = nullptr; char* abs_path = realpath(cpath, resolved_name); if (abs_path == nullptr) { return nil; } return @(abs_path); } // 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 = nil; NSString* name = nil; // Get runtime identifier based on version property to handle // cases when version and identifier are not the same, // e.g. below identifer is *13-2 but version is 13.2.2 // { // "version" : "13.2.2", // "bundlePath" : "path" // "identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-13-2", // "buildversion" : "17K90" // } for (NSDictionary* runtime in Runtimes(simctl_list)) { if ([runtime[@"version"] isEqualToString:sdk_version]) { sdk = runtime[@"identifier"]; name = runtime[@"name"]; break; } } if (sdk == nil) { printf("\nDid not find Runtime with specified version.\n"); PrintSupportedDevices(simctl_list); exit(kExitInvalidArguments); } NSArray* devices = [simctl_list[@"devices"] objectForKey:sdk]; if (devices == nil || devices.count == 0) { // Specific for XCode 10.1 and lower, // where name from 'runtimes' uses as a key in 'devices'. devices = [simctl_list[@"devices"] objectForKey:name]; } for (NSDictionary* device in devices) { if ([device[@"name"] isEqualToString:device_name]) { return device[@"udid"]; } } return nil; } // Create and return a device udid of |device| and |sdk_version|. NSString* CreateDeviceBySDKAndName(NSString* device, NSString* sdk_version, bool verbose) { NSString* sdk = [@"iOS" stringByAppendingString:sdk_version]; XCRunTask* create = [[XCRunTask alloc] initWithArguments:@[ @"simctl", @"create", device, device, sdk ]]; [create run:verbose]; NSDictionary* simctl_list = GetSimulatorList(verbose); return GetDeviceBySDKAndName(simctl_list, device, sdk_version); } 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, bool verbose) { XCRunTask* task = [[XCRunTask alloc] initWithArguments:@[ @"simctl", @"getenv", udid, @"HOME" ]]; [task run:verbose]; } // Erase a device, used by the bots before a clean test run. void WipeDevice(NSString* udid, bool verbose) { XCRunTask* shutdown = [[XCRunTask alloc] initWithArguments:@[ @"simctl", @"shutdown", udid ]]; shutdown.standardOutput = nil; shutdown.standardError = nil; [shutdown run:verbose]; XCRunTask* erase = [[XCRunTask alloc] initWithArguments:@[ @"simctl", @"erase", udid ]]; [erase run:verbose]; } void KillSimulator(bool verbose) { XCRunTask* task = [[XCRunTask alloc] initWithArguments:@[ @"killall", @"Simulator" ]]; task.standardOutput = nil; task.standardError = nil; [task run:verbose]; } NSString* GetBundleIdentifierFromPath(NSString* app_path) { NSFileManager* file_manager = [NSFileManager defaultManager]; NSString* info_plist_path = [app_path stringByAppendingPathComponent:@"Info.plist"]; if (![file_manager fileExistsAtPath:info_plist_path]) { return nil; } NSDictionary* info_dictionary = [NSDictionary dictionaryWithContentsOfFile:info_plist_path]; NSString* bundle_identifier = info_dictionary[@"CFBundleIdentifier"]; return bundle_identifier; } int RunSimCtl(NSArray* arguments, bool verbose) { XCRunTask* task = [[XCRunTask alloc] initWithArguments:[@[ @"simctl" ] arrayByAddingObjectsFromArray:arguments]]; [task run:verbose]; int ret = [task terminationStatus]; if (ret) { NSLog(@"Warning: the following command failed: xcrun simctl %@", [arguments componentsJoinedByString:@" "]); } return ret; } void PrepareWebTests(NSString* udid, NSString* app_path, bool verbose) { NSString* bundle_identifier = GetBundleIdentifierFromPath(app_path); RunSimCtl(@[ @"uninstall", udid, bundle_identifier ], verbose); RunSimCtl(@[ @"install", udid, app_path ], verbose); } int RunWebTest(NSString* app_path, NSString* udid, NSMutableArray* cmd_args, bool verbose) { NSMutableArray* arguments = [NSMutableArray array]; [arguments addObject:@"simctl"]; [arguments addObject:@"launch"]; [arguments addObject:@"--console"]; [arguments addObject:@"--terminate-running-process"]; [arguments addObject:udid]; [arguments addObject:GetBundleIdentifierFromPath(app_path)]; if (cmd_args.count == 1) { for (NSString* arg in [cmd_args[0] componentsSeparatedByString:@" "]) { [arguments addObject:arg]; } } [arguments addObject:@"-"]; XCRunTask* task = [[XCRunTask alloc] initWithArguments:arguments]; // The following stderr message causes a lot of test faiures on the web // tests. Strip the message here. NSArray* ignore_strings = @[ @"Class SwapLayerEAGL" ]; NSPipe* stderr_pipe = [NSPipe pipe]; stderr_pipe.fileHandleForReading.readabilityHandler = ^(NSFileHandle* handle) { NSString* log = [[NSString alloc] initWithData:handle.availableData encoding:NSUTF8StringEncoding]; for (NSString* ignore_string in ignore_strings) { if ([log rangeOfString:ignore_string].location != NSNotFound) { return; } } fprintf(stderr, "%s", log.UTF8String); }; task.standardError = stderr_pipe; [task run:verbose]; return [task terminationStatus]; } bool isSimDeviceBooted(NSDictionary* simctl_list, NSString* udid) { for (NSString* sdk in simctl_list[@"devices"]) { for (NSDictionary* device in simctl_list[@"devices"][sdk]) { if ([device[@"udid"] isEqualToString:udid]) { if ([device[@"state"] isEqualToString:@"Booted"]) { return true; } } } } return false; } int SimpleRunApplication(NSString* app_path, NSString* udid, NSMutableArray* cmd_args, bool verbose) { NSString* bundle_id = GetBundleIdentifierFromPath(app_path); RunSimCtl(@[ @"uninstall", udid, bundle_id ], verbose); RunSimCtl(@[ @"install", udid, app_path ], verbose); NSArray* command = [@[ @"launch", @"--console", @"--terminate-running-process", udid, bundle_id ] arrayByAddingObjectsFromArray:cmd_args]; return RunSimCtl(command, verbose); } int RunApplication(NSString* app_path, NSString* xctest_path, NSString* udid, NSMutableDictionary* app_env, NSMutableArray* cmd_args, NSMutableArray* tests_filter, bool verbose) { 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[@"IDEiPhoneInternalTestBundleName"] = app_path.lastPathComponent; testingEnvironmentVariables[@"DYLD_FRAMEWORK_PATH"] = @"__TESTROOT__/Debug-iphonesimulator:__PLATFORMS__/" @"iPhoneSimulator.platform/Developer/Library/Frameworks"; testingEnvironmentVariables[@"DYLD_LIBRARY_PATH"] = @"__TESTROOT__/Debug-iphonesimulator:__PLATFORMS__/" @"iPhoneSimulator.platform/Developer/Library"; if (xctest_path) { testTargetName[@"TestBundlePath"] = xctest_path; testingEnvironmentVariables[@"DYLD_INSERT_LIBRARIES"] = @"__PLATFORMS__/iPhoneSimulator.platform/Developer/" @"usr/lib/libXCTestBundleInject.dylib"; testingEnvironmentVariables[@"XCInjectBundleInto"] = [NSString stringWithFormat:@"__TESTHOST__/%@", app_path.lastPathComponent .stringByDeletingPathExtension]; } else { testTargetName[@"TestBundlePath"] = app_path; } testTargetName[@"TestHostPath"] = app_path; if (app_env.count) { testTargetName[@"EnvironmentVariables"] = app_env; } if (cmd_args.count > 0) { testTargetName[@"CommandLineArguments"] = cmd_args; } if (tests_filter.count > 0) { testTargetName[@"OnlyTestIdentifiers"] = tests_filter; } testTargetName[@"TestingEnvironmentVariables"] = testingEnvironmentVariables; xctestrun[@"TestTargetName"] = testTargetName; NSData* data = [NSPropertyListSerialization dataWithPropertyList:xctestrun format:NSPropertyListXMLFormat_v1_0 options:0 error:nil]; [data writeToFile:tempFilePath atomically:YES]; XCRunTask* task = [[XCRunTask alloc] initWithArguments:@[ @"xcodebuild", @"-xctestrun", tempFilePath, @"-destination", [@"platform=iOS Simulator,id=" stringByAppendingString:udid], @"test-without-building" ]]; 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]; for (NSString* ignore_string in ignore_strings) { if ([log rangeOfString:ignore_string].location != NSNotFound) { return; } } printf("%s", log.UTF8String); }; task.standardError = stderr_pipe; } [task run:verbose]; return [task terminationStatus]; } int main(int argc, char* const argv[]) { 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; bool wants_print_supported_devices = false; bool run_web_test = false; bool prepare_web_test = false; NSString* sdk_version = nil; NSMutableDictionary* app_env = [NSMutableDictionary dictionary]; NSMutableArray* cmd_args = [NSMutableArray array]; NSMutableArray* tests_filter = [NSMutableArray array]; bool verbose_commands = false; SimulatorKill kill_simulator = KILL_BOTH; bool wants_simple_iossim = false; int c; while ((c = getopt(argc, argv, "hs:d:u:t:e:c:pwlvk:i")) != -1) { switch (c) { case 's': sdk_version = @(optarg); break; case 'd': device_name = @(optarg); break; case 'u': udid = @(optarg); break; case 'w': wants_wipe = true; break; case 'c': { NSString* cmd_arg = @(optarg); [cmd_args addObject:cmd_arg]; } break; case 't': { NSString* test = @(optarg); [tests_filter addObject:test]; } break; case 'e': { NSString* envLine = @(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': wants_print_supported_devices = true; break; case 'v': verbose_commands = true; break; case 'k': { NSString* cmd_arg = @(optarg); if ([cmd_arg isEqualToString:@"before"]) { kill_simulator = KILL_BEFORE; } else if ([cmd_arg isEqualToString:@"after"]) { kill_simulator = KILL_AFTER; } else if ([cmd_arg isEqualToString:@"both"]) { kill_simulator = KILL_BOTH; } else if ([cmd_arg isEqualToString:@"never"]) { kill_simulator = KILL_NEVER; } else { PrintUsage(); exit(kExitInvalidArguments); } } break; case 'i': wants_simple_iossim = true; break; case 'h': PrintUsage(); exit(kExitSuccess); default: PrintUsage(); exit(kExitInvalidArguments); } } if (wants_simple_iossim && [tests_filter count]) { LogError(@"Cannot specify tests with -t when using -i."); exit(kExitInvalidArguments); } NSDictionary* simctl_list = GetSimulatorList(verbose_commands); if (wants_print_supported_devices) { PrintSupportedDevices(simctl_list); exit(kExitSuccess); } if (!sdk_version) { float sdk = 0; for (NSDictionary* runtime in Runtimes(simctl_list)) { sdk = fmax(sdk, [runtime[@"version"] floatValue]); } sdk_version = [NSString stringWithFormat:@"%0.1f", sdk]; } NSRange range; for (NSString* cmd_arg in cmd_args) { range = [cmd_arg rangeOfString:@"--run-web-tests"]; if (range.location != NSNotFound) { run_web_test = true; break; } } for (NSString* cmd_arg in cmd_args) { range = [cmd_arg rangeOfString:@"--prepare-web-tests"]; if (range.location != NSNotFound) { prepare_web_test = true; break; } } if (udid == nil) { udid = GetDeviceBySDKAndName(simctl_list, device_name, sdk_version); if (udid == nil) { udid = CreateDeviceBySDKAndName(device_name, sdk_version, verbose_commands); 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, verbose_commands); exit(kExitSuccess); } if (kill_simulator & KILL_BEFORE) { KillSimulator(verbose_commands); } if (wants_wipe) { WipeDevice(udid, verbose_commands); 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_app_path = [NSFileManager.defaultManager stringWithFileSystemRepresentation:argv[optind] length:strlen(argv[optind])]; app_path = ResolvePath(unresolved_app_path); if (!app_path) { LogError(@"Unable to resolve app_path %@", unresolved_app_path); exit(kExitInvalidArguments); } if (++optind < argc) { if (wants_simple_iossim) { fprintf(stderr, "Warning: xctest_path ignored when using -i"); } else { NSString* unresolved_xctest_path = [NSFileManager.defaultManager stringWithFileSystemRepresentation:argv[optind] length:strlen(argv[optind])]; xctest_path = ResolvePath(unresolved_xctest_path); if (!xctest_path) { LogError(@"Unable to resolve xctest_path %@", unresolved_xctest_path); exit(kExitInvalidArguments); } } } } else { LogError(@"Unable to parse command line arguments."); PrintUsage(); exit(kExitInvalidArguments); } if ((prepare_web_test || run_web_test || wants_simple_iossim) && !isSimDeviceBooted(simctl_list, udid)) { RunSimCtl(@[ @"boot", udid ], verbose_commands); } int return_code = -1; if (prepare_web_test) { PrepareWebTests(udid, app_path, verbose_commands); return_code = kExitSuccess; } else if (run_web_test) { return_code = RunWebTest(app_path, udid, cmd_args, verbose_commands); } else if (wants_simple_iossim) { return_code = SimpleRunApplication(app_path, udid, cmd_args, verbose_commands); } else { return_code = RunApplication(app_path, xctest_path, udid, app_env, cmd_args, tests_filter, verbose_commands); } if (kill_simulator & KILL_AFTER) { KillSimulator(verbose_commands); } return return_code; }