Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(150)

Side by Side Diff: testing/iossim/iossim.mm

Issue 10805004: Add iossim testing tool for running iOS unit tests. (Closed) Base URL: http://git.chromium.org/chromium/src.git@master
Patch Set: addressed review comments Created 8 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« testing/iossim/iossim.gyp ('K') | « testing/iossim/iossim.gyp ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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
5 #import <Foundation/Foundation.h>
6 #include <asl.h>
7 #include <libgen.h>
8 #include <stdarg.h>
9 #include <stdio.h>
10
11 // An executable (iossim) that runs an app in the iOS Simulator.
12 // Run 'iossim -h' for usage information.
13 //
14 // For best results, the iOS Simulator application should not be running when
15 // iossim is invoked.
16 //
17 // Headers for the iPhoneSimulatorRemoteClient framework used in this tool are
18 // generated by class-dump, via GYP.
19 // (class-dump is available at http://www.codethecode.com/projects/class-dump/)
20 //
21 // However, there are some forward declarations required to get things to
22 // compile. Also, the DTiPhoneSimulatorSessionDelegate protocol is referenced
23 // by the iPhoneSimulatorRemoteClient framework, but not defined in the object
24 // file, so it must be defined here before importing the generated
25 // iPhoneSimulatorRemoteClient.h file.
26
27 @class DTiPhoneSimulatorApplicationSpecifier;
28 @class DTiPhoneSimulatorSession;
29 @class DTiPhoneSimulatorSessionConfig;
30 @class DTiPhoneSimulatorSystemRoot;
31
32 @protocol DTiPhoneSimulatorSessionDelegate
33 - (void)session:(DTiPhoneSimulatorSession*)session
34 didEndWithError:(NSError*)error;
35 - (void)session:(DTiPhoneSimulatorSession*)session
36 didStart:(BOOL)started
37 withError:(NSError*)error;
38 @end
39
40 #import "iPhoneSimulatorRemoteClient.h"
41
42 // An undocumented system log key included in messages from launchd. The value
43 // is the PID of the process the message is about (as opposed to launchd's PID).
44 #define ASL_KEY_REF_PID "RefPID"
45
46 namespace {
47
48 // Name of environment variables that control the user's home directory in the
49 // simulator.
50 const char* const kUserHomeEnvVariable = "CFFIXED_USER_HOME";
51 const char* const kHomeEnvVariable = "HOME";
52
53 // Device family codes for iPhone and iPad.
54 const int kIPhoneFamily = 1;
55 const int kIPadFamily = 2;
56
57 // Max number of seconds to wait for the simulator session to start.
58 // This timeout must allow time to start up iOS Simulator, install the app
59 // and perform any other black magic that is encoded in the
60 // iPhoneSimulatorRemoteClient framework to kick things off. Normal start up
61 // time is only a couple seconds but machine load, disk caches, etc., can all
62 // affect startup time in the wild so the timeout needs to be fairly generous.
63 // If this timeout occurs iossim will likely exit with non-zero status; the
64 // exception being if the app is invoked and completes execution before the
65 // session is started (this case is handled in session:didStart:withError).
66 const NSTimeInterval kSessionStartTimeout = 30; // seconds
67
68 const char* gToolName = "iossim";
69
70 void LogError(NSString* format, ...) {
71 va_list list;
72 va_start(list, format);
73
74 NSString* message =
75 [[[NSString alloc] initWithFormat:format arguments:list] autorelease];
76
77 fprintf(stderr, "%s: ERROR: %s\n", gToolName, [message UTF8String]);
78 fflush(stderr);
79
80 va_end(list);
81 }
82
83 void LogWarning(NSString* format, ...) {
84 va_list list;
85 va_start(list, format);
86
87 NSString* message =
88 [[[NSString alloc] initWithFormat:format arguments:list] autorelease];
89
90 fprintf(stderr, "%s: WARNING: %s\n", gToolName, [message UTF8String]);
91 fflush(stderr);
92
93 va_end(list);
94 }
95
96 } // namespace
97
98 // A delegate that is called when the simulated app is started or ended in the
99 // simulator.
100 @interface SimulatorDelegate : NSObject <DTiPhoneSimulatorSessionDelegate> {
101 @private
102 NSString* stdioPath_; // weak
103 NSThread* outputThread_;
104 BOOL appRunning_;
105 }
106 @end
107
108 // An implementation that copies the simulated app's stdio to stdout of this
109 // executable. While it would be nice to get stdout and stderr independently
110 // from iOS Simulator, issues like I/O buffering and interleaved output
111 // between iOS Simulator and the app would cause iossim to display things out
112 // of order here. Printing all output to a single file keeps the order correct.
113 // Instances of this classe should be initialized with the location of the
114 // simulated app's output file. When the simulated app starts, a thread is
115 // started which handles copying data from the simulated app's output file to
116 // the stdout of this executable.
117 @implementation SimulatorDelegate
118
119 // Specifies the file locations of the simulated app's stdout and stderr.
120 - (SimulatorDelegate*)initWithStdioPath:(NSString*)stdioPath {
121 self = [super init];
122 if (self)
123 stdioPath_ = [stdioPath copy];
124
125 return self;
126 }
127
128 - (void)dealloc {
129 [stdioPath_ release];
130 [super dealloc];
131 }
132
133 // Reads data from the simulated app's output and writes it to stdout. This
134 // method blocks, so it should be called in a separate thread. The iOS
135 // Simulator takes a file path for the simulated app's stdout and stderr, but
136 // this path isn't always available (e.g. when the stdout is Xcode's build
137 // window). As a workaround, iossim creates a temp file to hold output, which
138 // this method reads and copies to stdout.
139 - (void)tailOutput {
140 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
141
142 // Copy data to stdout/stderr while the app is running.
143 NSFileHandle* simio = [NSFileHandle fileHandleForReadingAtPath:stdioPath_];
144 NSFileHandle* standardOutput = [NSFileHandle fileHandleWithStandardOutput];
145 while (appRunning_) {
146 NSAutoreleasePool* innerPool = [[NSAutoreleasePool alloc] init];
147 [standardOutput writeData:[simio readDataToEndOfFile]];
148 [NSThread sleepForTimeInterval:0.1]; // seconds
149 [innerPool drain];
150 }
151
152 // Once the app is no longer running, copy any data that was written during
153 // the last sleep cycle.
154 [standardOutput writeData:[simio readDataToEndOfFile]];
155
156 [pool drain];
157 }
158
159 - (void)session:(DTiPhoneSimulatorSession*)session
160 didStart:(BOOL)started
161 withError:(NSError*)error {
162 if (!started) {
163 // If the test executes very quickly (<30ms), the SimulatorDelegate may not
164 // get the initial session:started:withError: message indicating successful
165 // startup of the simulated app. Instead the delegate will get a
166 // session:started:withError: message after the timeout has elapsed. To
167 // account for this case, check if the simulated app's stdio file was
168 // ever created and if it exists dump it to stdout and return success.
169 NSFileManager* fileManager = [NSFileManager defaultManager];
170 if ([fileManager fileExistsAtPath:stdioPath_ isDirectory:NO]) {
171 appRunning_ = NO;
172 [self tailOutput];
173 // Note that exiting in this state leaves a process running
174 // (i.e. /.../iPhoneSimulator4.3.sdk/usr/libexec/installd -t 30) that will
stuartmorgan 2012/07/26 19:47:06 s/i.e./e.g.,/
lliabraa 2012/07/27 13:08:58 Done.
175 // prevent future simulator sessions from being started for 30 seconds
176 // unless the iOS Simulator application is killed altogether.
177 [self session:session didEndWithError:nil];
178
179 // session:didEndWithError should not return (because it exits) so
180 // the execution path should never get here.
181 exit(EXIT_FAILURE);
182 }
183
184 LogError(@"Simulator failed to start: %@", [error localizedDescription]);
185 exit(EXIT_FAILURE);
186 }
187
188 // Start a thread to write contents of outputPath to stdout.
189 appRunning_ = YES;
190 outputThread_ = [[NSThread alloc] initWithTarget:self
191 selector:@selector(tailOutput)
192 object:nil];
193 [outputThread_ start];
194 }
195
196 - (void)session:(DTiPhoneSimulatorSession*)session
197 didEndWithError:(NSError*)error {
198 appRunning_ = NO;
199 // Wait for the output thread to finish copying data to stdout.
200 if (outputThread_) {
201 while (![outputThread_ isFinished]) {
202 [NSThread sleepForTimeInterval:0.1]; // seconds
203 }
204 [outputThread_ release];
205 outputThread_ = nil;
206 }
207
208 if (error) {
209 LogError(@"Simulator ended with error: %@", [error localizedDescription]);
210 exit(EXIT_FAILURE);
211 }
212
213 // Check if the simulated app exited abnormally by looking for system log
214 // messages from launchd that refer to the simulated app's PID. Limit query
215 // to messages in the last minute since PIDs are cyclical.
216 aslmsg query = asl_new(ASL_TYPE_QUERY);
217 asl_set_query(query, ASL_KEY_SENDER, "launchd",
218 ASL_QUERY_OP_EQUAL | ASL_QUERY_OP_SUBSTRING);
219 asl_set_query(query, ASL_KEY_REF_PID,
220 [[[session simulatedApplicationPID] stringValue] UTF8String],
221 ASL_QUERY_OP_EQUAL);
222 asl_set_query(query, ASL_KEY_TIME, "-1m", ASL_QUERY_OP_GREATER_EQUAL);
223
224 // Log any messages found.
225 aslresponse response = asl_search(NULL, query);
226 BOOL entryFound = NO;
227 aslmsg entry;
228 while ((entry = aslresponse_next(response)) != NULL) {
229 entryFound = YES;
230 LogWarning(@"Console message: %s", asl_get(entry, ASL_KEY_MSG));
231 }
232
233 // launchd only sends messages if the process crashed or exits with a
234 // non-zero status, so if the query returned any results iossim should exit
235 // with non-zero status.
236 if (entryFound) {
237 LogError(@"Simulated app crashed or exited with non-zero status");
238 exit(EXIT_FAILURE);
239 }
240 exit(EXIT_SUCCESS);
241 }
242 @end
243
244 namespace {
245
246 // Converts the given app path to an application spec, which requires an
247 // absolute path.
248 DTiPhoneSimulatorApplicationSpecifier* BuildAppSpec(NSString* appPath) {
249 if (![appPath isAbsolutePath]) {
250 NSString* cwd = [[NSFileManager defaultManager] currentDirectoryPath];
251 appPath = [cwd stringByAppendingPathComponent:appPath];
252 }
253 appPath = [appPath stringByStandardizingPath];
254 return [DTiPhoneSimulatorApplicationSpecifier
255 specifierWithApplicationPath:appPath];
256 }
257
258 // Returns the system root for the given SDK version. If sdkVersion is nil, the
259 // default system root is returned. Will return nil if the sdkVersion is not
260 // valid.
261 DTiPhoneSimulatorSystemRoot* BuildSystemRoot(NSString* sdkVersion) {
262 DTiPhoneSimulatorSystemRoot* systemRoot =
263 [DTiPhoneSimulatorSystemRoot defaultRoot];
264 if (sdkVersion)
265 systemRoot = [DTiPhoneSimulatorSystemRoot rootWithSDKVersion:sdkVersion];
266
267 return systemRoot;
268 }
269
270 // Builds a config object for starting the specified app.
271 DTiPhoneSimulatorSessionConfig* BuildSessionConfig(
272 DTiPhoneSimulatorApplicationSpecifier* appSpec,
273 DTiPhoneSimulatorSystemRoot* systemRoot,
274 NSString* stdoutPath,
275 NSString* stderrPath,
276 NSArray* appArgs,
277 NSNumber* deviceFamily) {
278 DTiPhoneSimulatorSessionConfig* sessionConfig =
279 [[[DTiPhoneSimulatorSessionConfig alloc] init] autorelease];
280 sessionConfig.applicationToSimulateOnStart = appSpec;
281 sessionConfig.simulatedSystemRoot = systemRoot;
282 sessionConfig.localizedClientName = @"chromium";
283 sessionConfig.simulatedApplicationStdErrPath = stderrPath;
284 sessionConfig.simulatedApplicationStdOutPath = stdoutPath;
285 sessionConfig.simulatedApplicationLaunchArgs = appArgs;
286 // TODO(lliabraa): Add support for providing environment variables/values
287 // for the simulated app's environment. The environment can be set using:
288 // sessionConfig.simulatedApplicationLaunchEnvironment =
289 // [NSDictionary dictionary];
290 sessionConfig.simulatedDeviceFamily = deviceFamily;
291 return sessionConfig;
292 }
293
294 // Builds a simulator session that will use the given delegate.
295 DTiPhoneSimulatorSession* BuildSession(SimulatorDelegate* delegate) {
296 DTiPhoneSimulatorSession* session =
297 [[[DTiPhoneSimulatorSession alloc] init] autorelease];
298 session.delegate = delegate;
299 return session;
300 }
301
302 // Creates a temporary directory with a unique name based on the provided
303 // template. The template should not contain any path separators and be suffixed
304 // with X's, which will be substituted with a unique alphanumeric string (see
305 // 'man mkdtemp' for details). The directory will be created as a subdirectory
306 // of NSTemporaryDirectory(). For example, if dirNameTemplate is 'test-XXX',
307 // this method would return something like '/path/to/tempdir/test-3n2'.
308 //
309 // Returns the absolute path of the newly-created directory, or nill if unable
310 // to create a unique directory.
311 NSString* CreateTempDirectory(NSString* dirNameTemplate) {
312 NSString* fullPathTemplate =
313 [NSTemporaryDirectory() stringByAppendingPathComponent:dirNameTemplate];
314 char* fullPath = mkdtemp(const_cast<char*>([fullPathTemplate UTF8String]));
315 if (fullPath == NULL)
316 return nil;
317
318 return [NSString stringWithUTF8String:fullPath];
319 }
320
321 // Creates the necessary directory structure under the given user home directory
322 // path.
323 // Returns YES if successful, NO if unable to create the directories.
324 BOOL CreateHomeDirSubDirs(NSString* userHomePath) {
325 NSFileManager* fileManager = [NSFileManager defaultManager];
326
327 // Create user home and subdirectories.
328 NSArray* subDirsToCreate = [NSArray arrayWithObjects:
329 @"Documents",
330 @"Library/Caches",
331 nil];
332 for (NSString* subDir in subDirsToCreate) {
333 NSString* path = [userHomePath stringByAppendingPathComponent:subDir];
334 NSError* error;
335 if (![fileManager createDirectoryAtPath:path
336 withIntermediateDirectories:YES
337 attributes:nil
338 error:&error]) {
339 LogError(@"Unable to create directory: %@. Error: %@",
340 path, [error localizedDescription]);
341 return NO;
342 }
343 }
344
345 return YES;
346 }
347
348 // Creates the necessary directory structure under the given user home directory
349 // path, then sets the path in the appropriate environment variable.
350 // Returns YES if successful, NO if unable to create or initialize the given
351 // directory.
352 BOOL InitializeSimulatorUserHome(NSString* userHomePath) {
353 if (!CreateHomeDirSubDirs(userHomePath))
354 return NO;
355
356 // Update the environment to use the specified directory as the user home
357 // directory.
358 // Note: the third param of setenv specifies whether or not to overwrite the
359 // variable's value if it has already been set.
360 if ((setenv(kUserHomeEnvVariable, [userHomePath UTF8String], YES) == -1) ||
361 (setenv(kHomeEnvVariable, [userHomePath UTF8String], YES) == -1)) {
362 LogError(@"Unable to set environment variables for home directory.");
363 return NO;
364 }
365
366 return YES;
367 }
368
369 // Prints the usage information to stderr.
370 void PrintUsage() {
371 fprintf(stderr, "Usage: iossim [-d device] [-s sdkVersion] [-u homeDir] "
372 "<appPath> [<appArgs>]\n"
373 " where <appPath> is the path to the .app directory and appArgs are any"
374 " arguments to send the simulated app.\n"
375 "\n"
376 "Options:\n"
377 " -d Specifies the device (either 'iPhone' or 'iPad')."
378 " Defaults to 'iPhone'.\n"
379 " -s Specifies the SDK version to use (e.g '4.3')."
380 " Will use system default if not specified.\n"
381 " -u Specifies a user home directory for the simulator."
382 " Will create a new directory if not specified.\n");
383 }
384
385 } // namespace
386
387 int main(int argc, char* const argv[]) {
388 NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
389
390 char* toolName = basename(argv[0]);
391 if (toolName != NULL)
392 gToolName = toolName;
393
394 NSString* appPath = nil;
395 NSString* appName = nil;
396 NSString* sdkVersion = nil;
397 NSString* deviceName = @"iPhone";
398 NSString* simHomePath = nil;
399 NSMutableArray* appArgs = [NSMutableArray array];
400
401 // Parse the optional arguments
402 int c;
403 while ((c = getopt(argc, argv, "hs:d:u:")) != -1) {
404 switch (c) {
405 case 's':
406 sdkVersion = [NSString stringWithUTF8String:optarg];
407 break;
408 case 'd':
409 deviceName = [NSString stringWithUTF8String:optarg];
410 break;
411 case 'u':
412 simHomePath = [[NSFileManager defaultManager]
413 stringWithFileSystemRepresentation:optarg length:strlen(optarg)];
414 break;
415 case 'h':
416 PrintUsage();
417 exit(EXIT_SUCCESS);
418 default:
419 PrintUsage();
420 exit(EXIT_FAILURE);
421 }
422 }
423
424 // There should be at least one arg left, specifying the app path. Any
425 // additional args are passed as arguments to the app.
426 if (optind < argc) {
427 appPath = [[NSFileManager defaultManager]
428 stringWithFileSystemRepresentation:argv[optind]
429 length:strlen(argv[optind])];
430 appName = [appPath lastPathComponent];
431 while (++optind < argc) {
432 [appArgs addObject:[NSString stringWithUTF8String:argv[optind]]];
433 }
434 } else {
435 LogError(@"Unable to parse command line arguments.");
436 PrintUsage();
437 exit(EXIT_FAILURE);
438 }
439
440 // Make sure the app path provided is legit.
441 DTiPhoneSimulatorApplicationSpecifier* appSpec = BuildAppSpec(appPath);
442 if (!appSpec) {
443 LogError(@"Invalid app path: %@", appPath);
444 exit(EXIT_FAILURE);
445 }
446
447 // Make sure the SDK path provided is legit (or nil).
448 DTiPhoneSimulatorSystemRoot* systemRoot = BuildSystemRoot(sdkVersion);
449 if (!systemRoot) {
450 LogError(@"Invalid SDK version: %@", sdkVersion);
451 exit(EXIT_FAILURE);
452 }
453
454 // Get the paths for stdout and stderr so the simulated app's output will show
455 // up in the caller's stdout/stderr.
456 NSString* outputDir = CreateTempDirectory(@"iossim-XXXXXX");
457 NSString* stdioPath = [outputDir stringByAppendingPathComponent:@"stdio.txt"];
458
459 // Make sure the device name is legit.
460 NSNumber* deviceFamily = nil;
461 if (!deviceName ||
462 [@"iPhone" caseInsensitiveCompare:deviceName] == NSOrderedSame) {
463 deviceFamily = [NSNumber numberWithInt:kIPhoneFamily];
464 } else if ([@"iPad" caseInsensitiveCompare:deviceName] == NSOrderedSame) {
465 deviceFamily = [NSNumber numberWithInt:kIPadFamily];
466 } else {
467 LogError(@"Invalid device name: %@", deviceName);
468 exit(EXIT_FAILURE);
469 }
470
471 // Set up the user home directory for the simulator
472 if (!simHomePath) {
473 NSString* dirNameTemplate =
474 [NSString stringWithFormat:@"iossim-%@-%@-XXXXXX", appName, deviceName];
475 simHomePath = CreateTempDirectory(dirNameTemplate);
476 if (!simHomePath) {
477 LogError(@"Unable to create unique directory for template %@",
478 dirNameTemplate);
479 exit(EXIT_FAILURE);
480 }
481 }
482 if (!InitializeSimulatorUserHome(simHomePath)) {
483 LogError(@"Unable to initialize home directory for simulator: %@",
484 simHomePath);
485 exit(EXIT_FAILURE);
486 }
487
488 // Create the config and simulator session.
489 DTiPhoneSimulatorSessionConfig* config = BuildSessionConfig(appSpec,
490 systemRoot,
491 stdioPath,
492 stdioPath,
493 appArgs,
494 deviceFamily);
495 SimulatorDelegate* delegate =
496 [[[SimulatorDelegate alloc] initWithStdioPath:stdioPath] autorelease];
497 DTiPhoneSimulatorSession* session = BuildSession(delegate);
498
499 // Start the simulator session.
500 NSError* error;
501 BOOL started = [session requestStartWithConfig:config
502 timeout:kSessionStartTimeout
503 error:&error];
504
505 // Spin the runtime indefinitely. When the delegate gets the message that the
506 // app has quit it will exit this program.
507 if (started)
508 [[NSRunLoop mainRunLoop] run];
509 else
510 LogError(@"Simulator failed to start: %@", [error localizedDescription]);
511
512 // Note that this code is only executed if the simulator fails to start
513 // because once the main run loop is started, only the delegate calling
514 // exit() will end the program.
515 [pool drain];
516 return EXIT_FAILURE;
517 }
OLDNEW
« testing/iossim/iossim.gyp ('K') | « testing/iossim/iossim.gyp ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698