xcknife 0.6.4 → 0.11.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +2 -1
- data/.ruby-version +1 -1
- data/.travis.yml +1 -5
- data/Gemfile.lock +35 -0
- data/OWNERS.yml +2 -0
- data/README.md +12 -3
- data/Rakefile +2 -2
- data/TestDumper/README.md +2 -1
- data/TestDumper/TestDumper.xcodeproj/project.pbxproj +27 -5
- data/TestDumper/TestDumper.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- data/TestDumper/TestDumper.xcodeproj/xcshareddata/xcschemes/TestDumper.xcscheme +1 -1
- data/TestDumper/TestDumper/Initialize.m +83 -44
- data/example/run_example.rb +1 -0
- data/lib/xcknife.rb +1 -1
- data/lib/xcknife/exceptions.rb +4 -2
- data/lib/xcknife/stream_parser.rb +53 -18
- data/lib/xcknife/test_dumper.rb +182 -45
- data/lib/xcknife/xctool_cmd_helper.rb +29 -4
- data/xcknife.gemspec +0 -3
- metadata +7 -7
- data/.gitmodules +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e63c27c7af4820f72ed2ef5813306be1dc4d6793fc837bca402d53ac53eff7ff
|
4
|
+
data.tar.gz: 180ebe00183782dbb1b0b63cdf23ce010ffe31a6cc910b921ff031f32f97d462
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cb9824f69aef912ab664c29c56ef5b9f00a58765b3361f109917bfd0374d4ac003c3420f76e20f27b820d71a516e07d676bc0718d171cc1c0c1d9849553d9421
|
7
|
+
data.tar.gz: 50c7119def1b9d0334fcd55677838b346f7ba1971b6cf949fd664190494628a7799aacf1cc45b9977bc099e0675955a29bfdf571059456b309f86a7c0e4bdb7b
|
data/.gitignore
CHANGED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.6.1
|
data/.travis.yml
CHANGED
data/Gemfile.lock
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
xcknife (0.11.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.3)
|
10
|
+
rake (11.1.2)
|
11
|
+
rspec (3.4.0)
|
12
|
+
rspec-core (~> 3.4.0)
|
13
|
+
rspec-expectations (~> 3.4.0)
|
14
|
+
rspec-mocks (~> 3.4.0)
|
15
|
+
rspec-core (3.4.4)
|
16
|
+
rspec-support (~> 3.4.0)
|
17
|
+
rspec-expectations (3.4.0)
|
18
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
19
|
+
rspec-support (~> 3.4.0)
|
20
|
+
rspec-mocks (3.4.1)
|
21
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
22
|
+
rspec-support (~> 3.4.0)
|
23
|
+
rspec-support (3.4.1)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
bundler (~> 1.12)
|
30
|
+
rake (~> 11.1.1)
|
31
|
+
rspec (~> 3.4.0)
|
32
|
+
xcknife!
|
33
|
+
|
34
|
+
BUNDLED WITH
|
35
|
+
1.17.3
|
data/OWNERS.yml
ADDED
data/README.md
CHANGED
@@ -9,7 +9,7 @@ It works by leveraging [xctool's](https://github.com/facebook/xctool) [json-stre
|
|
9
9
|
|
10
10
|
XCKnife generates a list of only arguments meant to be pass to Xctool's [*-only* test arguments](https://github.com/facebook/xctool#testing), but alternatively could used to generate multiple xcschemes with the proper test partitions.
|
11
11
|
|
12
|
-
More information on XCKnife, go [here](https://
|
12
|
+
More information on XCKnife, go [here](https://developer.squareup.com/blog/xcknife-faster-distributed-tests-for-ios).
|
13
13
|
|
14
14
|
## Install
|
15
15
|
|
@@ -20,9 +20,10 @@ More information on XCKnife, go [here](https://corner.squareup.com/2016/06/xckni
|
|
20
20
|
```
|
21
21
|
$ xcknife --help
|
22
22
|
Usage: xcknife [options] worker-count historical-timings-json-stream-file [current-tests-json-stream-file]
|
23
|
-
-p, --partition TARGETS Comma separated list of targets. Can be used multiple times
|
23
|
+
-p, --partition TARGETS Comma separated list of targets. Can be used multiple times.
|
24
24
|
-o, --output FILENAME Output file. Defaults to STDOUT
|
25
25
|
-a, --abbrev Results are abbreviated
|
26
|
+
-x, --xcodebuild-output Output is formatted for xcodebuild
|
26
27
|
-h, --help Show this message
|
27
28
|
```
|
28
29
|
|
@@ -68,7 +69,7 @@ This will balance the tests onthe `iPhoneTestTarget` into 3 machines. The output
|
|
68
69
|
}]}]}
|
69
70
|
```
|
70
71
|
|
71
|
-
This provides a lot of data about the partitions and their
|
72
|
+
This provides a lot of data about the partitions and their imbalances (both internal to the partition sets, and amongst them).
|
72
73
|
|
73
74
|
If you only want the *-only* arguments, run with the `-a` flag:
|
74
75
|
|
@@ -193,6 +194,14 @@ XCKnife uses only a few attributes of a json-stream file. If you are storing the
|
|
193
194
|
|
194
195
|
`$ xcknife-min example/xcknife-exemplar-historical-data.json-stream minified.json-stream`
|
195
196
|
|
197
|
+
## Dependencies
|
198
|
+
|
199
|
+
XCKnife requires the use of the `gtimeout` command, which is provided in the GNU coreutils brew package. If you don't already have them, they can be installed with the command:
|
200
|
+
|
201
|
+
```
|
202
|
+
brew install coreutils
|
203
|
+
```
|
204
|
+
|
196
205
|
## Contributing
|
197
206
|
|
198
207
|
Any contributors to the master *xcknife* repository must sign the
|
data/Rakefile
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'rspec/core/rake_task'
|
2
|
-
require "bundler/gem_tasks"
|
3
2
|
require 'fileutils'
|
4
3
|
|
5
4
|
RSpec::Core::RakeTask.new(:spec)
|
@@ -19,4 +18,5 @@ end
|
|
19
18
|
desc "Release wih test_dumper"
|
20
19
|
task :gem_release => [:build_test_dumper, :build] do
|
21
20
|
system 'gem push pkg/xcknife-*.gem'
|
22
|
-
end
|
21
|
+
end
|
22
|
+
|
data/TestDumper/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# TestDumper - EXPERIMENTAL
|
2
2
|
|
3
|
-
Utility that replaces xctool for enumerating tests. It requires the `build-for-testing` feature
|
3
|
+
Utility that replaces xctool for enumerating tests. It requires the `build-for-testing` feature Xcode 8 introduced on xcodebuild. In particular, it leverages the xctestrun file (see `man xcodebuild.xctestrun`).
|
4
4
|
|
5
5
|
## Building.
|
6
6
|
|
@@ -17,6 +17,7 @@ $ xcknife-test-dumper --help
|
|
17
17
|
Usage: xcknife-test-dumper [options] derived_data_folder output_file [device_id]
|
18
18
|
-d, --debug Debug mode enabled
|
19
19
|
-r, --retry-count COUNT Max retry count for simulator output
|
20
|
+
-x, --simctl-timeout SECONDS Max allowed time in seconds for simctl commands
|
20
21
|
-t OUTPUT_FOLDER, Sets temporary Output folder
|
21
22
|
--temporary-output
|
22
23
|
-s, --scheme XCSCHEME_FILE Reads environments variables from the xcscheme file
|
@@ -181,7 +181,7 @@
|
|
181
181
|
isa = PBXProject;
|
182
182
|
attributes = {
|
183
183
|
LastSwiftUpdateCheck = 0700;
|
184
|
-
LastUpgradeCheck =
|
184
|
+
LastUpgradeCheck = 1010;
|
185
185
|
ORGANIZATIONNAME = "Square, Inc";
|
186
186
|
TargetAttributes = {
|
187
187
|
F60C68B91B8D038300CC8521 = {
|
@@ -198,7 +198,7 @@
|
|
198
198
|
};
|
199
199
|
};
|
200
200
|
};
|
201
|
-
buildConfigurationList = F60C68B41B8D038300CC8521 /* Build configuration list for PBXProject "
|
201
|
+
buildConfigurationList = F60C68B41B8D038300CC8521 /* Build configuration list for PBXProject "TestDumper" */;
|
202
202
|
compatibilityVersion = "Xcode 3.2";
|
203
203
|
developmentRegion = English;
|
204
204
|
hasScannedForEncodings = 0;
|
@@ -288,13 +288,23 @@
|
|
288
288
|
CLANG_CXX_LIBRARY = "libc++";
|
289
289
|
CLANG_ENABLE_MODULES = YES;
|
290
290
|
CLANG_ENABLE_OBJC_ARC = YES;
|
291
|
+
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
291
292
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
293
|
+
CLANG_WARN_COMMA = YES;
|
292
294
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
295
|
+
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
293
296
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
294
297
|
CLANG_WARN_EMPTY_BODY = YES;
|
295
298
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
299
|
+
CLANG_WARN_INFINITE_RECURSION = YES;
|
296
300
|
CLANG_WARN_INT_CONVERSION = YES;
|
301
|
+
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
302
|
+
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
303
|
+
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
297
304
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
305
|
+
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
306
|
+
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
307
|
+
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
298
308
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
299
309
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
300
310
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
@@ -337,13 +347,23 @@
|
|
337
347
|
CLANG_CXX_LIBRARY = "libc++";
|
338
348
|
CLANG_ENABLE_MODULES = YES;
|
339
349
|
CLANG_ENABLE_OBJC_ARC = YES;
|
350
|
+
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
340
351
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
352
|
+
CLANG_WARN_COMMA = YES;
|
341
353
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
354
|
+
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
342
355
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
343
356
|
CLANG_WARN_EMPTY_BODY = YES;
|
344
357
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
358
|
+
CLANG_WARN_INFINITE_RECURSION = YES;
|
345
359
|
CLANG_WARN_INT_CONVERSION = YES;
|
360
|
+
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
361
|
+
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
362
|
+
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
346
363
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
364
|
+
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
365
|
+
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
366
|
+
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
347
367
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
348
368
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
349
369
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
@@ -373,12 +393,13 @@
|
|
373
393
|
F60C68D11B8D038300CC8521 /* Debug */ = {
|
374
394
|
isa = XCBuildConfiguration;
|
375
395
|
buildSettings = {
|
396
|
+
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
376
397
|
CLANG_ENABLE_MODULES = YES;
|
398
|
+
CODE_SIGN_IDENTITY = "";
|
377
399
|
DEFINES_MODULE = YES;
|
378
400
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
379
401
|
DYLIB_CURRENT_VERSION = 1;
|
380
402
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
381
|
-
EMBEDDED_CONTENT_CONTAINS_SWIFT = YES;
|
382
403
|
FRAMEWORK_SEARCH_PATHS = (
|
383
404
|
"$(inherited)",
|
384
405
|
"$(SDKROOT)/../../Library/Frameworks/",
|
@@ -399,12 +420,13 @@
|
|
399
420
|
F60C68D21B8D038300CC8521 /* Release */ = {
|
400
421
|
isa = XCBuildConfiguration;
|
401
422
|
buildSettings = {
|
423
|
+
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
402
424
|
CLANG_ENABLE_MODULES = YES;
|
425
|
+
CODE_SIGN_IDENTITY = "";
|
403
426
|
DEFINES_MODULE = YES;
|
404
427
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
405
428
|
DYLIB_CURRENT_VERSION = 1;
|
406
429
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
407
|
-
EMBEDDED_CONTENT_CONTAINS_SWIFT = YES;
|
408
430
|
FRAMEWORK_SEARCH_PATHS = (
|
409
431
|
"$(inherited)",
|
410
432
|
"$(SDKROOT)/../../Library/Frameworks/",
|
@@ -483,7 +505,7 @@
|
|
483
505
|
/* End XCBuildConfiguration section */
|
484
506
|
|
485
507
|
/* Begin XCConfigurationList section */
|
486
|
-
F60C68B41B8D038300CC8521 /* Build configuration list for PBXProject "
|
508
|
+
F60C68B41B8D038300CC8521 /* Build configuration list for PBXProject "TestDumper" */ = {
|
487
509
|
isa = XCConfigurationList;
|
488
510
|
buildConfigurations = (
|
489
511
|
F60C68CE1B8D038300CC8521 /* Debug */,
|
@@ -7,13 +7,9 @@
|
|
7
7
|
//
|
8
8
|
|
9
9
|
@import Dispatch;
|
10
|
+
@import Foundation;
|
10
11
|
@import XCTest;
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
@class NSSet, NSString, NSURL, NSUUID;
|
16
|
-
|
17
13
|
@interface XCTestConfiguration : NSObject <NSSecureCoding>
|
18
14
|
{
|
19
15
|
NSURL *_testBundleURL;
|
@@ -71,6 +67,46 @@
|
|
71
67
|
|
72
68
|
#include <dlfcn.h>
|
73
69
|
|
70
|
+
// Logging
|
71
|
+
FILE *logFile;
|
72
|
+
|
73
|
+
void initializeLogFile(const char *logFilePath)
|
74
|
+
{
|
75
|
+
logFile = fopen(logFilePath, "w");
|
76
|
+
}
|
77
|
+
|
78
|
+
void logDebug(NSString *, ...) NS_FORMAT_FUNCTION(1, 2);
|
79
|
+
void logDebug(NSString *msg, ...)
|
80
|
+
{
|
81
|
+
assert(logFile);
|
82
|
+
va_list varargs;
|
83
|
+
va_start(varargs, msg);
|
84
|
+
msg = [[NSString alloc] initWithFormat:msg arguments:varargs];
|
85
|
+
va_end(varargs);
|
86
|
+
fprintf(logFile, "%s\n", msg.UTF8String);
|
87
|
+
|
88
|
+
NSLog(@"%@", msg);
|
89
|
+
}
|
90
|
+
|
91
|
+
void logInit()
|
92
|
+
{
|
93
|
+
logDebug(@"Starting TestDumper...");
|
94
|
+
logDebug(@"Environment Variables:");
|
95
|
+
logDebug(@"%@", NSProcessInfo.processInfo.environment.description);
|
96
|
+
logDebug(@"Command Line Arguments:");
|
97
|
+
logDebug(@"%@", NSProcessInfo.processInfo.arguments.description);
|
98
|
+
|
99
|
+
logDebug(@"--------------------------------");
|
100
|
+
}
|
101
|
+
|
102
|
+
OS_NORETURN
|
103
|
+
void logEnd(Boolean success)
|
104
|
+
{
|
105
|
+
int exitCode = success ? EXIT_SUCCESS : EXIT_FAILURE;
|
106
|
+
logDebug(@"EndingTestDumper...\nExiting with status %d", exitCode);
|
107
|
+
fclose(logFile);
|
108
|
+
exit(exitCode);
|
109
|
+
}
|
74
110
|
|
75
111
|
// Used for a structured log, just like Xctool's.
|
76
112
|
// Example: https://github.com/square/xcknife/blob/master/example/xcknife-exemplar.json-stream
|
@@ -81,10 +117,10 @@ static void PrintJSON(FILE *outFile, id JSONObject)
|
|
81
117
|
|
82
118
|
if (error) {
|
83
119
|
fprintf(outFile, "{ \"message\" : \"Error while serializing to JSON. Check out simulator logs for details\" }");
|
84
|
-
|
120
|
+
logDebug(@"ERROR: Error generating JSON for object: %s: %s\n",
|
85
121
|
[[JSONObject description] UTF8String],
|
86
122
|
[[error localizedFailureReason] UTF8String]);
|
87
|
-
|
123
|
+
logEnd(false);
|
88
124
|
}
|
89
125
|
|
90
126
|
fwrite([data bytes], 1, [data length], outFile);
|
@@ -114,58 +150,60 @@ static void PrintTestClass(FILE *outFile, NSString *testClass) {
|
|
114
150
|
@"totalDuration" : @"0"});
|
115
151
|
}
|
116
152
|
|
117
|
-
void enumerateTests();
|
153
|
+
void enumerateTests(NSString *);
|
118
154
|
|
119
155
|
const int TEST_TARGET_LEVEL = 0;
|
120
156
|
const int TEST_CLASS_LEVEL = 1;
|
121
157
|
const int TEST_METHOD_LEVEL = 2;
|
158
|
+
FILE *noteFile;
|
122
159
|
|
123
160
|
__attribute__((constructor))
|
124
161
|
void initialize() {
|
125
162
|
NSLog(@"Starting TestDumper");
|
163
|
+
const char *logFilePath = [[[NSProcessInfo processInfo] arguments][3] UTF8String];
|
164
|
+
initializeLogFile(logFilePath);
|
165
|
+
logInit();
|
166
|
+
NSString *testBundlePath = [[NSProcessInfo processInfo] arguments][4];
|
126
167
|
NSString *testDumperOutputPath = NSProcessInfo.processInfo.environment[@"TestDumperOutputPath"];
|
127
168
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
128
169
|
|
129
170
|
if ([fileManager fileExistsAtPath:testDumperOutputPath]) {
|
130
|
-
|
131
|
-
|
171
|
+
logDebug(@"File already exists %@. Stopping", testDumperOutputPath);
|
172
|
+
logEnd(true);
|
132
173
|
}
|
133
|
-
NSString *testType = [NSString stringWithUTF8String: getenv("XCTEST_TYPE")];
|
134
174
|
|
175
|
+
NSString *testType = [NSString stringWithUTF8String: getenv("XCTEST_TYPE")];
|
135
176
|
if ([testType isEqualToString: @"APPTEST"]) {
|
136
177
|
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
|
137
|
-
enumerateTests();
|
178
|
+
enumerateTests(testBundlePath);
|
138
179
|
}];
|
139
180
|
} else {
|
140
|
-
enumerateTests();
|
181
|
+
enumerateTests(testBundlePath);
|
141
182
|
}
|
142
183
|
}
|
143
184
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
NSString *testTarget = [NSString stringWithUTF8String: getenv("XCTEST_TARGET")];
|
149
|
-
|
150
|
-
if ([testType isEqualToString: @"APPTEST"]) {
|
151
|
-
config.testBundleURL = [NSURL fileURLWithPath:NSProcessInfo.processInfo.environment[@"XCInjectBundle"]];
|
152
|
-
config.targetApplicationPath = NSProcessInfo.processInfo.environment[@"XCInjectBundleInto"];
|
153
|
-
|
154
|
-
NSString *configPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"%ul.xctestconfiguration", arc4random()]];
|
155
|
-
|
156
|
-
NSLog(@"Writing config to %@", configPath);
|
157
|
-
|
158
|
-
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:config];
|
159
|
-
|
160
|
-
[data writeToFile:configPath atomically:true];
|
161
|
-
|
162
|
-
setenv("XCTestConfigurationFilePath", configPath.UTF8String, YES);
|
163
|
-
|
164
|
-
dlopen(getenv("IDE_INJECTION_PATH"), RTLD_GLOBAL);
|
185
|
+
void enumerateTests(NSString *testBundlePath) {
|
186
|
+
logDebug(@"Listing all test bundles");
|
187
|
+
for (NSBundle *bundle in NSBundle.allBundles) {
|
188
|
+
logDebug(@"Found a test bundle named: %@", bundle.bundlePath);
|
165
189
|
}
|
166
|
-
|
167
|
-
|
168
|
-
|
190
|
+
logDebug(@"Finished listing all test bundles");
|
191
|
+
|
192
|
+
NSBundle* testBundleObj = [NSBundle bundleWithPath:testBundlePath];
|
193
|
+
[testBundleObj load];
|
194
|
+
logDebug(@"test bundle loaded");
|
195
|
+
|
196
|
+
logDebug(@"Listing all test bundles");
|
197
|
+
for (NSBundle *bundle in NSBundle.allBundles) {
|
198
|
+
logDebug(@"Found a test bundle named: %@", bundle.bundlePath);
|
199
|
+
}
|
200
|
+
logDebug(@"Finished listing all test bundles");
|
201
|
+
|
202
|
+
NSString *testType = [NSString stringWithUTF8String: getenv("XCTEST_TYPE")];
|
203
|
+
NSString *testTarget = [[[testBundlePath componentsSeparatedByString:@"/"] lastObject] componentsSeparatedByString:@"."][0];
|
204
|
+
|
205
|
+
logDebug(@"The test target is: %@ of type %@", testTarget, testType);
|
206
|
+
|
169
207
|
FILE *outFile;
|
170
208
|
NSString *testDumperOutputPath = NSProcessInfo.processInfo.environment[@"TestDumperOutputPath"];
|
171
209
|
|
@@ -175,17 +213,18 @@ void enumerateTests() {
|
|
175
213
|
outFile = fopen(testDumperOutputPath.UTF8String, "w+");
|
176
214
|
}
|
177
215
|
|
178
|
-
|
216
|
+
logDebug(@"Opened %@ with fd %p", testDumperOutputPath, outFile);
|
179
217
|
if (outFile == NULL) {
|
180
|
-
|
181
|
-
|
218
|
+
logDebug(@"File already exists at %@. Stopping", testDumperOutputPath);
|
219
|
+
logEnd(true);
|
182
220
|
}
|
183
|
-
|
221
|
+
|
184
222
|
PrintDumpStart(outFile, testType);
|
185
|
-
|
223
|
+
XCTestSuite* testSuite = [XCTestSuite defaultTestSuite];
|
224
|
+
[testSuite printTestsWithLevel:0 withTarget: testTarget withParent: nil outputFile:outFile];
|
186
225
|
PrintDumpEnd(outFile, testType);
|
187
226
|
fclose(outFile);
|
188
|
-
|
227
|
+
logEnd(true);
|
189
228
|
}
|
190
229
|
|
191
230
|
|
@@ -208,7 +247,7 @@ void enumerateTests() {
|
|
208
247
|
// nothing to do here
|
209
248
|
break;
|
210
249
|
default:
|
211
|
-
|
250
|
+
logDebug(@"Unknown test level %ld for test %@", level, t.debugDescription);
|
212
251
|
|
213
252
|
}
|
214
253
|
if (level == TEST_METHOD_LEVEL) {
|
data/example/run_example.rb
CHANGED
@@ -26,6 +26,7 @@ def run(historical_file, current_file)
|
|
26
26
|
partition_set.each do |partition|
|
27
27
|
puts "target name for worker #{shard_number} = #{target_name}"
|
28
28
|
puts "only is: #{xctool_only_arguments(partition).inspect}"
|
29
|
+
puts "skip-only is: #{xcodebuild_skip_arguments(partition, result.test_time_for_partitions).inspect}"
|
29
30
|
shard_number += 1
|
30
31
|
end
|
31
32
|
end
|
data/lib/xcknife.rb
CHANGED
data/lib/xcknife/exceptions.rb
CHANGED
@@ -10,31 +10,40 @@ module XCKnife
|
|
10
10
|
|
11
11
|
attr_reader :number_of_shards, :test_partitions, :stats, :relevant_partitions
|
12
12
|
|
13
|
-
def initialize(number_of_shards, test_partitions)
|
13
|
+
def initialize(number_of_shards, test_partitions, options_for_metapartition: Array.new(test_partitions.size, {}), allow_fewer_shards: false)
|
14
14
|
@number_of_shards = number_of_shards
|
15
15
|
@test_partitions = test_partitions.map(&:to_set)
|
16
16
|
@relevant_partitions = test_partitions.flatten.to_set
|
17
17
|
@stats = ResultStats.new
|
18
|
-
|
18
|
+
@options_for_metapartition = options_for_metapartition.map { |o| Options::DEFAULT.merge(o) }
|
19
|
+
@allow_fewer_shards = allow_fewer_shards
|
20
|
+
ResultStats.members.each { |k| @stats[k] = 0 }
|
19
21
|
end
|
20
22
|
|
21
|
-
PartitionWithMachines = Struct.new :test_time_map, :number_of_shards, :partition_time, :max_shard_count
|
23
|
+
PartitionWithMachines = Struct.new :test_time_map, :number_of_shards, :partition_time, :max_shard_count, :options
|
22
24
|
MachineAssignment = Struct.new :test_time_map, :total_time
|
23
25
|
ResultStats = Struct.new :historical_total_tests, :current_total_tests, :class_extrapolations, :target_extrapolations
|
26
|
+
Options = Struct.new :max_shard_count, :split_bundles_across_machines, :allow_fewer_shards do
|
27
|
+
def merge(hash)
|
28
|
+
self.class.new(*to_h.merge(hash).values_at(*members))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
Options::DEFAULT = Options.new(nil, true, false).freeze
|
24
32
|
|
25
33
|
class PartitionResult
|
26
34
|
TimeImbalances = Struct.new :partition_set, :partitions
|
27
|
-
attr_reader :stats, :test_maps, :test_times, :total_test_time, :test_time_imbalances
|
35
|
+
attr_reader :stats, :test_maps, :test_times, :total_test_time, :test_time_imbalances, :test_time_for_partitions
|
28
36
|
extend Forwardable
|
29
37
|
delegate ResultStats.members => :@stats
|
30
38
|
|
31
|
-
def initialize(stats, partition_sets)
|
39
|
+
def initialize(stats, partition_sets, test_time_for_partitions)
|
32
40
|
@stats = stats
|
33
41
|
@partition_sets = partition_sets
|
34
42
|
@test_maps = partition_sets_map(&:test_time_map)
|
35
43
|
@test_times = partition_sets_map(&:total_time)
|
36
44
|
@total_test_time = test_times.flatten.inject(:+)
|
37
45
|
@test_time_imbalances = compute_test_time_imbalances
|
46
|
+
@test_time_for_partitions = test_time_for_partitions.inject(&:merge)
|
38
47
|
end
|
39
48
|
|
40
49
|
private
|
@@ -74,8 +83,8 @@ module XCKnife
|
|
74
83
|
|
75
84
|
def compute_shards_for_partitions(test_time_for_partitions)
|
76
85
|
PartitionResult.new(@stats, split_machines_proportionally(test_time_for_partitions).map do |partition|
|
77
|
-
compute_single_shards(partition.number_of_shards, partition.test_time_map)
|
78
|
-
end)
|
86
|
+
compute_single_shards(partition.number_of_shards, partition.test_time_map, options: partition.options)
|
87
|
+
end, test_time_for_partitions)
|
79
88
|
end
|
80
89
|
|
81
90
|
def test_time_for_partitions(historical_events, current_events = nil)
|
@@ -101,18 +110,21 @@ module XCKnife
|
|
101
110
|
|
102
111
|
used_shards = 0
|
103
112
|
assignable_shards = number_of_shards - partitions.size
|
104
|
-
partition_with_machines_list = partitions.map do |test_time_map|
|
113
|
+
partition_with_machines_list = partitions.each_with_index.map do |test_time_map, metapartition|
|
114
|
+
options = @options_for_metapartition[metapartition]
|
105
115
|
partition_time = 0
|
106
|
-
max_shard_count = test_time_map.
|
107
|
-
|
116
|
+
max_shard_count = test_time_map.each_value.map(&:size).reduce(&:+) || 1
|
117
|
+
max_shard_count = [max_shard_count, options.max_shard_count].min if options.max_shard_count
|
118
|
+
each_duration(test_time_map) { |duration_in_milliseconds| partition_time += duration_in_milliseconds }
|
108
119
|
n = [1 + (assignable_shards * partition_time.to_f / total).floor, max_shard_count].min
|
109
120
|
used_shards += n
|
110
|
-
PartitionWithMachines.new(test_time_map, n, partition_time, max_shard_count)
|
121
|
+
PartitionWithMachines.new(test_time_map, n, partition_time, max_shard_count, options)
|
111
122
|
end
|
112
123
|
|
113
124
|
fifo_with_machines_who_can_use_more_shards = partition_with_machines_list.select { |x| x.number_of_shards < x.max_shard_count}.sort_by(&:partition_time)
|
114
|
-
while
|
125
|
+
while number_of_shards > used_shards
|
115
126
|
if fifo_with_machines_who_can_use_more_shards.empty?
|
127
|
+
break if @allow_fewer_shards
|
116
128
|
raise XCKnife::XCKnifeError, "There are #{number_of_shards - used_shards} extra machines"
|
117
129
|
end
|
118
130
|
machine = fifo_with_machines_who_can_use_more_shards.pop
|
@@ -125,9 +137,9 @@ module XCKnife
|
|
125
137
|
partition_with_machines_list
|
126
138
|
end
|
127
139
|
|
128
|
-
# Computes
|
140
|
+
# Computes a 2-aproximation to the optimal partition_time, which is an instance of the Open shop scheduling problem (which is NP-hard)
|
129
141
|
# see: https://en.wikipedia.org/wiki/Open-shop_scheduling
|
130
|
-
def compute_single_shards(number_of_shards, test_time_map)
|
142
|
+
def compute_single_shards(number_of_shards, test_time_map, options: Options::DEFAULT)
|
131
143
|
raise XCKnife::XCKnifeError, "There are not enough workers provided" if number_of_shards <= 0
|
132
144
|
raise XCKnife::XCKnifeError, "Cannot shard an empty partition_time" if test_time_map.empty?
|
133
145
|
assignements = Array.new(number_of_shards) { MachineAssignment.new(Hash.new { |k, v| k[v] = [] }, 0) }
|
@@ -139,13 +151,36 @@ module XCKnife
|
|
139
151
|
end
|
140
152
|
end
|
141
153
|
|
142
|
-
|
143
|
-
|
154
|
+
# This might seem like an uncessary level of indirection, but it allows us to keep
|
155
|
+
# logic consistent regardless of the `split_bundles_across_machines` option
|
156
|
+
list_of_test_target_classes_times = list_of_test_target_class_times.group_by do |test_target, class_name, duration_in_milliseconds|
|
157
|
+
if options.split_bundles_across_machines
|
158
|
+
[test_target, class_name]
|
159
|
+
else
|
160
|
+
test_target
|
161
|
+
end
|
162
|
+
end.map do |(test_target, _), classes|
|
163
|
+
[
|
164
|
+
test_target,
|
165
|
+
classes.map { |test_target, class_name, duration_in_milliseconds| class_name },
|
166
|
+
classes.reduce(0) { |total_duration, (test_target, class_name, duration_in_milliseconds)| total_duration + duration_in_milliseconds},
|
167
|
+
]
|
168
|
+
end
|
169
|
+
|
170
|
+
list_of_test_target_classes_times.sort_by! { |test_target, class_names, duration_in_milliseconds| -duration_in_milliseconds }
|
171
|
+
list_of_test_target_classes_times.each do |test_target, class_names, duration_in_milliseconds|
|
144
172
|
assignemnt = assignements.min_by(&:total_time)
|
145
|
-
assignemnt.test_time_map[test_target]
|
173
|
+
assignemnt.test_time_map[test_target].concat class_names
|
146
174
|
assignemnt.total_time += duration_in_milliseconds
|
147
175
|
end
|
148
|
-
|
176
|
+
|
177
|
+
if (empty_test_map_assignments = assignements.select { |a| a.test_time_map.empty? }) && !empty_test_map_assignments.empty? && !options.allow_fewer_shards
|
178
|
+
test_grouping = options.split_bundles_across_machines ? 'classes' : 'targets'
|
179
|
+
raise XCKnife::XCKnifeError, "Too many shards -- #{empty_test_map_assignments.size} of #{number_of_shards} assignments are empty," \
|
180
|
+
" because there are not enough test #{test_grouping} for that many shards."
|
181
|
+
end
|
182
|
+
assignements.reject! { |a| a.test_time_map.empty? }
|
183
|
+
|
149
184
|
assignements
|
150
185
|
end
|
151
186
|
|
data/lib/xcknife/test_dumper.rb
CHANGED
@@ -5,6 +5,9 @@ require 'tmpdir'
|
|
5
5
|
require 'ostruct'
|
6
6
|
require 'set'
|
7
7
|
require 'logger'
|
8
|
+
require 'shellwords'
|
9
|
+
require 'open3'
|
10
|
+
require 'xcknife/exceptions'
|
8
11
|
|
9
12
|
module XCKnife
|
10
13
|
class TestDumper
|
@@ -14,21 +17,25 @@ module XCKnife
|
|
14
17
|
|
15
18
|
attr_reader :logger
|
16
19
|
|
17
|
-
def initialize(args)
|
20
|
+
def initialize(args, logger: Logger.new($stdout, progname: 'xcknife test dumper'))
|
18
21
|
@debug = false
|
19
22
|
@max_retry_count = 150
|
20
23
|
@temporary_output_folder = nil
|
21
24
|
@xcscheme_file = nil
|
22
25
|
@parser = build_parser
|
26
|
+
@naive_dump_bundle_names = []
|
27
|
+
@skip_dump_bundle_names = []
|
28
|
+
@simctl_timeout = 0
|
23
29
|
parse_arguments(args)
|
24
30
|
@device_id ||= "booted"
|
25
|
-
@logger =
|
31
|
+
@logger = logger
|
26
32
|
@logger.level = @debug ? Logger::DEBUG : Logger::FATAL
|
27
33
|
@parser = nil
|
28
34
|
end
|
29
35
|
|
30
36
|
def run
|
31
|
-
helper = TestDumperHelper.new(@device_id, @max_retry_count, @debug, @logger
|
37
|
+
helper = TestDumperHelper.new(@device_id, @max_retry_count, @debug, @logger, @dylib_logfile_path,
|
38
|
+
naive_dump_bundle_names: @naive_dump_bundle_names, skip_dump_bundle_names: @skip_dump_bundle_names, simctl_timeout: @simctl_timeout)
|
32
39
|
extra_environment_variables = parse_scheme_file
|
33
40
|
logger.info { "Environment variables from xcscheme: #{extra_environment_variables.pretty_inspect}" }
|
34
41
|
output_fd = File.open(@output_file, "w")
|
@@ -38,8 +45,7 @@ module XCKnife
|
|
38
45
|
end
|
39
46
|
else
|
40
47
|
unless File.directory?(@temporary_output_folder)
|
41
|
-
|
42
|
-
exit 1
|
48
|
+
raise TestDumpError, "Error no such directory: #{@temporary_output_folder}"
|
43
49
|
end
|
44
50
|
|
45
51
|
if Dir.entries(@temporary_output_folder).any? { |f| File.file?(File.join(@temporary_output_folder,f)) }
|
@@ -62,8 +68,7 @@ module XCKnife
|
|
62
68
|
def parse_scheme_file
|
63
69
|
return {} unless @xcscheme_file
|
64
70
|
unless File.exists?(@xcscheme_file)
|
65
|
-
|
66
|
-
exit 1
|
71
|
+
raise ArgumentError, "Error: no such xcscheme file: #{@xcscheme_file}"
|
67
72
|
end
|
68
73
|
XCKnife::XcschemeAnalyzer.extract_environment_variables(IO.read(@xcscheme_file))
|
69
74
|
end
|
@@ -89,8 +94,12 @@ module XCKnife
|
|
89
94
|
opts.banner += " #{arguments_banner}"
|
90
95
|
opts.on("-d", "--debug", "Debug mode enabled") { |v| @debug = v }
|
91
96
|
opts.on("-r", "--retry-count COUNT", "Max retry count for simulator output", Integer) { |v| @max_retry_count = v }
|
97
|
+
opts.on("-x", '--simctl-timeout SECONDS', "Max allowed time in seconds for simctl commands", Integer) { |v| @simctl_timeout = v }
|
92
98
|
opts.on("-t", "--temporary-output OUTPUT_FOLDER", "Sets temporary Output folder") { |v| @temporary_output_folder = v }
|
93
99
|
opts.on("-s", "--scheme XCSCHEME_FILE", "Reads environments variables from the xcscheme file") { |v| @xcscheme_file = v }
|
100
|
+
opts.on("-l", "--dylib_logfile DYLIB_LOG_FILE", "Path for dylib log file") { |v| @dylib_logfile_path = v }
|
101
|
+
opts.on('--naive-dump TEST_BUNDLE_NAMES', 'List of test bundles to dump using static analysis', Array) { |v| @naive_dump_bundle_names = v }
|
102
|
+
opts.on('--skip-dump TEST_BUNDLE_NAMES', 'List of test bundles to skip dumping', Array) { |v| @skip_dump_bundle_names = v }
|
94
103
|
|
95
104
|
opts.on_tail("-h", "--help", "Show this message") do
|
96
105
|
puts opts
|
@@ -104,7 +113,7 @@ module XCKnife
|
|
104
113
|
end
|
105
114
|
|
106
115
|
def optional_arguments
|
107
|
-
%w[device_id]
|
116
|
+
%w[device_id simctl_timeout]
|
108
117
|
end
|
109
118
|
|
110
119
|
def arguments_banner
|
@@ -113,8 +122,7 @@ module XCKnife
|
|
113
122
|
end
|
114
123
|
|
115
124
|
def warn_and_exit(msg)
|
116
|
-
|
117
|
-
exit 1
|
125
|
+
raise TestDumpError, "#{msg.to_s.capitalize} \n\n#{@parser}"
|
118
126
|
end
|
119
127
|
|
120
128
|
def concat_to_file(test_specification, output_fd)
|
@@ -143,51 +151,72 @@ module XCKnife
|
|
143
151
|
|
144
152
|
attr_reader :logger
|
145
153
|
|
146
|
-
def initialize(device_id, max_retry_count, debug, logger
|
154
|
+
def initialize(device_id, max_retry_count, debug, logger, dylib_logfile_path,
|
155
|
+
naive_dump_bundle_names: [], skip_dump_bundle_names: [], simctl_timeout: 0)
|
147
156
|
@xcode_path = `xcode-select -p`.strip
|
148
157
|
@simctl_path = `xcrun -f simctl`.strip
|
149
|
-
@
|
150
|
-
@
|
151
|
-
@
|
158
|
+
@nm_path = `xcrun -f nm`.strip
|
159
|
+
@swift_path = `xcrun -f swift`.strip
|
160
|
+
@platforms_path = File.join(@xcode_path, "Platforms")
|
161
|
+
@platform_path = File.join(@platforms_path, "iPhoneSimulator.platform")
|
162
|
+
@sdk_path = File.join(@platform_path, "Developer/SDKs/iPhoneSimulator.sdk")
|
152
163
|
@testroot = nil
|
153
164
|
@device_id = device_id
|
154
165
|
@max_retry_count = max_retry_count
|
166
|
+
@simctl_timeout = simctl_timeout
|
155
167
|
@logger = logger
|
156
168
|
@debug = debug
|
169
|
+
@dylib_logfile_path = dylib_logfile_path if dylib_logfile_path
|
170
|
+
@naive_dump_bundle_names = naive_dump_bundle_names
|
171
|
+
@skip_dump_bundle_names = skip_dump_bundle_names
|
157
172
|
end
|
158
173
|
|
159
174
|
def call(derived_data_folder, list_folder, extra_environment_variables = {})
|
160
|
-
@testroot =
|
161
|
-
xctestrun_file = Dir[
|
175
|
+
@testroot = File.join(derived_data_folder, 'Build', 'Products')
|
176
|
+
xctestrun_file = Dir[File.join(@testroot, '*.xctestrun')].first
|
162
177
|
if xctestrun_file.nil?
|
163
|
-
|
164
|
-
exit 1
|
178
|
+
raise ArgumentError, "No xctestrun on #{@testroot}"
|
165
179
|
end
|
166
180
|
xctestrun_as_json = `plutil -convert json -o - "#{xctestrun_file}"`
|
167
181
|
FileUtils.mkdir_p(list_folder)
|
168
|
-
JSON.load(xctestrun_as_json)
|
169
|
-
|
170
|
-
|
182
|
+
list_tests(JSON.load(xctestrun_as_json), list_folder, extra_environment_variables)
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
|
187
|
+
attr_reader :testroot
|
188
|
+
|
189
|
+
def list_tests(xctestrun, list_folder, extra_environment_variables)
|
190
|
+
xctestrun.reject! { |test_bundle_name, _| test_bundle_name == '__xctestrun_metadata__' }
|
191
|
+
xctestrun.map do |test_bundle_name, test_bundle|
|
192
|
+
if @skip_dump_bundle_names.include?(test_bundle_name)
|
193
|
+
logger.info { "Skipping dumping tests in `#{test_bundle_name}` -- writing out fake event"}
|
194
|
+
test_specification = list_single_test(list_folder, test_bundle, test_bundle_name)
|
195
|
+
elsif @naive_dump_bundle_names.include?(test_bundle_name)
|
196
|
+
test_specification = list_tests_with_nm(list_folder, test_bundle, test_bundle_name)
|
197
|
+
else
|
198
|
+
test_specification = list_tests_with_simctl(list_folder, test_bundle, test_bundle_name, extra_environment_variables)
|
199
|
+
wait_test_dumper_completion(test_specification.json_stream_file)
|
200
|
+
end
|
201
|
+
|
171
202
|
test_specification
|
172
203
|
end
|
173
204
|
end
|
174
205
|
|
175
|
-
|
176
|
-
def list_tests_wiht_simctl(list_folder, test_bundle, test_bundle_name, extra_environment_variables)
|
206
|
+
def list_tests_with_simctl(list_folder, test_bundle, test_bundle_name, extra_environment_variables)
|
177
207
|
env_variables = test_bundle["EnvironmentVariables"]
|
178
208
|
testing_env_variables = test_bundle["TestingEnvironmentVariables"]
|
179
|
-
outpath =
|
209
|
+
outpath = File.join(list_folder, test_bundle_name)
|
180
210
|
test_host = replace_vars(test_bundle["TestHostPath"])
|
181
211
|
test_bundle_path = replace_vars(test_bundle["TestBundlePath"], test_host)
|
182
212
|
test_dumper_path = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'TestDumper', 'TestDumper.dylib'))
|
183
213
|
unless File.exist?(test_dumper_path)
|
184
|
-
|
185
|
-
exit 1
|
214
|
+
raise TestDumpError, "Could not find TestDumper.dylib on #{test_dumper_path}"
|
186
215
|
end
|
187
216
|
|
188
217
|
is_logic_test = test_bundle["TestHostBundleIdentifier"].nil?
|
189
218
|
env = simctl_child_attrs(
|
190
|
-
"XCTEST_TYPE" =>
|
219
|
+
"XCTEST_TYPE" => xctest_type(test_bundle),
|
191
220
|
"XCTEST_TARGET" => test_bundle_name,
|
192
221
|
"TestDumperOutputPath" => outpath,
|
193
222
|
"IDE_INJECTION_PATH" => testing_env_variables["DYLD_INSERT_LIBRARIES"],
|
@@ -206,7 +235,7 @@ module XCKnife
|
|
206
235
|
)
|
207
236
|
env.merge!(simctl_child_attrs(extra_environment_variables))
|
208
237
|
inject_vars(env, test_host)
|
209
|
-
FileUtils.
|
238
|
+
FileUtils.rm_f(outpath)
|
210
239
|
logger.info { "Temporary TestDumper file for #{test_bundle_name} is #{outpath}" }
|
211
240
|
if is_logic_test
|
212
241
|
run_logic_test(env, test_host, test_bundle_path)
|
@@ -218,6 +247,46 @@ module XCKnife
|
|
218
247
|
return TestSpecification.new outpath, discover_tests_to_skip(test_bundle)
|
219
248
|
end
|
220
249
|
|
250
|
+
def list_tests_with_nm(list_folder, test_bundle, test_bundle_name)
|
251
|
+
output_methods(list_folder, test_bundle, test_bundle_name) do |test_bundle_path|
|
252
|
+
methods = []
|
253
|
+
swift_demangled_nm(test_bundle_path) do |output|
|
254
|
+
output.each_line do |line|
|
255
|
+
next unless method = method_from_nm_line(line)
|
256
|
+
methods << method
|
257
|
+
end
|
258
|
+
end
|
259
|
+
methods
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def list_single_test(list_folder, test_bundle, test_bundle_name)
|
264
|
+
output_methods(list_folder, test_bundle, test_bundle_name) do
|
265
|
+
[{ class: test_bundle_name, method: 'test' }]
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def output_methods(list_folder, test_bundle, test_bundle_name)
|
270
|
+
outpath = File.join(list_folder, test_bundle_name)
|
271
|
+
logger.info { "Writing out TestDumper file for #{test_bundle_name} to #{outpath}" }
|
272
|
+
test_specification = TestSpecification.new outpath, discover_tests_to_skip(test_bundle)
|
273
|
+
|
274
|
+
test_bundle_path = replace_vars(test_bundle["TestBundlePath"], replace_vars(test_bundle["TestHostPath"]))
|
275
|
+
methods = yield(test_bundle_path)
|
276
|
+
|
277
|
+
test_type = xctest_type(test_bundle)
|
278
|
+
File.open test_specification.json_stream_file, 'a' do |f|
|
279
|
+
f << JSON.dump(message: "Starting Test Dumper", event: "begin-test-suite", testType: test_type) << "\n"
|
280
|
+
f << JSON.dump(event: 'begin-ocunit', bundleName: File.basename(test_bundle_path), targetName: test_bundle_name) << "\n"
|
281
|
+
methods.map { |method| method[:class] }.uniq.each do |class_name|
|
282
|
+
f << JSON.dump(test: '1', className: class_name, event: "end-test", totalDuration: "0") << "\n"
|
283
|
+
end
|
284
|
+
f << JSON.dump(message: "Completed Test Dumper", event: "end-action", testType: test_type) << "\n"
|
285
|
+
end
|
286
|
+
|
287
|
+
test_specification
|
288
|
+
end
|
289
|
+
|
221
290
|
def discover_tests_to_skip(test_bundle)
|
222
291
|
identifier_for_test_method = "/"
|
223
292
|
skip_test_identifiers = test_bundle["SkipTestIdentifiers"] || []
|
@@ -228,10 +297,31 @@ module XCKnife
|
|
228
297
|
@simctl_path
|
229
298
|
end
|
230
299
|
|
300
|
+
def wrapped_simctl(args)
|
301
|
+
args = [*gtimeout, simctl] + args
|
302
|
+
args
|
303
|
+
end
|
304
|
+
|
305
|
+
def gtimeout
|
306
|
+
return [] unless @simctl_timeout > 0
|
307
|
+
|
308
|
+
path = gtimeout_path
|
309
|
+
if path.empty?
|
310
|
+
puts "warning: simctl_timeout specified but 'gtimeout' is not installed. The specified timeout will be ignored."
|
311
|
+
return []
|
312
|
+
end
|
313
|
+
|
314
|
+
[path, "-k", "5", "#{@simctl_timeout}"]
|
315
|
+
end
|
316
|
+
|
317
|
+
def gtimeout_path
|
318
|
+
`which gtimeout`.strip
|
319
|
+
end
|
320
|
+
|
231
321
|
def replace_vars(str, testhost = "<UNKNOWN>")
|
232
322
|
str.gsub("__PLATFORMS__", @platforms_path).
|
233
323
|
gsub("__TESTHOST__", testhost).
|
234
|
-
gsub("__TESTROOT__",
|
324
|
+
gsub("__TESTROOT__", testroot)
|
235
325
|
end
|
236
326
|
|
237
327
|
def inject_vars(env, test_host)
|
@@ -247,9 +337,19 @@ module XCKnife
|
|
247
337
|
end
|
248
338
|
|
249
339
|
def install_app(test_host_path)
|
250
|
-
|
251
|
-
|
340
|
+
retries_count = 0
|
341
|
+
max_retry_count = 3
|
342
|
+
until (retries_count > max_retry_count) or call_simctl(["install", @device_id, test_host_path])
|
343
|
+
retries_count += 1
|
344
|
+
call_simctl ['shutdown', @device_id]
|
345
|
+
call_simctl ['boot', @device_id]
|
346
|
+
sleep 1.0
|
347
|
+
end
|
348
|
+
|
349
|
+
if retries_count > max_retry_count
|
350
|
+
raise TestDumpError, "Installing #{test_host_path} failed"
|
252
351
|
end
|
352
|
+
|
253
353
|
end
|
254
354
|
|
255
355
|
def wait_test_dumper_completion(file)
|
@@ -257,8 +357,7 @@ module XCKnife
|
|
257
357
|
until has_test_dumper_terminated?(file) do
|
258
358
|
retries_count += 1
|
259
359
|
if retries_count == @max_retry_count
|
260
|
-
|
261
|
-
exit 1
|
360
|
+
raise TestDumpError, "Timeout error on: #{file}"
|
262
361
|
end
|
263
362
|
sleep 0.1
|
264
363
|
end
|
@@ -267,29 +366,67 @@ module XCKnife
|
|
267
366
|
def has_test_dumper_terminated?(file)
|
268
367
|
return false unless File.exists?(file)
|
269
368
|
last_line = `tail -n 1 "#{file}"`
|
270
|
-
return
|
369
|
+
return last_line.include?("Completed Test Dumper")
|
271
370
|
end
|
272
371
|
|
273
372
|
def run_apptest(env, test_host_bundle_identifier, test_bundle_path)
|
274
|
-
call_simctl
|
373
|
+
unless call_simctl(["launch", @device_id, test_host_bundle_identifier, '-XCTest', 'All', dylib_logfile_path, test_bundle_path], env: env)
|
374
|
+
raise TestDumpError, "Launching #{test_bundle_path} in #{test_host_bundle_identifier} failed"
|
375
|
+
end
|
275
376
|
end
|
276
377
|
|
277
378
|
def run_logic_test(env, test_host, test_bundle_path)
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
return '' unless @debug
|
283
|
-
' 2> /dev/null'
|
379
|
+
opts = @debug ? {} : { err: "/dev/null" }
|
380
|
+
unless call_simctl(["spawn", @device_id, test_host, '-XCTest', 'All', dylib_logfile_path, test_bundle_path], env: env, **opts)
|
381
|
+
raise TestDumpError, "Spawning #{test_bundle_path} in #{test_host} failed"
|
382
|
+
end
|
284
383
|
end
|
285
384
|
|
286
|
-
def call_simctl(env,
|
287
|
-
|
385
|
+
def call_simctl(args, env: {}, **spawn_opts)
|
386
|
+
args = wrapped_simctl(args)
|
387
|
+
cmd = Shellwords.shelljoin(args)
|
288
388
|
puts "Running:\n$ #{cmd}"
|
289
389
|
logger.info { "Environment variables:\n #{env.pretty_print_inspect}" }
|
290
|
-
|
291
|
-
|
390
|
+
|
391
|
+
ret = system(env, *args, **spawn_opts)
|
392
|
+
puts "Simctl errored with the following env:\n #{env.pretty_print_inspect}" unless ret
|
393
|
+
ret
|
394
|
+
end
|
395
|
+
|
396
|
+
def dylib_logfile_path
|
397
|
+
@dylib_logfile_path ||= '/tmp/xcknife_testdumper_dylib.log'
|
398
|
+
end
|
399
|
+
|
400
|
+
def xctest_type(test_bundle)
|
401
|
+
if test_bundle["TestHostBundleIdentifier"].nil?
|
402
|
+
"LOGICTEST"
|
403
|
+
else
|
404
|
+
"APPTEST"
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
def swift_demangled_nm(test_bundle_path)
|
409
|
+
Open3.pipeline_r([@nm_path, File.join(test_bundle_path, File.basename(test_bundle_path, '.xctest'))], [@swift_path, 'demangle']) do |o, _ts|
|
410
|
+
yield(o)
|
292
411
|
end
|
293
412
|
end
|
413
|
+
|
414
|
+
def method_from_nm_line(line)
|
415
|
+
return unless line.strip =~ %r{^
|
416
|
+
[\da-f]+\s # address
|
417
|
+
[tT]\s # symbol type
|
418
|
+
(?: # method
|
419
|
+
-\[(.+)\s(test.+)\] # objc instance method
|
420
|
+
| # or swift instance method
|
421
|
+
_? # only present on Xcode 10.0 and below
|
422
|
+
(?:@objc\s)? # optional objc annotation
|
423
|
+
(?:[^\.]+\.)? # module name
|
424
|
+
(.+) # class name
|
425
|
+
\.(test.+)\s->\s\(\) # method signature
|
426
|
+
)
|
427
|
+
$}ox
|
428
|
+
|
429
|
+
{ class: $1 || $3, method: $2 || $4 }
|
430
|
+
end
|
294
431
|
end
|
295
432
|
end
|
@@ -1,4 +1,5 @@
|
|
1
|
-
require '
|
1
|
+
require 'set'
|
2
|
+
|
2
3
|
module XCKnife
|
3
4
|
module XCToolCmdHelper
|
4
5
|
def only_arguments_for_a_partition_set(output_type, partition_set)
|
@@ -24,17 +25,41 @@ module XCKnife
|
|
24
25
|
end
|
25
26
|
|
26
27
|
# only-testing is available since Xcode 8
|
27
|
-
def xcodebuild_only_arguments(single_partition)
|
28
|
-
|
28
|
+
def xcodebuild_only_arguments(single_partition, meta_partition = nil)
|
29
|
+
only_targets = if meta_partition
|
30
|
+
single_partition.keys.to_set & meta_partition.flat_map(&:keys).group_by(&:to_s).select{|_,v| v.size == 1 }.map(&:first).to_set
|
31
|
+
else
|
32
|
+
Set.new
|
33
|
+
end
|
34
|
+
|
35
|
+
only_target_arguments = only_targets.sort.map { |test_target| "-only-testing:#{test_target}" }
|
36
|
+
|
37
|
+
only_class_arguments = single_partition.flat_map do |test_target, classes|
|
38
|
+
next [] if only_targets.include?(test_target)
|
39
|
+
|
29
40
|
classes.sort.map do |clazz|
|
30
41
|
"-only-testing:#{test_target}/#{clazz}"
|
31
42
|
end
|
43
|
+
end.sort
|
44
|
+
|
45
|
+
only_target_arguments + only_class_arguments
|
46
|
+
end
|
32
47
|
|
48
|
+
# skip-testing is available since Xcode 8
|
49
|
+
def xcodebuild_skip_arguments(single_partition, test_time_for_partitions)
|
50
|
+
excluded_targets = test_time_for_partitions.keys.to_set - single_partition.keys.to_set
|
51
|
+
skipped_target_arguments = excluded_targets.sort.map { |test_target| "-skip-testing:#{test_target}" }
|
52
|
+
|
53
|
+
skipped_classes_arguments = single_partition.flat_map do |test_target, classes|
|
54
|
+
all_classes = test_time_for_partitions[test_target].keys.to_set
|
55
|
+
(all_classes - classes.to_set).sort.map { |test_class| "-skip-testing:#{test_target}/#{test_class}" }
|
33
56
|
end
|
57
|
+
|
58
|
+
skipped_target_arguments + skipped_classes_arguments
|
34
59
|
end
|
35
60
|
|
36
61
|
def xcodebuild_only_arguments_for_a_partition_set(partition_set)
|
37
62
|
partition_set.map { |partition| xctool_only_arguments(partition) }
|
38
63
|
end
|
39
64
|
end
|
40
|
-
end
|
65
|
+
end
|
data/xcknife.gemspec
CHANGED
@@ -17,9 +17,6 @@ Gem::Specification.new do |s|
|
|
17
17
|
Works by leveraging xctool's json-streams timing and test data.
|
18
18
|
DESCRIPTION
|
19
19
|
|
20
|
-
# Only allow gem to be pushed to https://rubygems.org
|
21
|
-
s.metadata["allowed_push_host"] = 'https://rubygems.org'
|
22
|
-
|
23
20
|
s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR).reject { |f| f =~ /^spec/} + ["TestDumper/TestDumper.dylib"]
|
24
21
|
s.bindir = 'bin'
|
25
22
|
s.executables = ['xcknife', 'xcknife-min', 'xcknife-test-dumper']
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: xcknife
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.11.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Ribeiro
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-06-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -38,18 +38,20 @@ extensions: []
|
|
38
38
|
extra_rdoc_files: []
|
39
39
|
files:
|
40
40
|
- ".gitignore"
|
41
|
-
- ".gitmodules"
|
42
41
|
- ".rspec"
|
43
42
|
- ".ruby-version"
|
44
43
|
- ".travis.yml"
|
45
44
|
- CONTRIBUTING.md
|
46
45
|
- Gemfile
|
46
|
+
- Gemfile.lock
|
47
47
|
- LICENSE
|
48
|
+
- OWNERS.yml
|
48
49
|
- README.md
|
49
50
|
- Rakefile
|
50
51
|
- TestDumper/README.md
|
51
52
|
- TestDumper/TestDumper.dylib
|
52
53
|
- TestDumper/TestDumper.xcodeproj/project.pbxproj
|
54
|
+
- TestDumper/TestDumper.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
|
53
55
|
- TestDumper/TestDumper.xcodeproj/xcshareddata/xcschemes/TestDumper.xcscheme
|
54
56
|
- TestDumper/TestDumper/Info.plist
|
55
57
|
- TestDumper/TestDumper/Initialize.m
|
@@ -75,8 +77,7 @@ files:
|
|
75
77
|
homepage: https://github.com/square/xcknife
|
76
78
|
licenses:
|
77
79
|
- Apache-2.0
|
78
|
-
metadata:
|
79
|
-
allowed_push_host: https://rubygems.org
|
80
|
+
metadata: {}
|
80
81
|
post_install_message:
|
81
82
|
rdoc_options: []
|
82
83
|
require_paths:
|
@@ -92,8 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
93
|
- !ruby/object:Gem::Version
|
93
94
|
version: '0'
|
94
95
|
requirements: []
|
95
|
-
|
96
|
-
rubygems_version: 2.4.5.1
|
96
|
+
rubygems_version: 3.0.8
|
97
97
|
signing_key:
|
98
98
|
specification_version: 4
|
99
99
|
summary: Simple tool for optimizing XCTest runs across machines
|
data/.gitmodules
DELETED