Skip to content

Commit aa08ef0

Browse files
committed
[xctool] parallelize run-tests
1 parent b3827c5 commit aa08ef0

File tree

3 files changed

+155
-73
lines changed

3 files changed

+155
-73
lines changed

xctool/xctool/RunTestsAction.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
@property (nonatomic, assign) BOOL freshSimulator;
2222
@property (nonatomic, assign) BOOL freshInstall;
23+
@property (nonatomic, assign) BOOL parallelize;
2324
@property (nonatomic, retain) NSString *testSDK;
2425
@property (nonatomic, retain) NSMutableArray *onlyList;
2526

xctool/xctool/RunTestsAction.m

Lines changed: 145 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#import "RunTestsAction.h"
1818

19+
#import "BufferedReporter.h"
1920
#import "OCUnitIOSAppTestRunner.h"
2021
#import "OCUnitIOSLogicTestRunner.h"
2122
#import "OCUnitOSXAppTestRunner.h"
@@ -26,7 +27,9 @@
2627
#import "XcodeSubjectInfo.h"
2728
#import "XCToolUtil.h"
2829

29-
@implementation RunTestsAction
30+
@implementation RunTestsAction {
31+
NSLock *_appTestLock;
32+
}
3033

3134
+ (NSString *)name
3235
{
@@ -57,20 +60,26 @@ + (NSArray *)options
5760
description:
5861
@"Use clean install of TEST_HOST for every app test run"
5962
setFlag:@selector(setFreshInstall:)],
63+
[Action actionOptionWithName:@"parallelize"
64+
aliases:nil
65+
description:@"Parallelize execution of logic tests"
66+
setFlag:@selector(setParallelize:)],
6067
];
6168
}
6269

6370
- (id)init
6471
{
6572
if (self = [super init]) {
6673
self.onlyList = [NSMutableArray array];
74+
self->_appTestLock = [[NSLock alloc] init];
6775
}
6876
return self;
6977
}
7078

7179
- (void)dealloc {
7280
self.onlyList = nil;
7381
self.testSDK = nil;
82+
[self->_appTestLock release];
7483
[super dealloc];
7584
}
7685

@@ -220,17 +229,26 @@ - (NSDictionary *)enviornmentWithMacrosExpanded:(NSDictionary *)dict
220229
return result;
221230
}
222231

223-
- (BOOL)runTestable:(NSDictionary *)testable
224-
reporters:(NSArray *)reporters
225-
objRoot:(NSString *)objRoot
226-
symRoot:(NSString *)symRoot
227-
sharedPrecompsDir:(NSString *)sharedPrecompsDir
228-
xcodeArguments:(NSArray *)xcodeArguments
229-
testSDK:(NSString *)testSDK
230-
freshSimulator:(BOOL)freshSimulator
231-
freshInstall:(BOOL)freshInstall
232-
senTestList:(NSString *)senTestList
233-
senTestInvertScope:(BOOL)senTestInvertScope
232+
static NSString* const kTestableBlock = @"block";
233+
static NSString* const kTestableMustRunInMainThread = @"mustRunInMainThread";
234+
235+
/*!
236+
Retrieves build params of the test and prepares a block that will run the test.
237+
238+
@return an NSDicitonary of the block that runs the test, and whether the test
239+
must be run on the main thread
240+
*/
241+
- (NSDictionary *) blockForTestable:(NSDictionary *)testable
242+
reporters:(NSArray *)reporters
243+
objRoot:(NSString *)objRoot
244+
symRoot:(NSString *)symRoot
245+
sharedPrecompsDir:(NSString *)sharedPrecompsDir
246+
xcodeArguments:(NSArray *)xcodeArguments
247+
testSDK:(NSString *)testSDK
248+
freshSimulator:(BOOL)freshSimulator
249+
freshInstall:(BOOL)freshInstall
250+
senTestList:(NSString *)senTestList
251+
senTestInvertScope:(BOOL)senTestInvertScope
234252
{
235253
NSString *testableProjectPath = testable[@"projectPath"];
236254
NSString *testableTarget = testable[@"target"];
@@ -305,54 +323,65 @@ - (BOOL)runTestable:(NSDictionary *)testable
305323
NSAssert(NO, @"Unexpected SDK: %@", sdkName);
306324
}
307325

