diff options
author | lliabraa@chromium.org <lliabraa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-07-27 15:30:39 +0000 |
---|---|---|
committer | lliabraa@chromium.org <lliabraa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-07-27 15:30:39 +0000 |
commit | 6bb12cc75319a92820f4584b89fdd7b123a6aa1b (patch) | |
tree | 3271d756b6c159d52918cab22e9665f5f43e7118 /testing | |
parent | 3e13e5e9072aea9e547a3442271055aa179005ef (diff) | |
download | chromium_src-6bb12cc75319a92820f4584b89fdd7b123a6aa1b.zip chromium_src-6bb12cc75319a92820f4584b89fdd7b123a6aa1b.tar.gz chromium_src-6bb12cc75319a92820f4584b89fdd7b123a6aa1b.tar.bz2 |
Add iossim testing tool for running iOS unit tests.
iossim is a command line tool used to run an iOS app in the iOS Simulator.
BUG=None
TEST=None
Review URL: https://chromiumcodereview.appspot.com/10805004
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@148753 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'testing')
-rw-r--r-- | testing/iossim/OWNERS | 2 | ||||
-rw-r--r-- | testing/iossim/iossim.gyp | 60 | ||||
-rw-r--r-- | testing/iossim/iossim.mm | 522 | ||||
-rwxr-xr-x | testing/iossim/redirect-stdout.sh | 20 |
4 files changed, 604 insertions, 0 deletions
diff --git a/testing/iossim/OWNERS b/testing/iossim/OWNERS new file mode 100644 index 0000000..1b3348e --- /dev/null +++ b/testing/iossim/OWNERS @@ -0,0 +1,2 @@ +rohitrao@chromium.org +stuartmorgan@chromium.org diff --git a/testing/iossim/iossim.gyp b/testing/iossim/iossim.gyp new file mode 100644 index 0000000..ffb4f7d --- /dev/null +++ b/testing/iossim/iossim.gyp @@ -0,0 +1,60 @@ +# 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. + +{ + 'variables': { + 'iphone_sim_path': '$(DEVELOPER_DIR)/Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks', + 'other_frameworks_path': '$(DEVELOPER_DIR)/../OtherFrameworks' + }, + 'targets': [ + { + 'target_name': 'iossim', + 'type': 'executable', + 'dependencies': [ + '<(DEPTH)/testing/iossim/third_party/class-dump/class-dump.gyp:class-dump', + ], + 'include_dirs': [ + '<(INTERMEDIATE_DIR)/iossim', + ], + 'sources': [ + 'iossim.mm', + '<(INTERMEDIATE_DIR)/iossim/iPhoneSimulatorRemoteClient.h', + ], + 'libraries': [ + '$(SDKROOT)/System/Library/Frameworks/Foundation.framework', + '<(iphone_sim_path)/iPhoneSimulatorRemoteClient.framework', + ], + 'mac_framework_dirs': [ + '<(iphone_sim_path)', + ], + 'xcode_settings': { + 'LD_RUNPATH_SEARCH_PATHS': [ + '<(iphone_sim_path)', + '<(other_frameworks_path)', + ] + }, + 'actions': [ + { + 'action_name': 'generate_iphone_sim_header', + 'inputs': [ + '<(iphone_sim_path)/iPhoneSimulatorRemoteClient.framework/Versions/Current/iPhoneSimulatorRemoteClient', + '$(BUILD_DIR)/$(CONFIGURATION)/class-dump', + ], + 'outputs': [ + '<(INTERMEDIATE_DIR)/iossim/iPhoneSimulatorRemoteClient.h' + ], + 'action': [ + # Actions don't provide a way to redirect stdout, so a custom + # script is invoked that will execute the first argument and write + # the output to the file specified as the second argument. + '<(DEPTH)/testing/iossim/RedirectStdout.sh', + '$(BUILD_DIR)/$(CONFIGURATION)/class-dump -CiPhoneSimulator <(iphone_sim_path)/iPhoneSimulatorRemoteClient.framework', + '<(INTERMEDIATE_DIR)/iossim/iPhoneSimulatorRemoteClient.h', + ], + 'message': 'Generating header', + }, + ], + }, + ], +} 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; +} diff --git a/testing/iossim/redirect-stdout.sh b/testing/iossim/redirect-stdout.sh new file mode 100755 index 0000000..feff2c9 --- /dev/null +++ b/testing/iossim/redirect-stdout.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# 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. +# +# This script executes the command given as the first argument and redirects +# the command's stdout to the file given as the second argument. +# +# Example: Write the text 'foo' to a file called out.txt: +# RedirectStdout.sh "echo foo" out.txt +# +# This script is invoked from iossim.gyp in order to redirect the output of +# class-dump to a file (because gyp actions don't support redirecting output). + +if [ ${#} -ne 2 ] ; then + echo "usage: ${0} <command> <output file>" + exit 2 +fi + +exec $1 > $2
\ No newline at end of file |