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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 1e7dcc40d67413a5b35f7eba1289d75724965a63
4
- data.tar.gz: 7ea917dd613f5fcdd8e6a41471538c01bf9df4d2
2
+ SHA256:
3
+ metadata.gz: e63c27c7af4820f72ed2ef5813306be1dc4d6793fc837bca402d53ac53eff7ff
4
+ data.tar.gz: 180ebe00183782dbb1b0b63cdf23ce010ffe31a6cc910b921ff031f32f97d462
5
5
  SHA512:
6
- metadata.gz: b786211a86a81b9483a8721de939de4bcb7eba683698e334d6f299e706153ee607a3b8de6621599465cecbf236b42805618f2e71812a664c9c9cf8abd20c8ae3
7
- data.tar.gz: 1295e3a8a1e7040d0acadf14d42e2c36838f9fdde95c8d57c83b4bcaa4986165b689239a27cdecbdb512233110b3d54c40d1175c649f7c8b7c267642495c47bf
6
+ metadata.gz: cb9824f69aef912ab664c29c56ef5b9f00a58765b3361f109917bfd0374d4ac003c3420f76e20f27b820d71a516e07d676bc0718d171cc1c0c1d9849553d9421
7
+ data.tar.gz: 50c7119def1b9d0334fcd55677838b346f7ba1971b6cf949fd664190494628a7799aacf1cc45b9977bc099e0675955a29bfdf571059456b309f86a7c0e4bdb7b
data/.gitignore CHANGED
@@ -2,9 +2,10 @@
2
2
  pkg
3
3
  .idea/
4
4
  *.gem
5
- Gemfile.lock
6
5
  xcuserdata/
7
6
  testdumperbuild
8
7
  contents.xcworkspacedata
9
8
  TestDumper/TestDumper.dylib
10
9
  *.tmp
10
+ .bin/
11
+ .bundle/
@@ -1 +1 @@
1
- 2.2.3
1
+ 2.6.1
@@ -3,11 +3,7 @@ sudo: false
3
3
  matrix:
4
4
  include:
5
5
  - os: osx
6
- osx_image: xcode8
7
- - os: osx
8
- osx_image: xcode8.1
9
- - os: osx
10
- osx_image: xcode8.2
6
+ osx_image: xcode10.2
11
7
 
12
8
  before_script:
13
9
  - export LANG=en_US.UTF-8
