Index: testing/iossim/iossim.mm |
diff --git a/testing/iossim/iossim.mm b/testing/iossim/iossim.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..50b50a3a7fd823b0adb7c1802ececfe5ff00e958 |
--- /dev/null |
+++ b/testing/iossim/iossim.mm |
@@ -0,0 +1,522 @@ |
+// Copyright (c) 2012 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#import <Foundation/Foundation.h> |
+#include <asl.h> |
+#include <libgen.h> |
+#include <stdarg.h> |
+#include <stdio.h> |
+ |
+// An executable (iossim) that runs an app in the iOS Simulator. |
+// Run 'iossim -h' for usage information. |
+// |
+// For best results, the iOS Simulator application should not be running when |
+// iossim is invoked. |
+// |
+// Headers for the iPhoneSimulatorRemoteClient framework used in this tool are |
+// generated by class-dump, via GYP. |
+// (class-dump is available at http://www.codethecode.com/projects/class-dump/) |
+// |
+// However, there are some forward declarations required to get things to |
+// compile. Also, the DTiPhoneSimulatorSessionDelegate protocol is referenced |
+// by the iPhoneSimulatorRemoteClient framework, but not defined in the object |
+// file, so it must be defined here before importing the generated |
+// iPhoneSimulatorRemoteClient.h file. |
+ |
+@class DTiPhoneSimulatorApplicationSpecifier; |
+@class DTiPhoneSimulatorSession; |
+@class DTiPhoneSimulatorSessionConfig; |
+@class DTiPhoneSimulatorSystemRoot; |
+ |
+@protocol DTiPhoneSimulatorSessionDelegate |
+- (void)session:(DTiPhoneSimulatorSession*)session |
+ didEndWithError:(NSError*)error; |
+- (void)session:(DTiPhoneSimulatorSession*)session |
+ didStart:(BOOL)started |
+ withError:(NSError*)error; |
+@end |
+ |
+#import "iPhoneSimulatorRemoteClient.h" |
+ |
+// An undocumented system log key included in messages from launchd. The value |
+// is the PID of the process the message is about (as opposed to launchd's PID). |
+#define ASL_KEY_REF_PID "RefPID" |
+ |
+namespace { |
+ |
+// Name of environment variables that control the user's home directory in the |
+// simulator. |
+const char* const kUserHomeEnvVariable = "CFFIXED_USER_HOME"; |
+const char* const kHomeEnvVariable = "HOME"; |
+ |
+// Device family codes for iPhone and iPad. |
+const int kIPhoneFamily = 1; |
+const int kIPadFamily = 2; |
+ |
+// Max number of seconds to wait for the simulator session to start. |
+// This timeout must allow time to start up iOS Simulator, install the app |
+// and perform any other black magic that is encoded in the |
+// iPhoneSimulatorRemoteClient framework to kick things off. Normal start up |
+// time is only a couple seconds but machine load, disk caches, etc., can all |
+// affect startup time in the wild so the timeout needs to be fairly generous. |
+// If this timeout occurs iossim will likely exit with non-zero status; the |
+// exception being if the app is invoked and completes execution before the |
+// session is started (this case is handled in session:didStart:withError). |
+const NSTimeInterval kSessionStartTimeoutSeconds = 30; |
+ |
+// While the simulated app is running, its stdout is redirected to a file which |
+// is polled by iossim and written to iossim's stdout using the following |
+// polling interval. |
+const NSTimeInterval kOutputPollIntervalSeconds = 0.1; |
+ |
+const char* gToolName = "iossim"; |
+ |
+void LogError(NSString* format, ...) { |
+ va_list list; |
+ va_start(list, format); |
+ |
+ NSString* message = |
+ [[[NSString alloc] initWithFormat:format arguments:list] autorelease]; |
+ |
+ fprintf(stderr, "%s: ERROR: %s\n", gToolName, [message UTF8String]); |
+ fflush(stderr); |
+ |
+ va_end(list); |
+} |
+ |
+void LogWarning(NSString* format, ...) { |
+ va_list list; |
+ va_start(list, format); |
+ |
+ NSString* message = |
+ [[[NSString alloc] initWithFormat:format arguments:list] autorelease]; |
+ |
+ fprintf(stderr, "%s: WARNING: %s\n", gToolName, [message UTF8String]); |
+ fflush(stderr); |
+ |
+ va_end(list); |
+} |
+ |
+} // namespace |
+ |
+// A delegate that is called when the simulated app is started or ended in the |
+// simulator. |
+@interface SimulatorDelegate : NSObject <DTiPhoneSimulatorSessionDelegate> { |
+ @private |
+ NSString* stdioPath_; // weak |
+ NSThread* outputThread_; |
+ BOOL appRunning_; |
+} |
+@end |
+ |
+// An implementation that copies the simulated app's stdio to stdout of this |
+// executable. While it would be nice to get stdout and stderr independently |
+// from iOS Simulator, issues like I/O buffering and interleaved output |
+// between iOS Simulator and the app would cause iossim to display things out |
+// of order here. Printing all output to a single file keeps the order correct. |
+// Instances of this classe should be initialized with the location of the |
+// simulated app's output file. When the simulated app starts, a thread is |
+// started which handles copying data from the simulated app's output file to |
+// the stdout of this executable. |
+@implementation SimulatorDelegate |
+ |
+// Specifies the file locations of the simulated app's stdout and stderr. |
+- (SimulatorDelegate*)initWithStdioPath:(NSString*)stdioPath { |
+ self = [super init]; |
+ if (self) |
+ stdioPath_ = [stdioPath copy]; |
+ |
+ return self; |
+} |
+ |
+- (void)dealloc { |
+ [stdioPath_ release]; |
+ [super dealloc]; |
+} |
+ |
+// Reads data from the simulated app's output and writes it to stdout. This |
+// method blocks, so it should be called in a separate thread. The iOS |
+// Simulator takes a file path for the simulated app's stdout and stderr, but |
+// this path isn't always available (e.g. when the stdout is Xcode's build |
+// window). As a workaround, iossim creates a temp file to hold output, which |
+// this method reads and copies to stdout. |
+- (void)tailOutput { |
+ NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; |
+ |
+ // Copy data to stdout/stderr while the app is running. |
+ NSFileHandle* simio = [NSFileHandle fileHandleForReadingAtPath:stdioPath_]; |
+ NSFileHandle* standardOutput = [NSFileHandle fileHandleWithStandardOutput]; |
+ while (appRunning_) { |
+ NSAutoreleasePool* innerPool = [[NSAutoreleasePool alloc] init]; |
+ [standardOutput writeData:[simio readDataToEndOfFile]]; |
+ [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds]; |
+ [innerPool drain]; |
+ } |
+ |
+ // Once the app is no longer running, copy any data that was written during |
+ // the last sleep cycle. |
+ [standardOutput writeData:[simio readDataToEndOfFile]]; |
+ |
+ [pool drain]; |
+} |
+ |
+- (void)session:(DTiPhoneSimulatorSession*)session |
+ didStart:(BOOL)started |
+ withError:(NSError*)error { |
+ if (!started) { |
+ // If the test executes very quickly (<30ms), the SimulatorDelegate may not |
+ // get the initial session:started:withError: message indicating successful |
+ // startup of the simulated app. Instead the delegate will get a |
+ // session:started:withError: message after the timeout has elapsed. To |
+ // account for this case, check if the simulated app's stdio file was |
+ // ever created and if it exists dump it to stdout and return success. |
+ NSFileManager* fileManager = [NSFileManager defaultManager]; |
+ if ([fileManager fileExistsAtPath:stdioPath_ isDirectory:NO]) { |
+ appRunning_ = NO; |
+ [self tailOutput]; |
+ // Note that exiting in this state leaves a process running |
+ // (e.g. /.../iPhoneSimulator4.3.sdk/usr/libexec/installd -t 30) that will |
+ // prevent future simulator sessions from being started for 30 seconds |
+ // unless the iOS Simulator application is killed altogether. |
+ [self session:session didEndWithError:nil]; |
+ |
+ // session:didEndWithError should not return (because it exits) so |
+ // the execution path should never get here. |
+ exit(EXIT_FAILURE); |
+ } |
+ |
+ LogError(@"Simulator failed to start: %@", [error localizedDescription]); |
+ exit(EXIT_FAILURE); |
+ } |
+ |
+ // Start a thread to write contents of outputPath to stdout. |
+ appRunning_ = YES; |
+ outputThread_ = [[NSThread alloc] initWithTarget:self |
+ selector:@selector(tailOutput) |
+ object:nil]; |
+ [outputThread_ start]; |
+} |
+ |
+- (void)session:(DTiPhoneSimulatorSession*)session |
+ didEndWithError:(NSError*)error { |
+ appRunning_ = NO; |
+ // Wait for the output thread to finish copying data to stdout. |
+ if (outputThread_) { |
+ while (![outputThread_ isFinished]) { |
+ [NSThread sleepForTimeInterval:kOutputPollIntervalSeconds]; |
+ } |
+ [outputThread_ release]; |
+ outputThread_ = nil; |
+ } |
+ |
+ if (error) { |
+ LogError(@"Simulator ended with error: %@", [error localizedDescription]); |
+ exit(EXIT_FAILURE); |
+ } |
+ |
+ // Check if the simulated app exited abnormally by looking for system log |
+ // messages from launchd that refer to the simulated app's PID. Limit query |
+ // to messages in the last minute since PIDs are cyclical. |
+ aslmsg query = asl_new(ASL_TYPE_QUERY); |
+ asl_set_query(query, ASL_KEY_SENDER, "launchd", |
+ ASL_QUERY_OP_EQUAL | ASL_QUERY_OP_SUBSTRING); |
+ asl_set_query(query, ASL_KEY_REF_PID, |
+ [[[session simulatedApplicationPID] stringValue] UTF8String], |
+ ASL_QUERY_OP_EQUAL); |
+ asl_set_query(query, ASL_KEY_TIME, "-1m", ASL_QUERY_OP_GREATER_EQUAL); |
+ |
+ // Log any messages found. |
+ aslresponse response = asl_search(NULL, query); |
+ BOOL entryFound = NO; |
+ aslmsg entry; |
+ while ((entry = aslresponse_next(response)) != NULL) { |
+ entryFound = YES; |
+ LogWarning(@"Console message: %s", asl_get(entry, ASL_KEY_MSG)); |
+ } |
+ |
+ // launchd only sends messages if the process crashed or exits with a |
+ // non-zero status, so if the query returned any results iossim should exit |
+ // with non-zero status. |
+ if (entryFound) { |
+ LogError(@"Simulated app crashed or exited with non-zero status"); |
+ exit(EXIT_FAILURE); |
+ } |
+ exit(EXIT_SUCCESS); |
+} |
+@end |
+ |
+namespace { |
+ |
+// Converts the given app path to an application spec, which requires an |
+// absolute path. |
+DTiPhoneSimulatorApplicationSpecifier* BuildAppSpec(NSString* appPath) { |
+ if (![appPath isAbsolutePath]) { |
+ NSString* cwd = [[NSFileManager defaultManager] currentDirectoryPath]; |
+ appPath = [cwd stringByAppendingPathComponent:appPath]; |
+ } |
+ appPath = [appPath stringByStandardizingPath]; |
+ return [DTiPhoneSimulatorApplicationSpecifier |
+ specifierWithApplicationPath:appPath]; |
+} |
+ |
+// Returns the system root for the given SDK version. If sdkVersion is nil, the |
+// default system root is returned. Will return nil if the sdkVersion is not |
+// valid. |
+DTiPhoneSimulatorSystemRoot* BuildSystemRoot(NSString* sdkVersion) { |
+ DTiPhoneSimulatorSystemRoot* systemRoot = |
+ [DTiPhoneSimulatorSystemRoot defaultRoot]; |
+ if (sdkVersion) |
+ systemRoot = [DTiPhoneSimulatorSystemRoot rootWithSDKVersion:sdkVersion]; |
+ |
+ return systemRoot; |
+} |
+ |
+// Builds a config object for starting the specified app. |
+DTiPhoneSimulatorSessionConfig* BuildSessionConfig( |
+ DTiPhoneSimulatorApplicationSpecifier* appSpec, |
+ DTiPhoneSimulatorSystemRoot* systemRoot, |
+ NSString* stdoutPath, |
+ NSString* stderrPath, |
+ NSArray* appArgs, |
+ NSNumber* deviceFamily) { |
+ DTiPhoneSimulatorSessionConfig* sessionConfig = |
+ [[[DTiPhoneSimulatorSessionConfig alloc] init] autorelease]; |
+ sessionConfig.applicationToSimulateOnStart = appSpec; |
+ sessionConfig.simulatedSystemRoot = systemRoot; |
+ sessionConfig.localizedClientName = @"chromium"; |
+ sessionConfig.simulatedApplicationStdErrPath = stderrPath; |
+ sessionConfig.simulatedApplicationStdOutPath = stdoutPath; |
+ sessionConfig.simulatedApplicationLaunchArgs = appArgs; |
+ // TODO(lliabraa): Add support for providing environment variables/values |
+ // for the simulated app's environment. The environment can be set using: |
+ // sessionConfig.simulatedApplicationLaunchEnvironment = |
+ // [NSDictionary dictionary]; |
+ sessionConfig.simulatedDeviceFamily = deviceFamily; |
+ return sessionConfig; |
+} |
+ |
+// Builds a simulator session that will use the given delegate. |
+DTiPhoneSimulatorSession* BuildSession(SimulatorDelegate* delegate) { |
+ DTiPhoneSimulatorSession* session = |
+ [[[DTiPhoneSimulatorSession alloc] init] autorelease]; |
+ session.delegate = delegate; |
+ return session; |
+} |
+ |
+// Creates a temporary directory with a unique name based on the provided |
+// template. The template should not contain any path separators and be suffixed |
+// with X's, which will be substituted with a unique alphanumeric string (see |
+// 'man mkdtemp' for details). The directory will be created as a subdirectory |
+// of NSTemporaryDirectory(). For example, if dirNameTemplate is 'test-XXX', |
+// this method would return something like '/path/to/tempdir/test-3n2'. |
+// |
+// Returns the absolute path of the newly-created directory, or nill if unable |
+// to create a unique directory. |
+NSString* CreateTempDirectory(NSString* dirNameTemplate) { |
+ NSString* fullPathTemplate = |
+ [NSTemporaryDirectory() stringByAppendingPathComponent:dirNameTemplate]; |
+ char* fullPath = mkdtemp(const_cast<char*>([fullPathTemplate UTF8String])); |
+ if (fullPath == NULL) |
+ return nil; |
+ |
+ return [NSString stringWithUTF8String:fullPath]; |
+} |
+ |
+// Creates the necessary directory structure under the given user home directory |
+// path. |
+// Returns YES if successful, NO if unable to create the directories. |
+BOOL CreateHomeDirSubDirs(NSString* userHomePath) { |
+ NSFileManager* fileManager = [NSFileManager defaultManager]; |
+ |
+ // Create user home and subdirectories. |
+ NSArray* subDirsToCreate = [NSArray arrayWithObjects: |
+ @"Documents", |
+ @"Library/Caches", |
+ nil]; |
+ for (NSString* subDir in subDirsToCreate) { |
+ NSString* path = [userHomePath stringByAppendingPathComponent:subDir]; |
+ NSError* error; |
+ if (![fileManager createDirectoryAtPath:path |
+ withIntermediateDirectories:YES |
+ attributes:nil |
+ error:&error]) { |
+ LogError(@"Unable to create directory: %@. Error: %@", |
+ path, [error localizedDescription]); |
+ return NO; |
+ } |
+ } |
+ |
+ return YES; |
+} |
+ |
+// Creates the necessary directory structure under the given user home directory |
+// path, then sets the path in the appropriate environment variable. |
+// Returns YES if successful, NO if unable to create or initialize the given |
+// directory. |
+BOOL InitializeSimulatorUserHome(NSString* userHomePath) { |
+ if (!CreateHomeDirSubDirs(userHomePath)) |
+ return NO; |
+ |
+ // Update the environment to use the specified directory as the user home |
+ // directory. |
+ // Note: the third param of setenv specifies whether or not to overwrite the |
+ // variable's value if it has already been set. |
+ if ((setenv(kUserHomeEnvVariable, [userHomePath UTF8String], YES) == -1) || |
+ (setenv(kHomeEnvVariable, [userHomePath UTF8String], YES) == -1)) { |
+ LogError(@"Unable to set environment variables for home directory."); |
+ return NO; |
+ } |
+ |
+ return YES; |
+} |
+ |
+// Prints the usage information to stderr. |
+void PrintUsage() { |
+ fprintf(stderr, "Usage: iossim [-d device] [-s sdkVersion] [-u homeDir] " |
+ "<appPath> [<appArgs>]\n" |
+ " where <appPath> is the path to the .app directory and appArgs are any" |
+ " arguments to send the simulated app.\n" |
+ "\n" |
+ "Options:\n" |
+ " -d Specifies the device (either 'iPhone' or 'iPad')." |
+ " Defaults to 'iPhone'.\n" |
+ " -s Specifies the SDK version to use (e.g '4.3')." |
+ " Will use system default if not specified.\n" |
+ " -u Specifies a user home directory for the simulator." |
+ " Will create a new directory if not specified.\n"); |
+} |
+ |
+} // namespace |
+ |
+int main(int argc, char* const argv[]) { |
+ NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; |
+ |
+ char* toolName = basename(argv[0]); |
+ if (toolName != NULL) |
+ gToolName = toolName; |
+ |
+ NSString* appPath = nil; |
+ NSString* appName = nil; |
+ NSString* sdkVersion = nil; |
+ NSString* deviceName = @"iPhone"; |
+ NSString* simHomePath = nil; |
+ NSMutableArray* appArgs = [NSMutableArray array]; |
+ |
+ // Parse the optional arguments |
+ int c; |
+ while ((c = getopt(argc, argv, "hs:d:u:")) != -1) { |
+ switch (c) { |
+ case 's': |
+ sdkVersion = [NSString stringWithUTF8String:optarg]; |
+ break; |
+ case 'd': |
+ deviceName = [NSString stringWithUTF8String:optarg]; |
+ break; |
+ case 'u': |
+ simHomePath = [[NSFileManager defaultManager] |
+ stringWithFileSystemRepresentation:optarg length:strlen(optarg)]; |
+ break; |
+ case 'h': |
+ PrintUsage(); |
+ exit(EXIT_SUCCESS); |
+ default: |
+ PrintUsage(); |
+ exit(EXIT_FAILURE); |
+ } |
+ } |
+ |
+ // There should be at least one arg left, specifying the app path. Any |
+ // additional args are passed as arguments to the app. |
+ if (optind < argc) { |
+ appPath = [[NSFileManager defaultManager] |
+ stringWithFileSystemRepresentation:argv[optind] |
+ length:strlen(argv[optind])]; |
+ appName = [appPath lastPathComponent]; |
+ while (++optind < argc) { |
+ [appArgs addObject:[NSString stringWithUTF8String:argv[optind]]]; |
+ } |
+ } else { |
+ LogError(@"Unable to parse command line arguments."); |
+ PrintUsage(); |
+ exit(EXIT_FAILURE); |
+ } |
+ |
+ // Make sure the app path provided is legit. |
+ DTiPhoneSimulatorApplicationSpecifier* appSpec = BuildAppSpec(appPath); |
+ if (!appSpec) { |
+ LogError(@"Invalid app path: %@", appPath); |
+ exit(EXIT_FAILURE); |
+ } |
+ |
+ // Make sure the SDK path provided is legit (or nil). |
+ DTiPhoneSimulatorSystemRoot* systemRoot = BuildSystemRoot(sdkVersion); |
+ if (!systemRoot) { |
+ LogError(@"Invalid SDK version: %@", sdkVersion); |
+ exit(EXIT_FAILURE); |
+ } |
+ |
+ // Get the paths for stdout and stderr so the simulated app's output will show |
+ // up in the caller's stdout/stderr. |
+ NSString* outputDir = CreateTempDirectory(@"iossim-XXXXXX"); |
+ NSString* stdioPath = [outputDir stringByAppendingPathComponent:@"stdio.txt"]; |
+ |
+ // Make sure the device name is legit. |
+ NSNumber* deviceFamily = nil; |
+ if (!deviceName || |
+ [@"iPhone" caseInsensitiveCompare:deviceName] == NSOrderedSame) { |
+ deviceFamily = [NSNumber numberWithInt:kIPhoneFamily]; |
+ } else if ([@"iPad" caseInsensitiveCompare:deviceName] == NSOrderedSame) { |
+ deviceFamily = [NSNumber numberWithInt:kIPadFamily]; |
+ } else { |
+ LogError(@"Invalid device name: %@", deviceName); |
+ exit(EXIT_FAILURE); |
+ } |
+ |
+ // Set up the user home directory for the simulator |
+ if (!simHomePath) { |
+ NSString* dirNameTemplate = |
+ [NSString stringWithFormat:@"iossim-%@-%@-XXXXXX", appName, deviceName]; |
+ simHomePath = CreateTempDirectory(dirNameTemplate); |
+ if (!simHomePath) { |
+ LogError(@"Unable to create unique directory for template %@", |
+ dirNameTemplate); |
+ exit(EXIT_FAILURE); |
+ } |
+ } |
+ if (!InitializeSimulatorUserHome(simHomePath)) { |
+ LogError(@"Unable to initialize home directory for simulator: %@", |
+ simHomePath); |
+ exit(EXIT_FAILURE); |
+ } |
+ |
+ // Create the config and simulator session. |
+ DTiPhoneSimulatorSessionConfig* config = BuildSessionConfig(appSpec, |
+ systemRoot, |
+ stdioPath, |
+ stdioPath, |
+ appArgs, |
+ deviceFamily); |
+ SimulatorDelegate* delegate = |
+ [[[SimulatorDelegate alloc] initWithStdioPath:stdioPath] autorelease]; |
+ DTiPhoneSimulatorSession* session = BuildSession(delegate); |
+ |
+ // Start the simulator session. |
+ NSError* error; |
+ BOOL started = [session requestStartWithConfig:config |
+ timeout:kSessionStartTimeoutSeconds |
+ error:&error]; |
+ |
+ // Spin the runtime indefinitely. When the delegate gets the message that the |
+ // app has quit it will exit this program. |
+ if (started) |
+ [[NSRunLoop mainRunLoop] run]; |
+ else |
+ LogError(@"Simulator failed to start: %@", [error localizedDescription]); |
+ |
+ // Note that this code is only executed if the simulator fails to start |
+ // because once the main run loop is started, only the delegate calling |
+ // exit() will end the program. |
+ [pool drain]; |
+ return EXIT_FAILURE; |
+} |