308-
BOOL succeeded = YES;
309-
310-
for (NSArray *testConfiguration in testConfigurations) {
311-
Class testRunnerClass = testConfiguration[0];
312-
BOOL garbageCollectionEnabled = [testConfiguration[1] boolValue];
313-
314-
OCUnitTestRunner *testRunner = [[[testRunnerClass alloc]
315-
initWithBuildSettings:testableBuildSettings
316-
senTestList:senTestList
317-
senTestInvertScope:senTestInvertScope
318-
arguments:arguments
319-
environment:environment
320-
garbageCollection:garbageCollectionEnabled
321-
freshSimulator:freshSimulator
322-
freshInstall:freshInstall
323-
standardOutput:nil
324-
standardError:nil
325-
reporters:reporters] autorelease];
326-
327-
NSDictionary *commonEventInfo = @{kReporter_BeginOCUnit_BundleNameKey: testableBuildSettings[@"FULL_PRODUCT_NAME"],
328-
kReporter_BeginOCUnit_SDKNameKey: testableBuildSettings[@"SDK_NAME"],
329-
kReporter_BeginOCUnit_TestTypeKey: isApplicationTest ? @"application-test" : @"logic-test",
330-
kReporter_BeginOCUnit_GCEnabledKey: @(garbageCollectionEnabled),
331-
};
332-
333-
NSMutableDictionary *beginEvent = [NSMutableDictionary dictionaryWithDictionary:@{
334-
@"event": kReporter_Events_BeginOCUnit,
335-
}];
336-
[beginEvent addEntriesFromDictionary:commonEventInfo];
337-
[reporters makeObjectsPerformSelector:@selector(handleEvent:) withObject:beginEvent];
326+
BOOL (^action)() = ^() {
327+
BOOL succeeded = YES;
328+
329+
for (NSArray *testConfiguration in testConfigurations) {
330+
Class testRunnerClass = testConfiguration[0];
331+
BOOL garbageCollectionEnabled = [testConfiguration[1] boolValue];
332+
333+
OCUnitTestRunner *testRunner = [[[testRunnerClass alloc]
334+
initWithBuildSettings:testableBuildSettings
335+
senTestList:senTestList
336+
senTestInvertScope:senTestInvertScope
337+
arguments:arguments
338+
environment:environment
339+
garbageCollection:garbageCollectionEnabled
340+
freshSimulator:freshSimulator
341+
freshInstall:freshInstall
342+
standardOutput:nil
343+
standardError:nil
344+
reporters:reporters] autorelease];
345+
346+
NSDictionary *commonEventInfo = @{kReporter_BeginOCUnit_BundleNameKey: testableBuildSettings[@"FULL_PRODUCT_NAME"],
347+
kReporter_BeginOCUnit_SDKNameKey: testableBuildSettings[@"SDK_NAME"],
348+
kReporter_BeginOCUnit_TestTypeKey: isApplicationTest ? @"application-test" : @"logic-test",
349+
kReporter_BeginOCUnit_GCEnabledKey: @(garbageCollectionEnabled),
350+
};
351+
352+
NSMutableDictionary *beginEvent = [NSMutableDictionary dictionaryWithDictionary:@{
353+
@"event": kReporter_Events_BeginOCUnit,
354+
}];
355+
[beginEvent addEntriesFromDictionary:commonEventInfo];
356+
[reporters makeObjectsPerformSelector:@selector(handleEvent:) withObject:beginEvent];
357+
358+
NSString *error = nil;
359+
BOOL configurationSucceeded = [testRunner runTestsWithError:&error];
360+
361+
if (!configurationSucceeded) {
362+
succeeded = NO;
363+
}
338364

339-
NSString *error = nil;
340-
BOOL configurationSucceeded = [testRunner runTestsWithError:&error];
365+
NSMutableDictionary *endEvent = [NSMutableDictionary dictionaryWithDictionary:@{
366+
@"event": kReporter_Events_EndOCUnit,
367+
kReporter_EndOCUnit_SucceededKey: @(succeeded),
368+
kReporter_EndOCUnit_FailureReasonKey: (error ? error : [NSNull null]),
369+
}];
370+
[endEvent addEntriesFromDictionary:commonEventInfo];
371+
[reporters makeObjectsPerformSelector:@selector(handleEvent:) withObject:endEvent];
372+
}
341373

342-
if (!configurationSucceeded) {
343-
succeeded = NO;
374+
for (id reporter in reporters) {
375+
if ([reporter respondsToSelector:@selector(flush)]) {
376+
[reporter flush];
377+
}
344378
}
345379

346-
NSMutableDictionary *endEvent = [NSMutableDictionary dictionaryWithDictionary:@{
347-
@"event": kReporter_Events_EndOCUnit,
348-
kReporter_EndOCUnit_SucceededKey: @(succeeded),
349-
kReporter_EndOCUnit_FailureReasonKey: (error ? error : [NSNull null]),
350-
}];
351-
[endEvent addEntriesFromDictionary:commonEventInfo];
352-
[reporters makeObjectsPerformSelector:@selector(handleEvent:) withObject:endEvent];
353-
}
380+
return succeeded;
381+
};
354382

355-
return succeeded;
383+
return @{kTestableBlock: [[action copy] autorelease],
384+
kTestableMustRunInMainThread: @(isApplicationTest)};
356385
}
357386

