xcknife 0.6.4 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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