• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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