@@ -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
@@ -0,0 +1,2 @@
1
+ ---
2
+ cc: [mdx-ios]
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://corner.squareup.com/2016/06/xcknife-faster-distributed-tests-for-ios.html).
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 imabalances (both internal to the partition sets, and amongst them).
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
+
@@ -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 Xcode8 introduced on xcodebuild. In particular, it leverages the xctestrun file (see `man xcodebuild.xctestrun`).
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 = 0700;
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 "toolc" */;
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 "toolc" */ = {
508
+ F60C68B41B8D038300CC8521 /* Build configuration list for PBXProject "TestDumper" */ = {
487
509
  isa = XCConfigurationList;
488
510
  buildConfigurations = (
489
511
  F60C68CE1B8D038300CC8521 /* Debug */,
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>IDEDidComputeMac32BitWarning</key>
6
+ <true/>
7
+ </dict>
8
+ </plist>
@@ -1,6 +1,6 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
2
  <Scheme
3
- LastUpgradeVersion = "0800"
3
+ LastUpgradeVersion = "1010"
4
4
  version = "1.3">
5
5
  <BuildAction
6
6
  parallelizeBuildables = "YES"
@@ -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
- NSLog(@"ERROR: Error generating JSON for object: %s: %s\n",
120
+ logDebug(@"ERROR: Error generating JSON for object: %s: %s\n",
85
121
  [[JSONObject description] UTF8String],
86
122
  [[error localizedFailureReason] UTF8String]);
87
- exit(1);
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
- NSLog(@"File already exists %@. Stopping", testDumperOutputPath);
131
- exit(0);
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
- void enumerateTests() {
146
- XCTestConfiguration *config = [[XCTestConfiguration alloc] init];
147
- NSString *testType = [NSString stringWithUTF8String: getenv("XCTEST_TYPE")];
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
- NSString *testBundle = [[[NSProcessInfo processInfo] arguments] lastObject];
167
- [[NSBundle bundleWithPath:testBundle] load];
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
- NSLog(@"Opened %@ with fd %p", testDumperOutputPath, outFile);
216
+ logDebug(@"Opened %@ with fd %p", testDumperOutputPath, outFile);
179
217
  if (outFile == NULL) {
180
- NSLog(@"File already exists %@. Stopping", testDumperOutputPath);
181
- exit(0);
218
+ logDebug(@"File already exists at %@. Stopping", testDumperOutputPath);
219
+ logEnd(true);
182
220
  }
183
-
221
+
184
222
  PrintDumpStart(outFile, testType);
185
- [[XCTestSuite defaultTestSuite] printTestsWithLevel:0 withTarget: testTarget withParent: nil outputFile:outFile];
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
- exit(0);
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
- NSLog(@"Uknown level %ld", level);
250
+ logDebug(@"Unknown test level %ld for test %@", level, t.debugDescription);
212
251
 
213
252
  }
214
253
  if (level == TEST_METHOD_LEVEL) {
@@ -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
@@ -7,5 +7,5 @@ require 'xcknife/exceptions'
7
7
  require 'xcknife/xcscheme_analyzer'
8
8
 
9
9
  module XCKnife
10
- VERSION = '0.6.4'
10
+ VERSION = '0.11.1'
11
11
  end
@@ -1,6 +1,8 @@
1
1
  module XCKnife
2
2
  # Base error class for xcknife
3
- XCKnifeError = Class.new(StandardError)
3
+ XCKnifeError = Class.new(RuntimeError)
4
+
5
+ TestDumpError = Class.new(XCKnifeError)
4
6
 
5
7
  StreamParsingError = Class.new(XCKnifeError)
6
- end
8
+ end
@@ -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
- ResultStats.members.each { |k| @stats[k] = 0}
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.values.map(&:size).inject(:+) || 1
107
- each_duration(test_time_map) { |duration_in_milliseconds| partition_time += duration_in_milliseconds}
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 (number_of_shards - used_shards) > 0
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 a 2-aproximation to the optimal partition_time, which is an instance of the Open shop scheduling problem (which is NP-hard)
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
- list_of_test_target_class_times.sort_by! { |test_target, class_name, duration_in_milliseconds| -duration_in_milliseconds }
143
- list_of_test_target_class_times.each do |test_target, class_name, duration_in_milliseconds|
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] << class_name
173
+ assignemnt.test_time_map[test_target].concat class_names
146
174
  assignemnt.total_time += duration_in_milliseconds
147
175
  end
148
- raise XCKnife::XCKnifeError, "Too many shards" if assignements.any? { |a| a.test_time_map.empty? }
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
 
@@ -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 = Logger.new($stdout)
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
- puts "Error no such directory: #{@temporary_output_folder}"
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
- puts "Error: no such xcscheme file: #{@xcscheme_file}"
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
- warn "#{msg.to_s.capitalize} \n\n#{@parser}"
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
- @platforms_path = "#{@xcode_path}/Platforms/"
150
- @platform_path = "#{@platforms_path}/iPhoneSimulator.platform"
151
- @sdk_path = "#{@platform_path}/Developer/SDKs/iPhoneSimulator.sdk"
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 = "#{derived_data_folder}/Build/Products/"
161
- xctestrun_file = Dir["#{@testroot}/*.xctestrun"].first
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
- puts "No xctestrun on #{@testroot}"
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).map do |test_bundle_name, test_bundle|
169
- test_specification = list_tests_wiht_simctl(list_folder, test_bundle, test_bundle_name, extra_environment_variables)
170
- wait_test_dumper_completion(test_specification.json_stream_file)
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
- private
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 = "#{list_folder}/#{test_bundle_name}"
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
- warn "Could not find TestDumpber.dylib on #{test_dumper_path}"
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" => is_logic_test ? "LOGICTEST" : "APPTEST",
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.remove(outpath) if File.exists?(outpath)
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__", @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
- until system("#{simctl} install #{@device_id} '#{test_host_path}'")
251
- sleep 0.1
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
- puts "Timeout error on: #{file}"
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 /Completed Test Dumper/.match(last_line)
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 env, "launch #{@device_id} '#{test_host_bundle_identifier}' -XCTest All '#{test_bundle_path}'"
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
- call_simctl env, "spawn #{@device_id} '#{test_host}' -XCTest All '#{test_bundle_path}'#{redirect_output}"
279
- end
280
-
281
- def redirect_output
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, string_args)
287
- cmd = "#{simctl} #{string_args}"
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
- unless system(env, cmd)
291
- puts "Simctl errored with the following env:\n #{env.pretty_print_inspect}"
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 'pp'
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
- single_partition.flat_map do |test_target, classes|
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
@@ -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.6.4
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: 2017-02-22 00:00:00.000000000 Z
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
- rubyforge_project:
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
@@ -1,3 +0,0 @@
1
- [submodule "spec/xcknife-exemplar"]
2
- path = spec/xcknife-exemplar
3
- url = https://github.com/danielribeiro/xcknife-exemplar.git