iossim.mm 18 KB

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