From e48eaebd02082efa6c5efa736c9b33eb1bdb7fae Mon Sep 17 00:00:00 2001 From: Mansfield Mark Date: Fri, 8 May 2020 11:44:52 -0700 Subject: [PATCH] Record video using simctl --- README.md | 69 ++++++++++++------------ bluepill/tests/BPIntegrationTests.m | 67 ++++++++++++++++++++++- bp/src/BPConfiguration.h | 1 + bp/src/BPConfiguration.m | 22 +++++++- bp/src/BPTestBundleConnection.h | 4 +- bp/src/BPTestBundleConnection.m | 82 ++++++++++++++++++++++++++++- bp/src/BPUtils.h | 24 +++++++++ bp/src/BPUtils.m | 43 +++++++++------ bp/src/Bluepill.m | 2 +- bp/src/SimulatorMonitor.m | 4 +- bp/tests/BPUtilsTests.m | 23 ++++++++ 11 files changed, 282 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 934791b9..de209c30 100644 --- a/README.md +++ b/README.md @@ -54,41 +54,42 @@ $ bluepill -c config.json A full list supported options are listed here. -| Config Arguments | Command Line Arguments | Explanation | Required | Default value | -|:----------------------:|:----------------------:|-------------------------------------------------------------------------------------|:--------:|:----------------:| -| app | -a | The path to the host application to execute (your `.app`) | N | n/a | -| xctestrun-path | | The path to the `.xctestrun` file that xcode leaves when you `build-for-testing`. | Y | n/a | +| Config Arguments | Command Line Arguments | Explanation | Required | Default value | +|:----------------------:|:----------------------:|----------------------------------------------------------------------------------------------|:--------:|:----------------:| +| app | -a | The path to the host application to execute (your `.app`) | N | n/a | +| xctestrun-path | | The path to the `.xctestrun` file that xcode leaves when you `build-for-testing`. | Y | n/a | | test-plan-path | | The path of a json file which describes the test plan. It is equivalent to the `.xctestrun` file generated by Xcode, but it can be generated by a different build system, e.g. Bazel | Y | n/a | -| output-dir | -o | Directory where to put output log files. **(bluepill only)** | Y | n/a | -| config | -c | Read options from the specified configuration file instead of the command line. | N | n/a | -| device | -d | On which device to run the app. | N | iPhone 8 | -| exclude | -x | Exclude a testcase in the set of tests to run (takes priority over `include`). | N | empty | -| headless | -H | Run in headless mode (no GUI). | N | off | -| clone-simulator | -L | Spawn simulator by clone from simulator template. | N | off | -| xcode-path | -X | Path to xcode. | N | xcode-select -p | -| include | -i | Include a testcase in the set of tests to run (unless specified in `exclude`). | N | all tests | -| list-tests | -l | Only list tests and exit without executing tests. | N | false | -| num-sims | -n | Number of simulators to run in parallel. **(bluepill only)** | N | 4 | -| printf-config | -P | Print a configuration file suitable for passing back using the `-c` option. | N | n/a | -| error-retries | -R | Number of times to recover from simulator/app crashing/hanging and continue running.| N | 4 | -| failure-tolerance | -f | Number of times to retry on test failures | N | 0 | -| only-retry-failed | -F | Only retry failed tests instead of all. Also retry test that timed-out/crashed. | N | false | -| runtime | -r | What runtime to use. | N | iOS 13.3 | -| stuck-timeout | -S | Timeout in seconds for a test that seems stuck (no output). | N | 300s | -| test-timeout | -T | Timeout in seconds for a test that is producing output. | N | 300s | -| test-bundle-path | -t | The path to the test bundle to execute (single `.xctest`). | N | n/a | -| additional-unit-xctests| n/a | Additional XCTest bundles that is not Plugin folder | N | n/a | -| additional-ui-xctests | n/a | Additional XCTUITest bundles that is not Plugin folder | N | n/a | -| repeat-count | -C | Number of times we'll run the entire test suite (used for load testing). | N | 1 | -| no-split | -N | Test bundles you don't want to be packed into different groups to run in parallel. | N | n/a | -| quiet | -q | Turn off all output except fatal errors. | N | YES | -| diagnostics | n/a | Enable collection of diagnostics in output directory in case of test failures. | N | NO | -| help | -h | Help. | N | n/a | -| runner-app-path | -u | The test runner for UI tests. | N | n/a | -| screenshots-directory | n/a | Directory where simulator screenshots for failed ui tests will be stored. | N | n/a | -| video-paths | -V | A list of videos that will be saved in the simulators. | N | n/a | -| image-paths | -I | A list of images that will be saved in the simulators. | N | n/a | -| unsafe-skip-xcode-version-check | | Skip Xcode version check | N | NO | +| output-dir | -o | Directory where to put output log files. **(bluepill only)** | Y | n/a | +| config | -c | Read options from the specified configuration file instead of the command line. | N | n/a | +| device | -d | On which device to run the app. | N | iPhone 8 | +| exclude | -x | Exclude a testcase in the set of tests to run (takes priority over `include`). | N | empty | +| headless | -H | Run in headless mode (no GUI). | N | off | +| clone-simulator | -L | Spawn simulator by clone from simulator template. | N | off | +| xcode-path | -X | Path to xcode. | N | xcode-select -p | +| include | -i | Include a testcase in the set of tests to run (unless specified in `exclude`). | N | all tests | +| list-tests | -l | Only list tests and exit without executing tests. | N | false | +| num-sims | -n | Number of simulators to run in parallel. **(bluepill only)** | N | 4 | +| printf-config | -P | Print a configuration file suitable for passing back using the `-c` option. | N | n/a | +| error-retries | -R | Number of times to recover from simulator/app crashing/hanging and continue running. | N | 4 | +| failure-tolerance | -f | Number of times to retry on test failures | N | 0 | +| only-retry-failed | -F | Only retry failed tests instead of all. Also retry test that timed-out/crashed. | N | false | +| runtime | -r | What runtime to use. | N | iOS 13.3 | +| stuck-timeout | -S | Timeout in seconds for a test that seems stuck (no output). | N | 300s | +| test-timeout | -T | Timeout in seconds for a test that is producing output. | N | 300s | +| test-bundle-path | -t | The path to the test bundle to execute (single `.xctest`). | N | n/a | +| additional-unit-xctests| n/a | Additional XCTest bundles that is not Plugin folder | N | n/a | +| additional-ui-xctests | n/a | Additional XCTUITest bundles that is not Plugin folder | N | n/a | +| repeat-count | -C | Number of times we'll run the entire test suite (used for load testing). | N | 1 | +| no-split | -N | Test bundles you don't want to be packed into different groups to run in parallel. | N | n/a | +| quiet | -q | Turn off all output except fatal errors. | N | YES | +| diagnostics | n/a | Enable collection of diagnostics in output directory in case of test failures. | N | NO | +| help | -h | Help. | N | n/a | +| runner-app-path | -u | The test runner for UI tests. | N | n/a | +| screenshots-directory | n/a | Directory where simulator screenshots for failed ui tests will be stored. | N | n/a | +| videos-directory | n/a | Directory where videos of test runs will be saved. If not provided, videos are not recorded. | N | n/a | +| video-paths | -V | A list of videos that will be saved in the simulators. | N | n/a | +| image-paths | -I | A list of images that will be saved in the simulators. | N | n/a | +| unsafe-skip-xcode-version-check | | Skip Xcode version check | N | NO | ## Exit Status diff --git a/bluepill/tests/BPIntegrationTests.m b/bluepill/tests/BPIntegrationTests.m index 91f71d66..1bfeebfb 100644 --- a/bluepill/tests/BPIntegrationTests.m +++ b/bluepill/tests/BPIntegrationTests.m @@ -21,7 +21,7 @@ @interface BPIntegrationTests : XCTestCase @implementation BPIntegrationTests -- (BPConfiguration *)generateConfig { +- (BPConfiguration *)generateConfigWithVideoDir:(NSString *)videoDir { NSString *hostApplicationPath = [BPTestHelper sampleAppPath]; NSString *testBundlePath = [BPTestHelper sampleAppBalancingTestsBundlePath]; BPConfiguration *config = [[BPConfiguration alloc] initWithProgram:BP_MASTER]; @@ -36,9 +36,16 @@ - (BPConfiguration *)generateConfig { config.deviceType = @BP_DEFAULT_DEVICE_TYPE; config.headlessMode = YES; config.quiet = [BPUtils isBuildScript]; + if (videoDir != nil) { + config.videosDirectory = videoDir; + } return config; } +- (BPConfiguration *)generateConfig { + return [self generateConfigWithVideoDir: nil]; +} + - (void)setUp { [super setUp]; // Put setup code here. This method is called before the invocation of each test method in the class. @@ -215,4 +222,62 @@ - (void)writeTestPlan { [jsonData writeToFile:[BPTestHelper testPlanPath] atomically:YES]; } +- (void)testOutputFiles { + NSString *path = [BPUtils mkdtemp:@"bpout" withError:nil]; + NSString *testFilePath = [path stringByAppendingPathComponent:@"test.txt"]; + [[NSFileManager defaultManager] createFileAtPath:testFilePath contents:[@"datadata" dataUsingEncoding:NSUTF8StringEncoding] attributes:nil]; + + NSArray *directoryContent = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:nil]; + XCTAssertNotNil(directoryContent); + XCTAssertEqual(directoryContent.count, 1); + + NSSet *filenameSet = [NSSet setWithArray: directoryContent]; + XCTAssertEqual(filenameSet.count, 1); + BOOL hasFile = [filenameSet containsObject: @"test.txt"]; + XCTAssertTrue(hasFile); +} + +- (void)testTwoBPInstancesWithVideo { + NSError *mkdtempError; + NSString *path = [BPUtils mkdtemp:@"bpout" withError:&mkdtempError]; + XCTAssertNil(mkdtempError); + + NSString* videoDirName = @"my_videos"; + NSString *videoPath = [path stringByAppendingPathComponent:videoDirName]; + BPConfiguration *config = [self generateConfigWithVideoDir:videoPath]; + config.numSims = @2; + config.errorRetriesCount = @1; + config.failureTolerance = @0; + // This looks backwards but we want the main app to be the runner + // and the sampleApp is launched from the callback. + config.testBundlePath = [BPTestHelper sampleAppUITestBundlePath]; + config.testRunnerAppPath = [BPTestHelper sampleAppPath]; + config.appBundlePath = [BPTestHelper sampleAppUITestRunnerPath]; + + NSError *err; + BPApp *app = [BPApp appWithConfig:config + withError:&err]; + + NSString *bpPath = [BPTestHelper bpExecutablePath]; + BPRunner *runner = [BPRunner BPRunnerWithConfig:config withBpPath:bpPath]; + XCTAssert(runner != nil); + int rc = [runner runWithBPXCTestFiles:app.testBundles]; + XCTAssert(rc == 0); + XCTAssert([runner.nsTaskList count] == 0); + + NSError *dirContentsError; + NSArray *directoryContent = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:videoPath error:&dirContentsError]; + XCTAssertNil(dirContentsError); + XCTAssertNotNil(directoryContent); + XCTAssertEqual(directoryContent.count, 2); + + NSString *testClass = @"BPSampleAppUITests"; + NSSet *filenameSet = [NSSet setWithArray: directoryContent]; + XCTAssertEqual(filenameSet.count, 2); + BOOL hasTest1 = [filenameSet containsObject: [NSString stringWithFormat:@"%@__%@__1.mp4", testClass, @"testExample"]]; + XCTAssertTrue(hasTest1); + BOOL hasTest2 = [filenameSet containsObject: [NSString stringWithFormat:@"%@__%@__1.mp4", testClass, @"testExample2"]]; + XCTAssertTrue(hasTest2); +} + @end diff --git a/bp/src/BPConfiguration.h b/bp/src/BPConfiguration.h index ac8cf6ae..9fb2cbca 100644 --- a/bp/src/BPConfiguration.h +++ b/bp/src/BPConfiguration.h @@ -86,6 +86,7 @@ typedef NS_ENUM(NSInteger, BPProgram) { @property (nonatomic, strong) NSString *outputDirectory; @property (nonatomic, strong) NSString *testTimeEstimatesJsonFile; @property (nonatomic, strong) NSString *screenshotsDirectory; +@property (nonatomic, strong) NSString *videosDirectory; @property (nonatomic, strong) NSString *simulatorPreferencesFile; @property (nonatomic, strong) NSString *scriptFilePath; @property (nonatomic) BOOL headlessMode; diff --git a/bp/src/BPConfiguration.m b/bp/src/BPConfiguration.m index 88eb702d..cce9e60e 100644 --- a/bp/src/BPConfiguration.m +++ b/bp/src/BPConfiguration.m @@ -143,8 +143,9 @@ typedef NS_OPTIONS(NSUInteger, BPOptionType) { {364, "test-plan-path", BP_MASTER | BP_SLAVE, NO, NO, required_argument, NULL, BP_VALUE | BP_PATH, "testPlanPath", "The path of a json file which describes the test plan. It is equivalent to the .xctestrun file generated by Xcode, but it can be generated by a different build system, e.g. Bazel"}, {365, "unsafe-skip-xcode-version-check", BP_MASTER | BP_SLAVE, NO, NO, no_argument, "Off", BP_VALUE | BP_BOOL , "unsafeSkipXcodeVersionCheck", - " "}, - + " "}, + {366, "videos-directory", BP_MASTER | BP_SLAVE, NO, NO, required_argument, NULL, BP_VALUE | BP_PATH, "videosDirectory", + "Directory where videos of test runs will be saved. If not provided, videos are not recorded."}, {0, 0, 0, 0, 0, 0, 0} }; @@ -772,6 +773,23 @@ - (BOOL)validateConfigWithError:(NSError *__autoreleasing *)errPtr { } } + if (self.videosDirectory) { + if ([[NSFileManager defaultManager] fileExistsAtPath:self.videosDirectory isDirectory:&isdir]) { + if (!isdir) { + BP_SET_ERROR(errPtr, @"%@ is not a directory.", self.videosDirectory); + return NO; + } + } else { + // create the directory + if (![[NSFileManager defaultManager] createDirectoryAtPath:self.videosDirectory + withIntermediateDirectories:YES + attributes:nil + error:errPtr]) { + return NO; + } + } + } + if (self.simulatorPreferencesFile) { if ([[NSFileManager defaultManager] fileExistsAtPath:self.simulatorPreferencesFile isDirectory:&isdir]) { if (isdir) { diff --git a/bp/src/BPTestBundleConnection.h b/bp/src/BPTestBundleConnection.h index 7169c3bd..5bca100a 100644 --- a/bp/src/BPTestBundleConnection.h +++ b/bp/src/BPTestBundleConnection.h @@ -7,6 +7,7 @@ // #import +#import "BPExecutionContext.h" #import "BPSimulator.h" // This is a small subset of XCTestManager_IDEInterface protocol @@ -16,9 +17,10 @@ @interface BPTestBundleConnection : NSObject @property (nonatomic, strong) BPConfiguration *config; +@property (nonatomic, strong) BPExecutionContext *context; @property (nonatomic, strong) BPSimulator *simulator; @property (nonatomic, copy) void (^completionBlock)(NSError *, pid_t); -- (instancetype)initWithDevice:(BPSimulator *)device andInterface:(id)interface; +- (instancetype)initWithContext:(BPExecutionContext *)context andInterface:(id)interface; - (void)connectWithTimeout:(NSTimeInterval)timeout; - (void)startTestPlan; @end diff --git a/bp/src/BPTestBundleConnection.m b/bp/src/BPTestBundleConnection.m index b32ca80e..77b453b0 100644 --- a/bp/src/BPTestBundleConnection.m +++ b/bp/src/BPTestBundleConnection.m @@ -49,15 +49,19 @@ @interface BPTestBundleConnection() @property (nonatomic, strong) dispatch_queue_t queue; @property (nonatomic, strong) NSString *bundleID; @property (nonatomic, assign) pid_t appProcessPID; +@property (nonatomic, nullable) NSTask *recordVideoTask; +@property (nonatomic, nullable) NSPipe *recordVideoPipe; + @end @implementation BPTestBundleConnection -- (instancetype)initWithDevice:(BPSimulator *)simulator andInterface:(id)interface { +- (instancetype)initWithContext:(BPExecutionContext *)context andInterface:(id)interface { self = [super init]; if (self) { - self.simulator = simulator; + self.context = context; + self.simulator = context.runner; self.interface = interface; self.queue = dispatch_queue_create("com.linkedin.bluepill.connection.queue", DISPATCH_QUEUE_PRIORITY_DEFAULT); } @@ -212,6 +216,71 @@ - (id)_XCT_initializationForUITestingDidFailWithError:(NSError *__strong)errPtr return nil; } +#pragma mark - Video Recording + +static inline NSString* getVideoPath(NSString *directory, NSString *testClass, NSString *method, NSInteger attemptNumber) +{ + return [NSString stringWithFormat:@"%@/%@__%@__%ld.mp4", directory, testClass, method, (long)attemptNumber]; +} + +- (BOOL)shouldRecordVideo { + return self.context.config.videosDirectory.length > 0; +} + +- (void)startVideoRecordingForTestClass:(NSString *)testClass method:(NSString *)method +{ + [self stopVideoRecording:YES]; + NSString *videoFileName = getVideoPath(self.context.config.videosDirectory, testClass, method, self.context.attemptNumber); + NSString *command = [NSString stringWithFormat:@"xcrun simctl io %@ recordVideo --codec=h264 --force %@", [self.simulator UDID], videoFileName]; + self.recordVideoPipe = [[NSPipe alloc] init]; + NSTask *task = [BPUtils buildShellTaskForCommand:command withPipe:self.recordVideoPipe]; + self.recordVideoTask = task; + [task launch]; + [BPUtils printInfo:INFO withString:@"Started recording video to %@", videoFileName]; + [BPUtils printInfo:DEBUGINFO withString:@"Started recording video task with pid %d and command: %@", [task processIdentifier], [BPUtils getCommandStringForTask:task]]; +} + +- (void)stopVideoRecording:(BOOL)forced +{ + NSTask *task = self.recordVideoTask; + if (task == nil) { + if (!forced) { + [BPUtils printInfo:ERROR withString: @"Tried to end video recording task normally, but there was no task."]; + } + return; + } + + if (forced) { + [BPUtils printInfo:ERROR withString: @"Found dangling video recording task. Stopping it."]; + } + + if (![task isRunning]) { + [BPUtils printInfo:ERROR withString:@"Video task exists but it was already terminated with status %d", [task terminationStatus]]; + } + + [BPUtils printInfo:INFO withString:@"Stopping recording video."]; + [BPUtils printInfo:DEBUGINFO withString:@"Stopping video recording task with pid %d and command: %@", [task processIdentifier], [BPUtils getCommandStringForTask:task]]; + [task interrupt]; + [task waitUntilExit]; + + if ([task terminationStatus] != 0) { + [BPUtils printInfo:ERROR withString:@"Video task was interrupted, but exited with non-zero status %d", [task terminationStatus]]; + } + + NSString *filePath = [[task arguments].lastObject componentsSeparatedByString:@" "].lastObject; + if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) { + [BPUtils printInfo:ERROR withString:@"Video recording file missing, expected at path %@!", filePath]; + } + NSFileHandle *fh = self.recordVideoPipe.fileHandleForReading; + NSData *data = [fh readDataToEndOfFile]; + NSString *result = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]; + if (result) { + [BPUtils printInfo:INFO withString:@"Video task output: %@", result]; + } + self.recordVideoPipe = nil; + self.recordVideoTask = nil; +} + #pragma mark - XCTestManager_IDEInterface protocol #pragma mark Process Launch Delegation @@ -277,6 +346,9 @@ - (id)_XCT_testSuite:(NSString *)tests didStartAt:(NSString *)time { - (id)_XCT_testCaseDidStartForTestClass:(NSString *)testClass method:(NSString *)method { [BPUtils printInfo:DEBUGINFO withString:@"BPTestBundleConnection_XCT_testCaseDidStartForTestClass: %@ and method: %@", testClass, method]; + if ([self shouldRecordVideo]) { + [self startVideoRecordingForTestClass:testClass method:method]; + } return nil; } @@ -298,12 +370,18 @@ - (id)_XCT_logMessage:(NSString *)message { - (id)_XCT_testCaseDidFinishForTestClass:(NSString *)testClass method:(NSString *)method withStatus:(NSString *)statusString duration:(NSNumber *)duration { [BPUtils printInfo:DEBUGINFO withString: @"BPTestBundleConnection_XCT_testCaseDidFinishForTestClass: %@, method: %@, withStatus: %@, duration: %@", testClass, method, statusString, duration]; + if ([self shouldRecordVideo]) { + [self stopVideoRecording:NO]; + } return nil; } - (id)_XCT_testSuite:(NSString *)arg1 didFinishAt:(NSString *)time runCount:(NSNumber *)count withFailures:(NSNumber *)failureCount unexpected:(NSNumber *)unexpectedCount testDuration:(NSNumber *)testDuration totalDuration:(NSNumber *)totalTime { [BPUtils printInfo:DEBUGINFO withString: @"BPTestBundleConnection_XCT_testSuite: %@, didFinishAt: %@, runCount: %@, withFailures: %@, unexpectedCount: %@, testDuration: %@, totalDuration: %@", arg1, time, count, failureCount, unexpectedCount, testDuration, totalTime]; + if ([self shouldRecordVideo]) { + [self stopVideoRecording:YES]; + } return nil; } diff --git a/bp/src/BPUtils.h b/bp/src/BPUtils.h index f80b3342..751b86df 100644 --- a/bp/src/BPUtils.h +++ b/bp/src/BPUtils.h @@ -125,6 +125,30 @@ typedef NS_ENUM(int, BPKind) { * @return return the shell output */ + (NSString *)runShell:(NSString *)command; + +/*! + * @discussion builds a task to run a shell command + * @param command the shell command the task should run + * @return an NSTask that will run the provided command. + */ ++ (NSTask *)buildShellTaskForCommand:(NSString *)command; + +/*! + * @discussion builds a task to run a shell command, pointing stdout and stderr to the provided pipe + * @param command the shell command the task should run + * @param pipe the pipe that stdout and stderr will be pointed to, so the caller can handle the output. + * @return an NSTask that will run the provided command. + */ ++ (NSTask *)buildShellTaskForCommand:(NSString *)command withPipe:(NSPipe *)pipe; + ++ (void)pointPipeToToPrint:(NSPipe *)pipe; +/*! + * @discussion builds a user readable representation of the command that a task is configured to run + * @param task to get command from + * @return a user readable string of the task's command +*/ ++ (NSString *)getCommandStringForTask:(NSTask *)task; + + (NSString *)getXcodeRuntimeVersion; typedef BOOL (^BPRunBlock)(void); diff --git a/bp/src/BPUtils.m b/bp/src/BPUtils.m index 10fd8c47..c9fbf957 100644 --- a/bp/src/BPUtils.m +++ b/bp/src/BPUtils.m @@ -189,29 +189,40 @@ + (BOOL)isStdOut:(NSString *)fileName { + (NSString *)runShell:(NSString *)command { NSAssert(command, @"Command should not be nil"); - NSTask *task = [[NSTask alloc] init]; - NSData *data; - task.launchPath = @"/bin/sh"; - task.arguments = @[@"-c", command]; NSPipe *pipe = [[NSPipe alloc] init]; - task.standardError = pipe; - task.standardOutput = pipe; + NSTask *task = [BPUtils buildShellTaskForCommand:command withPipe:pipe]; + NSAssert(task, @"task should not be nil"); NSFileHandle *fh = pipe.fileHandleForReading; - if (task) { - [task launch]; - } else { - NSAssert(task, @"task should not be nil"); - } - if (fh) { - data = [fh readDataToEndOfFile]; - } else { - NSAssert(task, @"fh should not be nil"); - } + NSAssert(fh, @"fh should not be nil"); + + [task launch]; + NSData *data = [fh readDataToEndOfFile]; [task waitUntilExit]; NSString *result = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]; return result; } ++ (NSTask *)buildShellTaskForCommand:(NSString *)command { + return [BPUtils buildShellTaskForCommand:command withPipe: nil]; +} + ++ (NSTask *)buildShellTaskForCommand:(NSString *)command withPipe:(NSPipe *)pipe { + NSAssert(command, @"Command should not be nil"); + NSTask *task = [[NSTask alloc] init]; + task.launchPath = @"/bin/sh"; + task.arguments = @[@"-c", command]; + if (pipe != nil) { + task.standardError = pipe; + task.standardOutput = pipe; + } + NSAssert(task, @"task should not be nil"); + return task; +} + ++ (NSString *)getCommandStringForTask:(NSTask *)task { + return [NSString stringWithFormat:@"%@ %@", [task launchPath], [[task arguments] componentsJoinedByString:@" "]]; +} + + (BOOL)runWithTimeOut:(NSTimeInterval)timeout until:(BPRunBlock)block { if (!block) { return NO; diff --git a/bp/src/Bluepill.m b/bp/src/Bluepill.m index 9d466058..a10eefc3 100644 --- a/bp/src/Bluepill.m +++ b/bp/src/Bluepill.m @@ -431,7 +431,7 @@ - (void)connectTestBundleAndTestDaemonWithContext:(BPExecutionContext *)context // If the isTestRunnerContext is flipped on, don't connect testbundle again. return; } - BPTestBundleConnection *bConnection = [[BPTestBundleConnection alloc] initWithDevice:context.runner andInterface:self]; + BPTestBundleConnection *bConnection = [[BPTestBundleConnection alloc] initWithContext:context andInterface:self]; bConnection.simulator = context.runner; bConnection.config = self.config; diff --git a/bp/src/SimulatorMonitor.m b/bp/src/SimulatorMonitor.m index 06cf2938..91af3a74 100644 --- a/bp/src/SimulatorMonitor.m +++ b/bp/src/SimulatorMonitor.m @@ -247,8 +247,8 @@ - (void)stopTestsWithErrorMessage:(NSString *)message forTestName:(NSString *)te NSString *sampleLogFile = [NSString stringWithFormat:@"%@/sampleLog_PID_%d_%@.txt", self.config.outputDirectory, self.appPID, [dateFormatter stringFromDate:[NSDate date]]]; [BPUtils printInfo:INFO withString:@"saving 'sample' command log to: %@", sampleLogFile]; [BPUtils runShell:[NSString stringWithFormat:@"/usr/bin/sample %d -file %@", self.appPID, sampleLogFile]]; - if ((kill(self.appPID, 0) == 0) && (kill(self.appPID, SIGKILL) < 0)) { - [BPUtils printInfo:ERROR withString:@"Failed to kill the process with appPID: %d: %s", + if ((kill(self.appPID, 0) == 0) && (kill(self.appPID, SIGTERM) < 0)) { + [BPUtils printInfo:ERROR withString:@"Failed to terminate the process with appPID: %d: %s", self.appPID, strerror(errno)]; } } diff --git a/bp/tests/BPUtilsTests.m b/bp/tests/BPUtilsTests.m index 566c5691..9b0adf33 100644 --- a/bp/tests/BPUtilsTests.m +++ b/bp/tests/BPUtilsTests.m @@ -148,4 +148,27 @@ - (void) testExitStatus { XCTAssert([[BPExitStatusHelper stringFromExitStatus: exitCode] isEqualToString:@"BPExitStatusSimulatorCreationFailed UNKNOWN_BPEXITSTATUS - 2048"]); } +- (void) testBuildShellTaskForCommand_withoutPipe { + NSString *command = @"ls -al"; + NSTask *task = [BPUtils buildShellTaskForCommand:command]; + XCTAssertEqual(task.launchPath, @"/bin/sh"); + XCTAssertEqual(task.arguments.count, 2); + XCTAssertEqual(task.arguments[0], @"-c"); + XCTAssertEqual(task.arguments[1], command); + XCTAssertFalse(task.isRunning); +} + +- (void) testBuildShellTaskForCommand_withPipe { + NSString *command = @"ls -al"; + NSPipe *pipe = [[NSPipe alloc] init]; + NSTask *task = [BPUtils buildShellTaskForCommand:command withPipe: pipe]; + XCTAssertEqual(task.standardError, pipe); + XCTAssertEqual(task.standardOutput, pipe); + XCTAssertEqual(task.launchPath, @"/bin/sh"); + XCTAssertEqual(task.arguments.count, 2); + XCTAssertEqual(task.arguments[0], @"-c"); + XCTAssertEqual(task.arguments[1], command); + XCTAssertFalse(task.isRunning); +} + @end