xcknife 0.6.4 → 0.11.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml 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