1// Copyright 2012 The Chromium Authors 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4#import <Foundation/Foundation.h> 5#include <getopt.h> 6#include <string> 7 8namespace { 9 10void PrintUsage() { 11 fprintf( 12 stderr, 13 "Usage: iossim [-d device] [-s sdk_version] <app_path> <xctest_path>\n" 14 " where <app_path> is the path to the .app directory and <xctest_path> " 15 "is the path to an optional xctest bundle.\n" 16 "Options:\n" 17 " -u Specifies the device udid to use. Will use -d, -s values to get " 18 "devices if not specified.\n" 19 " -d Specifies the device (must be one of the values from the iOS " 20 "Simulator's Hardware -> Device menu. Defaults to 'iPhone 6s'.\n" 21 " -w Wipe the device's contents and settings before running the " 22 "test.\n" 23 " -e Specifies an environment key=value pair that will be" 24 " set in the simulated application's environment.\n" 25 " -t Specifies a test or test suite that should be included in the " 26 "test run. All other tests will be excluded from this run.\n" 27 " -c Specifies command line flags to pass to application.\n" 28 " -p Print the device's home directory, does not run a test.\n" 29 " -s Specifies the SDK version to use (e.g '9.3'). Will use system " 30 "default if not specified.\n"); 31} 32 33// Exit status codes. 34const int kExitSuccess = EXIT_SUCCESS; 35const int kExitInvalidArguments = 2; 36 37void LogError(NSString* format, ...) { 38 va_list list; 39 va_start(list, format); 40 41 NSString* message = 42 [[[NSString alloc] initWithFormat:format arguments:list] autorelease]; 43 44 fprintf(stderr, "iossim: ERROR: %s\n", [message UTF8String]); 45 fflush(stderr); 46 47 va_end(list); 48} 49 50} 51 52// Wrap boiler plate calls to xcrun NSTasks. 53@interface XCRunTask : NSObject { 54 NSTask* _task; 55} 56- (instancetype)initWithArguments:(NSArray*)arguments; 57- (void)run; 58- (void)setStandardOutput:(id)output; 59- (void)setStandardError:(id)error; 60- (int)getTerminationStatus; 61@end 62 63@implementation XCRunTask 64 65- (instancetype)initWithArguments:(NSArray*)arguments { 66 self = [super init]; 67 if (self) { 68 _task = [[NSTask alloc] init]; 69 SEL selector = @selector(setStartsNewProcessGroup:); 70 if ([_task respondsToSelector:selector]) 71 [_task performSelector:selector withObject:nil]; 72 [_task setLaunchPath:@"/usr/bin/xcrun"]; 73 [_task setArguments:arguments]; 74 } 75 return self; 76} 77 78- (void)dealloc { 79 [_task release]; 80 [super dealloc]; 81} 82 83- (void)setStandardOutput:(id)output { 84 [_task setStandardOutput:output]; 85} 86 87- (void)setStandardError:(id)error { 88 [_task setStandardError:error]; 89} 90 91- (int)getTerminationStatus { 92 return [_task terminationStatus]; 93} 94 95- (void)run { 96 [_task launch]; 97 [_task waitUntilExit]; 98} 99 100- (void)launch { 101 [_task launch]; 102} 103 104- (void)waitUntilExit { 105 [_task waitUntilExit]; 106} 107 108@end 109 110// Return array of available iOS runtime dictionaries. Unavailable (old Xcode 111// versions) or other runtimes (tvOS, watchOS) are removed. 112NSArray* Runtimes(NSDictionary* simctl_list) { 113 NSMutableArray* runtimes = 114 [[simctl_list[@"runtimes"] mutableCopy] autorelease]; 115 for (NSDictionary* runtime in simctl_list[@"runtimes"]) { 116 BOOL available = 117 [runtime[@"availability"] isEqualToString:@"(available)"] || 118 runtime[@"isAvailable"]; 119 120 if (![runtime[@"identifier"] 121 hasPrefix:@"com.apple.CoreSimulator.SimRuntime.iOS"] || 122 !available) { 123 [runtimes removeObject:runtime]; 124 } 125 } 126 return runtimes; 127} 128 129// Return array of device dictionaries. 130NSArray* Devices(NSDictionary* simctl_list) { 131 NSMutableArray* devicetypes = 132 [[simctl_list[@"devicetypes"] mutableCopy] autorelease]; 133 for (NSDictionary* devicetype in simctl_list[@"devicetypes"]) { 134 if (![devicetype[@"identifier"] 135 hasPrefix:@"com.apple.CoreSimulator.SimDeviceType.iPad"] && 136 ![devicetype[@"identifier"] 137 hasPrefix:@"com.apple.CoreSimulator.SimDeviceType.iPhone"]) { 138 [devicetypes removeObject:devicetype]; 139 } 140 } 141 return devicetypes; 142} 143 144// Get list of devices, runtimes, etc from sim_ctl. 145NSDictionary* GetSimulatorList() { 146 XCRunTask* task = [[[XCRunTask alloc] 147 initWithArguments:@[ @"simctl", @"list", @"-j" ]] autorelease]; 148 NSPipe* out = [NSPipe pipe]; 149 [task setStandardOutput:out]; 150 151 // In the rest of the this file we read from the pipe after -waitUntilExit 152 // (We normally wrap -launch and -waitUntilExit in one -run method). However, 153 // on some swarming slaves this led to a hang on simctl's pipe. Since the 154 // output of simctl is so instant, reading it before exit seems to work, and 155 // seems to avoid the hang. 156 [task launch]; 157 NSData* data = [[out fileHandleForReading] readDataToEndOfFile]; 158 [task waitUntilExit]; 159 160 NSError* error = nil; 161 return [NSJSONSerialization JSONObjectWithData:data 162 options:kNilOptions 163 error:&error]; 164} 165 166// List supported runtimes and devices. 167void PrintSupportedDevices(NSDictionary* simctl_list) { 168 printf("\niOS devices:\n"); 169 for (NSDictionary* type in Devices(simctl_list)) { 170 printf("%s\n", [type[@"name"] UTF8String]); 171 } 172 printf("\nruntimes:\n"); 173 for (NSDictionary* runtime in Runtimes(simctl_list)) { 174 printf("%s\n", [runtime[@"version"] UTF8String]); 175 } 176} 177 178// Expand path to absolute path. 179NSString* ResolvePath(NSString* path) { 180 path = [path stringByExpandingTildeInPath]; 181 path = [path stringByStandardizingPath]; 182 const char* cpath = [path cStringUsingEncoding:NSUTF8StringEncoding]; 183 char* resolved_name = NULL; 184 char* abs_path = realpath(cpath, resolved_name); 185 if (abs_path == NULL) { 186 return nil; 187 } 188 return [NSString stringWithCString:abs_path encoding:NSUTF8StringEncoding]; 189} 190 191// Search |simctl_list| for a udid matching |device_name| and |sdk_version|. 192NSString* GetDeviceBySDKAndName(NSDictionary* simctl_list, 193 NSString* device_name, 194 NSString* sdk_version) { 195 NSString* sdk = nil; 196 NSString* name = nil; 197 // Get runtime identifier based on version property to handle 198 // cases when version and identifier are not the same, 199 // e.g. below identifer is *13-2 but version is 13.2.2 200 // { 201 // "version" : "13.2.2", 202 // "bundlePath" : "path" 203 // "identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-13-2", 204 // "buildversion" : "17K90" 205 // } 206 for (NSDictionary* runtime in Runtimes(simctl_list)) { 207 if ([runtime[@"version"] isEqualToString:sdk_version]) { 208 sdk = runtime[@"identifier"]; 209 name = runtime[@"name"]; 210 break; 211 } 212 } 213 if (sdk == nil) { 214 printf("\nDid not find Runtime with specified version.\n"); 215 PrintSupportedDevices(simctl_list); 216 exit(kExitInvalidArguments); 217 } 218 NSArray* devices = [simctl_list[@"devices"] objectForKey:sdk]; 219 if (devices == nil || [devices count] == 0) { 220 // Specific for XCode 10.1 and lower, 221 // where name from 'runtimes' uses as a key in 'devices'. 222 devices = [simctl_list[@"devices"] objectForKey:name]; 223 } 224 for (NSDictionary* device in devices) { 225 if ([device[@"name"] isEqualToString:device_name]) { 226 return device[@"udid"]; 227 } 228 } 229 return nil; 230} 231 232// Create and return a device udid of |device| and |sdk_version|. 233NSString* CreateDeviceBySDKAndName(NSString* device, NSString* sdk_version) { 234 NSString* sdk = [@"iOS" stringByAppendingString:sdk_version]; 235 XCRunTask* create = [[[XCRunTask alloc] 236 initWithArguments:@[ @"simctl", @"create", device, device, sdk ]] 237 autorelease]; 238 [create run]; 239 240 NSDictionary* simctl_list = GetSimulatorList(); 241 return GetDeviceBySDKAndName(simctl_list, device, sdk_version); 242} 243 244bool FindDeviceByUDID(NSDictionary* simctl_list, NSString* udid) { 245 NSDictionary* devices_table = simctl_list[@"devices"]; 246 for (id runtimes in devices_table) { 247 NSArray* devices = devices_table[runtimes]; 248 for (NSDictionary* device in devices) { 249 if ([device[@"udid"] isEqualToString:udid]) { 250 return true; 251 } 252 } 253 } 254 return false; 255} 256 257// Prints the HOME environment variable for a device. Used by the bots to 258// package up all the test data. 259void PrintDeviceHome(NSString* udid) { 260 XCRunTask* task = [[[XCRunTask alloc] 261 initWithArguments:@[ @"simctl", @"getenv", udid, @"HOME" ]] autorelease]; 262 [task run]; 263} 264 265// Erase a device, used by the bots before a clean test run. 266void WipeDevice(NSString* udid) { 267 XCRunTask* shutdown = [[[XCRunTask alloc] 268 initWithArguments:@[ @"simctl", @"shutdown", udid ]] autorelease]; 269 [shutdown setStandardOutput:nil]; 270 [shutdown setStandardError:nil]; 271 [shutdown run]; 272 273 XCRunTask* erase = [[[XCRunTask alloc] 274 initWithArguments:@[ @"simctl", @"erase", udid ]] autorelease]; 275 [erase run]; 276} 277 278void KillSimulator() { 279 XCRunTask* task = [[[XCRunTask alloc] 280 initWithArguments:@[ @"killall", @"Simulator" ]] autorelease]; 281 [task setStandardOutput:nil]; 282 [task setStandardError:nil]; 283 [task run]; 284} 285 286int RunApplication(NSString* app_path, 287 NSString* xctest_path, 288 NSString* udid, 289 NSMutableDictionary* app_env, 290 NSMutableArray* cmd_args, 291 NSMutableArray* tests_filter) { 292 NSString* tempFilePath = [NSTemporaryDirectory() 293 stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]]; 294 [[NSFileManager defaultManager] createFileAtPath:tempFilePath 295 contents:nil 296 attributes:nil]; 297 298 NSMutableDictionary* xctestrun = [NSMutableDictionary dictionary]; 299 NSMutableDictionary* testTargetName = [NSMutableDictionary dictionary]; 300 301 NSMutableDictionary* testingEnvironmentVariables = 302 [NSMutableDictionary dictionary]; 303 [testingEnvironmentVariables setValue:[app_path lastPathComponent] 304 forKey:@"IDEiPhoneInternalTestBundleName"]; 305 306 [testingEnvironmentVariables 307 setValue: 308 @"__TESTROOT__/Debug-iphonesimulator:__PLATFORMS__/" 309 @"iPhoneSimulator.platform/Developer/Library/Frameworks" 310 forKey:@"DYLD_FRAMEWORK_PATH"]; 311 [testingEnvironmentVariables 312 setValue: 313 @"__TESTROOT__/Debug-iphonesimulator:__PLATFORMS__/" 314 @"iPhoneSimulator.platform/Developer/Library" 315 forKey:@"DYLD_LIBRARY_PATH"]; 316 317 if (xctest_path) { 318 [testTargetName setValue:xctest_path forKey:@"TestBundlePath"]; 319 NSString* inject = @"__PLATFORMS__/iPhoneSimulator.platform/Developer/" 320 @"usr/lib/libXCTestBundleInject.dylib"; 321 [testingEnvironmentVariables setValue:inject 322 forKey:@"DYLD_INSERT_LIBRARIES"]; 323 [testingEnvironmentVariables 324 setValue:[NSString stringWithFormat:@"__TESTHOST__/%@", 325 [[app_path lastPathComponent] 326 stringByDeletingPathExtension]] 327 forKey:@"XCInjectBundleInto"]; 328 } else { 329 [testTargetName setValue:app_path forKey:@"TestBundlePath"]; 330 } 331 [testTargetName setValue:app_path forKey:@"TestHostPath"]; 332 333 if ([app_env count]) { 334 [testTargetName setObject:app_env forKey:@"EnvironmentVariables"]; 335 } 336 337 if ([cmd_args count] > 0) { 338 [testTargetName setObject:cmd_args forKey:@"CommandLineArguments"]; 339 } 340 341 if ([tests_filter count] > 0) { 342 [testTargetName setObject:tests_filter forKey:@"OnlyTestIdentifiers"]; 343 } 344 345 [testTargetName setObject:testingEnvironmentVariables 346 forKey:@"TestingEnvironmentVariables"]; 347 [xctestrun setObject:testTargetName forKey:@"TestTargetName"]; 348 349 NSData* data = [NSPropertyListSerialization 350 dataWithPropertyList:xctestrun 351 format:NSPropertyListXMLFormat_v1_0 352 options:0 353 error:nil]; 354 [data writeToFile:tempFilePath atomically:YES]; 355 XCRunTask* task = [[[XCRunTask alloc] initWithArguments:@[ 356 @"xcodebuild", @"-xctestrun", tempFilePath, @"-destination", 357 [@"platform=iOS Simulator,id=" stringByAppendingString:udid], 358 @"test-without-building" 359 ]] autorelease]; 360 361 if (!xctest_path) { 362 // The following stderr messages are meaningless on iossim when not running 363 // xctests and can be safely stripped. 364 NSArray* ignore_strings = @[ 365 @"IDETestOperationsObserverErrorDomain", @"** TEST EXECUTE FAILED **" 366 ]; 367 NSPipe* stderr_pipe = [NSPipe pipe]; 368 stderr_pipe.fileHandleForReading.readabilityHandler = 369 ^(NSFileHandle* handle) { 370 NSString* log = [[[NSString alloc] initWithData:handle.availableData 371 encoding:NSUTF8StringEncoding] 372 autorelease]; 373 for (NSString* ignore_string in ignore_strings) { 374 if ([log rangeOfString:ignore_string].location != NSNotFound) { 375 return; 376 } 377 } 378 printf("%s", [log UTF8String]); 379 }; 380 [task setStandardError:stderr_pipe]; 381 } 382 [task run]; 383 return [task getTerminationStatus]; 384} 385 386int main(int argc, char* const argv[]) { 387 // When the last running simulator is from Xcode 7, an Xcode 8 run will yeild 388 // a failure to "unload a stale CoreSimulatorService job" message. Sending a 389 // hidden simctl to do something simple (list devices) helpfully works around 390 // this issue. 391 XCRunTask* workaround_task = [[[XCRunTask alloc] 392 initWithArguments:@[ @"simctl", @"list", @"-j" ]] autorelease]; 393 [workaround_task setStandardOutput:nil]; 394 [workaround_task setStandardError:nil]; 395 [workaround_task run]; 396 397 NSString* app_path = nil; 398 NSString* xctest_path = nil; 399 NSString* udid = nil; 400 NSString* device_name = @"iPhone 6s"; 401 bool wants_wipe = false; 402 bool wants_print_home = false; 403 NSDictionary* simctl_list = GetSimulatorList(); 404 float sdk = 0; 405 for (NSDictionary* runtime in Runtimes(simctl_list)) { 406 sdk = fmax(sdk, [runtime[@"version"] floatValue]); 407 } 408 NSString* sdk_version = [NSString stringWithFormat:@"%0.1f", sdk]; 409 NSMutableDictionary* app_env = [NSMutableDictionary dictionary]; 410 NSMutableArray* cmd_args = [NSMutableArray array]; 411 NSMutableArray* tests_filter = [NSMutableArray array]; 412 413 int c; 414 while ((c = getopt(argc, argv, "hs:d:u:t:e:c:pwl")) != -1) { 415 switch (c) { 416 case 's': 417 sdk_version = [NSString stringWithUTF8String:optarg]; 418 break; 419 case 'd': 420 device_name = [NSString stringWithUTF8String:optarg]; 421 break; 422 case 'u': 423 udid = [NSString stringWithUTF8String:optarg]; 424 break; 425 case 'w': 426 wants_wipe = true; 427 break; 428 case 'c': { 429 NSString* cmd_arg = [NSString stringWithUTF8String:optarg]; 430 [cmd_args addObject:cmd_arg]; 431 } break; 432 case 't': { 433 NSString* test = [NSString stringWithUTF8String:optarg]; 434 [tests_filter addObject:test]; 435 } break; 436 case 'e': { 437 NSString* envLine = [NSString stringWithUTF8String:optarg]; 438 NSRange range = [envLine rangeOfString:@"="]; 439 if (range.location == NSNotFound) { 440 LogError(@"Invalid key=value argument for -e."); 441 PrintUsage(); 442 exit(kExitInvalidArguments); 443 } 444 NSString* key = [envLine substringToIndex:range.location]; 445 NSString* value = [envLine substringFromIndex:(range.location + 1)]; 446 [app_env setObject:value forKey:key]; 447 } break; 448 case 'p': 449 wants_print_home = true; 450 break; 451 case 'l': 452 PrintSupportedDevices(simctl_list); 453 exit(kExitSuccess); 454 case 'h': 455 PrintUsage(); 456 exit(kExitSuccess); 457 default: 458 PrintUsage(); 459 exit(kExitInvalidArguments); 460 } 461 } 462 463 if (udid == nil) { 464 udid = GetDeviceBySDKAndName(simctl_list, device_name, sdk_version); 465 if (udid == nil) { 466 udid = CreateDeviceBySDKAndName(device_name, sdk_version); 467 if (udid == nil) { 468 LogError(@"Unable to find a device %@ with SDK %@.", device_name, 469 sdk_version); 470 PrintSupportedDevices(simctl_list); 471 exit(kExitInvalidArguments); 472 } 473 } 474 } else { 475 if (!FindDeviceByUDID(simctl_list, udid)) { 476 LogError( 477 @"Unable to find a device with udid %@. Use 'xcrun simctl list' to " 478 @"see valid device udids.", 479 udid); 480 exit(kExitInvalidArguments); 481 } 482 } 483 484 if (wants_print_home) { 485 PrintDeviceHome(udid); 486 exit(kExitSuccess); 487 } 488 489 KillSimulator(); 490 if (wants_wipe) { 491 WipeDevice(udid); 492 printf("Device wiped.\n"); 493 exit(kExitSuccess); 494 } 495 496 // There should be at least one arg left, specifying the app path. Any 497 // additional args are passed as arguments to the app. 498 if (optind < argc) { 499 NSString* unresolved_app_path = [[NSFileManager defaultManager] 500 stringWithFileSystemRepresentation:argv[optind] 501 length:strlen(argv[optind])]; 502 app_path = ResolvePath(unresolved_app_path); 503 if (!app_path) { 504 LogError(@"Unable to resolve app_path %@", unresolved_app_path); 505 exit(kExitInvalidArguments); 506 } 507 508 if (++optind < argc) { 509 NSString* unresolved_xctest_path = [[NSFileManager defaultManager] 510 stringWithFileSystemRepresentation:argv[optind] 511 length:strlen(argv[optind])]; 512 xctest_path = ResolvePath(unresolved_xctest_path); 513 if (!xctest_path) { 514 LogError(@"Unable to resolve xctest_path %@", unresolved_xctest_path); 515 exit(kExitInvalidArguments); 516 } 517 } 518 } else { 519 LogError(@"Unable to parse command line arguments."); 520 PrintUsage(); 521 exit(kExitInvalidArguments); 522 } 523 524 int return_code = RunApplication(app_path, xctest_path, udid, app_env, 525 cmd_args, tests_filter); 526 KillSimulator(); 527 return return_code; 528} 529