summaryrefslogtreecommitdiffstats
path: root/testing/iossim/iossim.mm
diff options
context:
space:
mode:
Diffstat (limited to 'testing/iossim/iossim.mm')
-rw-r--r--testing/iossim/iossim.mm522
1 files changed, 522 insertions, 0 deletions
diff --git a/testing/iossim/iossim.mm b/testing/iossim/iossim.mm
new file mode 100644
index 0000000..50b50a3
--- /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;
+}