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

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

Powered by Google App Engine
This is Rietveld 408576698