358387
- (BOOL)runTestables:(NSArray *)testables
@@ -362,24 +391,67 @@ - (BOOL)runTestables:(NSArray *)testables
362391
options:(Options *)options
363392
xcodeSubjectInfo:(XcodeSubjectInfo *)xcodeSubjectInfo
364393
{
365-
BOOL succeeded = YES;
366-
367-
for (NSDictionary *testable in testables) {
368-
BOOL senTestInvertScope = [testable[@"senTestInvertScope"] boolValue];
369-
NSString *senTestList = testable[@"senTestList"];
370-
371-
if (![self runTestable:testable
372-
reporters:options.reporters
373-
objRoot:xcodeSubjectInfo.objRoot
374-
symRoot:xcodeSubjectInfo.symRoot
375-
sharedPrecompsDir:xcodeSubjectInfo.sharedPrecompsDir
376-
xcodeArguments:[options commonXcodeBuildArgumentsIncludingSDK:NO]
377-
testSDK:testSDK
378-
freshSimulator:freshSimulator
379-
freshInstall:freshInstall
380-
senTestList:senTestList
381-
senTestInvertScope:senTestInvertScope]) {
382-
succeeded = NO;
394+
NSObject *succeededLock = [[[NSObject alloc] init] autorelease];
395+
__block BOOL succeeded = YES;
396+
397+
@autoreleasepool {
398+
NSMutableArray *blocksToRunInMainThread = [NSMutableArray array];
399+
NSOperationQueue *operationQueue = [[[NSOperationQueue alloc] init] autorelease];
400+
if (!self.parallelize) {
401+
operationQueue.maxConcurrentOperationCount = 1;
402+
}
403+
404+
for (NSDictionary *testable in testables) {
405+
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
406+
BOOL senTestInvertScope = [testable[@"senTestInvertScope"] boolValue];
407+
NSString *senTestList = testable[@"senTestList"];
408+
409+
NSArray *reporters = options.reporters;
410+
if (self.parallelize) {
411+
// parallel execution will cause reporters to be buffered and flushed
412+
// atomically at end of test to prevent interleaved output.
413+
NSMutableArray *bufferedReporters = [NSMutableArray arrayWithCapacity:reporters.count];
414+
for (Reporter *reporter in reporters) {
415+
[bufferedReporters addObject:[BufferedReporter bufferedReporterWithReporter:reporter]];
416+
}
417+
reporters = bufferedReporters;
418+
}
419+
420+
NSDictionary *action = [self blockForTestable:testable
421+
reporters:reporters
422+
objRoot:xcodeSubjectInfo.objRoot
423+
symRoot:xcodeSubjectInfo.symRoot
424+
sharedPrecompsDir:xcodeSubjectInfo.sharedPrecompsDir
425+
xcodeArguments:[options commonXcodeBuildArgumentsIncludingSDK:NO]
426+
testSDK:testSDK
427+
freshSimulator:freshSimulator
428+
freshInstall:freshInstall
429+
senTestList:senTestList
430+
senTestInvertScope:senTestInvertScope];
431+
432+
BOOL (^block)() = action[kTestableBlock];
433+
if ([action[kTestableMustRunInMainThread] boolValue]) {
434+
@synchronized(blocksToRunInMainThread) {
435+
[blocksToRunInMainThread addObject:block];
436+
}
437+
} else {
438+
if (!block()) {
439+
@synchronized(succeededLock) {
440+
succeeded = NO;
441+
}
442+
}
443+
}
444+
}];
445+
446+
[operationQueue addOperation:operation];
447+
}
448+
449+
[operationQueue waitUntilAllOperationsAreFinished];
450+
451+
for (BOOL (^block)() in blocksToRunInMainThread) {
452+
if (!block()) {
453+
succeeded = NO;
454+
}
383455
}
384456
}
385457

xctool/xctool/TestAction.m

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ + (NSArray *)options
6262
description:
6363
@"Use clean install of TEST_HOST for every app test run"
6464
setFlag:@selector(setFreshInstall:)],
65+
[Action actionOptionWithName:@"parallelize"
66+
aliases:nil
67+
description:@"Parallelize execution of logic tests"
68+
setFlag:@selector(setParallelize:)],
6569
];
6670
}
6771

@@ -95,6 +99,11 @@ - (void)setFreshInstall:(BOOL)freshInstall
9599
[_runTestsAction setFreshInstall:freshInstall];
96100
}
97101

102+
- (void)setParallelize:(BOOL)parallelize
103+
{
104+
[_runTestsAction setParallelize:parallelize];
105+
}
106+
98107
- (void)setSkipDependencies:(BOOL)skipDependencies
99108
{
100109
_buildTestsAction.skipDependencies = skipDependencies;

0 commit comments

Comments
 (0)