xcknife 0.6.6 → 0.10.0

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: 7503a50d7e372941c78a9274cafb77a1df3da572
4
- data.tar.gz: 4b033eb3921f6e77174754e8b144feabd39d8d19
2
+ SHA256:
3
+ metadata.gz: aca898703e491f17c9923c8c7e28496e0cbac2e2ad1f9b82ae1a632142bacad9
4
+ data.tar.gz: ea35d7ce95bd76c7d5011683047245a457668acf2b49c070808e99eb06be6da3
5
5
  SHA512:
6
- metadata.gz: 8beab2cbde2dd91002831cc729912a95e95e2ff8fe85187ae94842d2038c7673972d32d1ecda6ff00737886a5db450c19caf29b2b2c1064954c7f954821a6b22
7
- data.tar.gz: 1354e663134f746808560e11af61406986759fe3f30b4341bb054cd07ce92ae207413865cee6d86995a4fef8d562f4f27bf9d638f5a4947ec30b4c6c386f7ecb
6
+ metadata.gz: 548661d771e7e367e9fd4cee9687761ecd8b1a504702816e096efd1a0b41c60454deda107f56e73096bd851b20d054931ef3b8605c90c87948d4af59aecad157
7
+ data.tar.gz: 052d69c88e71f0ad388c676ee3f6f374c814b1acec74c090f27acb54541db02f189d3e18bf8e578947b5fa227931bbcfd9aa16ac087e0aee78652c9032c5441a
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/
@@ -0,0 +1,5 @@
1
+ [build "PR"]
2
+ template = iOS-Builder-Multi-Xcode-PR-Template
3
+ build_script = ./sqiosbuild/sqiosbuild.sh specs
4
+ trigger_build = true
5
+ pull_request = true
@@ -1 +1 @@
1
- 2.2.3
1
+ 2.3.1
@@ -0,0 +1,6 @@
1
+ {
2
+ "specs": {
3
+ "post_build": ["bundle", "exec", "rake", "build_test_dumper", "spec"],
4
+ "xcode_version": "9.4.0"
5
+ }
6
+ }
data/Gemfile CHANGED
@@ -1,8 +1,11 @@
1
- source 'https://rubygems.org'
1
+ source 'https://gems.vip.global.square'
2
+ source 'https://gems.vip.global.square/private'
2
3
 
3
4
  group :development, :test do
4
5
  gem 'rake', '~> 11.1.1'
5
6
  gem 'rspec', '~> 3.4.0'
6
7
  end
7
8
 
9
+ gem 'sq-gem_tasks', '~> 1.0'
10
+
8
11
  gemspec
@@ -0,0 +1,41 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ xcknife (0.10.0)
5
+
6
+ GEM
7
+ remote: https://gems.vip.global.square/
8
+ remote: https://gems.vip.global.square/private/
9
+ specs:
10
+ diff-lcs (1.3)
11
+ rake (11.1.2)
12
+ rangeclient (0.0.17)
13
+ rspec (3.4.0)
14
+ rspec-core (~> 3.4.0)
15
+ rspec-expectations (~> 3.4.0)
16
+ rspec-mocks (~> 3.4.0)
17
+ rspec-core (3.4.4)
18
+ rspec-support (~> 3.4.0)
19
+ rspec-expectations (3.4.0)
20
+ diff-lcs (>= 1.2.0, < 2.0)
21
+ rspec-support (~> 3.4.0)
22
+ rspec-mocks (3.4.1)
23
+ diff-lcs (>= 1.2.0, < 2.0)
24
+ rspec-support (~> 3.4.0)
25
+ rspec-support (3.4.1)
26
+ sq-gem_tasks (1.7.1)
27
+ bundler (~> 1.6)
28
+ rangeclient (>= 0.0.12)
29
+
30
+ PLATFORMS
31
+ ruby
32
+
33
+ DEPENDENCIES
34
+ bundler (~> 1.12)
35
+ rake (~> 11.1.1)
36
+ rspec (~> 3.4.0)
37
+ sq-gem_tasks (~> 1.0)
38
+ xcknife!
39
+
40
+ BUNDLED WITH
41
+ 1.17.3
@@ -0,0 +1,2 @@
1
+ ---
2
+ cc: [mdx-ios]
data/Rakefile CHANGED
@@ -1,5 +1,5 @@
1
1
  require 'rspec/core/rake_task'
2
- require "bundler/gem_tasks"
2
+ require 'sq/gem_tasks'
3
3
  require 'fileutils'
4
4
 
5
5
  RSpec::Core::RakeTask.new(:spec)
@@ -19,4 +19,5 @@ end
19
19
  desc "Release wih test_dumper"
20
20
  task :gem_release => [:build_test_dumper, :build] do
21
21
  system 'gem push pkg/xcknife-*.gem'
22
- end
22
+ end
23
+
@@ -71,6 +71,38 @@
71
71
 
72
72
  #include <dlfcn.h>
73
73
 
74
+ // Logging
75
+ FILE *logFile;
76
+
77
+ void initializeLogFile(char *logFilePath)
78
+ {
79
+ logFile = fopen(logFilePath, "w");
80
+ }
81
+
82
+ void logDebug(NSString *msg)
83
+ {
84
+ assert(logFile);
85
+ fprintf(logFile, "%s", msg.UTF8String);
86
+ fprintf(logFile, "\n");
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
+ void logEnd()
103
+ {
104
+ logDebug(@"EndingTestDumper...");
105
+ }
74
106
 
75
107
  // Used for a structured log, just like Xctool's.
76
108
  // Example: https://github.com/square/xcknife/blob/master/example/xcknife-exemplar.json-stream
@@ -119,10 +151,15 @@ void enumerateTests();
119
151
  const int TEST_TARGET_LEVEL = 0;
120
152
  const int TEST_CLASS_LEVEL = 1;
121
153
  const int TEST_METHOD_LEVEL = 2;
154
+ FILE *noteFile;
122
155
 
123
156
  __attribute__((constructor))
124
157
  void initialize() {
125
158
  NSLog(@"Starting TestDumper");
159
+ char *logFilePath = [[[NSProcessInfo processInfo] arguments][3] UTF8String];
160
+ initializeLogFile(logFilePath);
161
+ logInit();
162
+ NSString *testBundlePath = [[NSProcessInfo processInfo] arguments][4];
126
163
  NSString *testDumperOutputPath = NSProcessInfo.processInfo.environment[@"TestDumperOutputPath"];
127
164
  NSFileManager *fileManager = [NSFileManager defaultManager];
128
165
 
@@ -134,38 +171,41 @@ void initialize() {
134
171
 
135
172
  if ([testType isEqualToString: @"APPTEST"]) {
136
173
  [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
137
- enumerateTests();
174
+ enumerateTests(testBundlePath);
138
175
  }];
139
176
  } else {
140
- enumerateTests();
177
+ enumerateTests(testBundlePath);
141
178
  }
142
179
  }
143
180
 
144
-
145
- void enumerateTests() {
146
- XCTestConfiguration *config = [[XCTestConfiguration alloc] init];
181
+ void enumerateTests(NSString *testBundlePath) {
182
+ logDebug(@"Listing all test bundles");
183
+ for (NSBundle *bundle in NSBundle.allBundles) {
184
+ NSString *string = [@"Found a test bundle named: " stringByAppendingString:bundle.bundlePath];
185
+ logDebug(string);
186
+ }
187
+ logDebug(@"Finished listing all test bundles");
188
+
189
+ NSBundle* testBundleObj = [NSBundle bundleWithPath:testBundlePath];
190
+ [testBundleObj load];
191
+ logDebug(@"test bundle loaded");
192
+
193
+ logDebug(@"Listing all test bundles");
194
+ for (NSBundle *bundle in NSBundle.allBundles) {
195
+ NSString *string = [@"Found a test bundle named: " stringByAppendingString:bundle.bundlePath];
196
+ logDebug(string);
197
+ }
198
+ logDebug(@"Finished listing all test bundles");
199
+
147
200
  NSString *testType = [NSString stringWithUTF8String: getenv("XCTEST_TYPE")];
148
- NSString *testTarget = [NSString stringWithUTF8String: getenv("XCTEST_TARGET")];
149
-
201
+ NSString *testTarget = [[[testBundlePath componentsSeparatedByString:@"/"] lastObject] componentsSeparatedByString:@"."][0];
202
+
203
+ logDebug(@"The test target is:");
204
+ logDebug(testTarget);
150
205
  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);
206
+ logDebug(@"IS APPTEST");
165
207
  }
166
- NSString *testBundle = [[[NSProcessInfo processInfo] arguments] lastObject];
167
- [[NSBundle bundleWithPath:testBundle] load];
168
-
208
+
169
209
  FILE *outFile;
170
210
  NSString *testDumperOutputPath = NSProcessInfo.processInfo.environment[@"TestDumperOutputPath"];
171
211
 
@@ -177,14 +217,17 @@ void enumerateTests() {
177
217
 
178
218
  NSLog(@"Opened %@ with fd %p", testDumperOutputPath, outFile);
179
219
  if (outFile == NULL) {
220
+ logDebug(@"File already exists. Stopping");
180
221
  NSLog(@"File already exists %@. Stopping", testDumperOutputPath);
181
222
  exit(0);
182
223
  }
183
-
224
+
184
225
  PrintDumpStart(outFile, testType);
185
- [[XCTestSuite defaultTestSuite] printTestsWithLevel:0 withTarget: testTarget withParent: nil outputFile:outFile];
226
+ XCTestSuite* testSuite = [XCTestSuite defaultTestSuite];
227
+ [testSuite printTestsWithLevel:0 withTarget: testTarget withParent: nil outputFile:outFile];
186
228
  PrintDumpEnd(outFile, testType);
187
229
  fclose(outFile);
230
+ logEnd();
188
231
  exit(0);
189
232
  }
190
233
 
@@ -7,5 +7,5 @@ require 'xcknife/exceptions'
7
7
  require 'xcknife/xcscheme_analyzer'
8
8
 
9
9
  module XCKnife
10
- VERSION = '0.6.6'
10
+ VERSION = '0.10.0'
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,17 +10,25 @@ 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
@@ -75,7 +83,7 @@ module XCKnife
75
83
 
76
84
  def compute_shards_for_partitions(test_time_for_partitions)
77
85
  PartitionResult.new(@stats, split_machines_proportionally(test_time_for_partitions).map do |partition|
78
- compute_single_shards(partition.number_of_shards, partition.test_time_map)
86
+ compute_single_shards(partition.number_of_shards, partition.test_time_map, options: partition.options)
79
87
  end, test_time_for_partitions)
80
88
  end
81
89
 
@@ -102,18 +110,21 @@ module XCKnife
102
110
 
103
111
  used_shards = 0
104
112
  assignable_shards = number_of_shards - partitions.size
105
- 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]
106
115
  partition_time = 0
107
- max_shard_count = test_time_map.values.map(&:size).inject(:+) || 1
108
- 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 }
109
119
  n = [1 + (assignable_shards * partition_time.to_f / total).floor, max_shard_count].min
110
120
  used_shards += n
111
- PartitionWithMachines.new(test_time_map, n, partition_time, max_shard_count)
121
+ PartitionWithMachines.new(test_time_map, n, partition_time, max_shard_count, options)
112
122
  end
113
123
 
114
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)
115
- while (number_of_shards - used_shards) > 0
125
+ while number_of_shards > used_shards
116
126
  if fifo_with_machines_who_can_use_more_shards.empty?
127
+ break if @allow_fewer_shards
117
128
  raise XCKnife::XCKnifeError, "There are #{number_of_shards - used_shards} extra machines"
118
129
  end
119
130
  machine = fifo_with_machines_who_can_use_more_shards.pop
@@ -128,7 +139,7 @@ module XCKnife
128
139
 
129
140
  # Computes a 2-aproximation to the optimal partition_time, which is an instance of the Open shop scheduling problem (which is NP-hard)
130
141
  # see: https://en.wikipedia.org/wiki/Open-shop_scheduling
131
- def compute_single_shards(number_of_shards, test_time_map)
142
+ def compute_single_shards(number_of_shards, test_time_map, options: Options::DEFAULT)
132
143
  raise XCKnife::XCKnifeError, "There are not enough workers provided" if number_of_shards <= 0
133
144
  raise XCKnife::XCKnifeError, "Cannot shard an empty partition_time" if test_time_map.empty?
134
145
  assignements = Array.new(number_of_shards) { MachineAssignment.new(Hash.new { |k, v| k[v] = [] }, 0) }
@@ -140,13 +151,36 @@ module XCKnife
140
151
  end
141
152
  end
142
153
 
143
- list_of_test_target_class_times.sort_by! { |test_target, class_name, duration_in_milliseconds| -duration_in_milliseconds }
144
- 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|
145
172
  assignemnt = assignements.min_by(&:total_time)
146
- assignemnt.test_time_map[test_target] << class_name
173
+ assignemnt.test_time_map[test_target].concat class_names
147
174
  assignemnt.total_time += duration_in_milliseconds
148
175
  end
149
- 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
+
150
184
  assignements
151
185
  end
152
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,24 @@ 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 = []
23
28
  parse_arguments(args)
24
29
  @device_id ||= "booted"
25
- @logger = Logger.new($stdout)
30
+ @logger = logger
26
31
  @logger.level = @debug ? Logger::DEBUG : Logger::FATAL
27
32
  @parser = nil
28
33
  end
29
34
 
30
35
  def run
31
- helper = TestDumperHelper.new(@device_id, @max_retry_count, @debug, @logger)
36
+ helper = TestDumperHelper.new(@device_id, @max_retry_count, @debug, @logger, @dylib_logfile_path,
37
+ naive_dump_bundle_names: @naive_dump_bundle_names, skip_dump_bundle_names: @skip_dump_bundle_names)
32
38
  extra_environment_variables = parse_scheme_file
33
39
  logger.info { "Environment variables from xcscheme: #{extra_environment_variables.pretty_inspect}" }
34
40
  output_fd = File.open(@output_file, "w")
@@ -38,8 +44,7 @@ module XCKnife
38
44
  end
39
45
  else
40
46
  unless File.directory?(@temporary_output_folder)
41
- puts "Error no such directory: #{@temporary_output_folder}"
42
- exit 1
47
+ raise TestDumpError, "Error no such directory: #{@temporary_output_folder}"
43
48
  end
44
49
 
45
50
  if Dir.entries(@temporary_output_folder).any? { |f| File.file?(File.join(@temporary_output_folder,f)) }
@@ -62,8 +67,7 @@ module XCKnife
62
67
  def parse_scheme_file
63
68
  return {} unless @xcscheme_file
64
69
  unless File.exists?(@xcscheme_file)
65
- puts "Error: no such xcscheme file: #{@xcscheme_file}"
66
- exit 1
70
+ raise ArgumentError, "Error: no such xcscheme file: #{@xcscheme_file}"
67
71
  end
68
72
  XCKnife::XcschemeAnalyzer.extract_environment_variables(IO.read(@xcscheme_file))
69
73
  end
@@ -91,6 +95,9 @@ module XCKnife
91
95
  opts.on("-r", "--retry-count COUNT", "Max retry count for simulator output", Integer) { |v| @max_retry_count = v }
92
96
  opts.on("-t", "--temporary-output OUTPUT_FOLDER", "Sets temporary Output folder") { |v| @temporary_output_folder = v }
93
97
  opts.on("-s", "--scheme XCSCHEME_FILE", "Reads environments variables from the xcscheme file") { |v| @xcscheme_file = v }
98
+ opts.on("-l", "--dylib_logfile DYLIB_LOG_FILE", "Path for dylib log file") { |v| @dylib_logfile_path = v }
99
+ opts.on('--naive-dump TEST_BUNDLE_NAMES', 'List of test bundles to dump using static analysis', Array) { |v| @naive_dump_bundle_names = v }
100
+ opts.on('--skip-dump TEST_BUNDLE_NAMES', 'List of test bundles to skip dumping', Array) { |v| @skip_dump_bundle_names = v }
94
101
 
95
102
  opts.on_tail("-h", "--help", "Show this message") do
96
103
  puts opts
@@ -113,8 +120,7 @@ module XCKnife
113
120
  end
114
121
 
115
122
  def warn_and_exit(msg)
116
- warn "#{msg.to_s.capitalize} \n\n#{@parser}"
117
- exit 1
123
+ raise TestDumpError, "#{msg.to_s.capitalize} \n\n#{@parser}"
118
124
  end
119
125
 
120
126
  def concat_to_file(test_specification, output_fd)
@@ -143,51 +149,71 @@ module XCKnife
143
149
 
144
150
  attr_reader :logger
145
151
 
146
- def initialize(device_id, max_retry_count, debug, logger)
152
+ def initialize(device_id, max_retry_count, debug, logger, dylib_logfile_path,
153
+ naive_dump_bundle_names: [], skip_dump_bundle_names: [])
147
154
  @xcode_path = `xcode-select -p`.strip
148
155
  @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"
156
+ @nm_path = `xcrun -f nm`.strip
157
+ @swift_path = `xcrun -f swift`.strip
158
+ @platforms_path = File.join(@xcode_path, "Platforms")
159
+ @platform_path = File.join(@platforms_path, "iPhoneSimulator.platform")
160
+ @sdk_path = File.join(@platform_path, "Developer/SDKs/iPhoneSimulator.sdk")
152
161
  @testroot = nil
153
162
  @device_id = device_id
154
163
  @max_retry_count = max_retry_count
155
164
  @logger = logger
156
165
  @debug = debug
166
+ @dylib_logfile_path = dylib_logfile_path if dylib_logfile_path
167
+ @naive_dump_bundle_names = naive_dump_bundle_names
168
+ @skip_dump_bundle_names = skip_dump_bundle_names
157
169
  end
158
170
 
159
171
  def call(derived_data_folder, list_folder, extra_environment_variables = {})
160
- @testroot = "#{derived_data_folder}/Build/Products/"
161
- xctestrun_file = Dir["#{@testroot}/*.xctestrun"].first
172
+ @testroot = File.join(derived_data_folder, 'Build', 'Products')
173
+ xctestrun_file = Dir[File.join(@testroot, '*.xctestrun')].first
162
174
  if xctestrun_file.nil?
163
- puts "No xctestrun on #{@testroot}"
164
- exit 1
175
+ raise ArgumentError, "No xctestrun on #{@testroot}"
165
176
  end
166
177
  xctestrun_as_json = `plutil -convert json -o - "#{xctestrun_file}"`
167
178
  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)
179
+ list_tests(JSON.load(xctestrun_as_json), list_folder, extra_environment_variables)
180
+ end
181
+
182
+ private
183
+
184
+ attr_reader :testroot
185
+
186
+ def list_tests(xctestrun, list_folder, extra_environment_variables)
187
+ xctestrun.reject! { |test_bundle_name, _| test_bundle_name == '__xctestrun_metadata__' }
188
+ xctestrun.map do |test_bundle_name, test_bundle|
189
+ if @skip_dump_bundle_names.include?(test_bundle_name)
190
+ logger.info { "Skipping dumping tests in `#{test_bundle_name}` -- writing out fake event"}
191
+ test_specification = list_single_test(list_folder, test_bundle, test_bundle_name)
192
+ elsif @naive_dump_bundle_names.include?(test_bundle_name)
193
+ test_specification = list_tests_with_nm(list_folder, test_bundle, test_bundle_name)
194
+ else
195
+ test_specification = list_tests_with_simctl(list_folder, test_bundle, test_bundle_name, extra_environment_variables)
196
+ wait_test_dumper_completion(test_specification.json_stream_file)
197
+ end
198
+
171
199
  test_specification
172
200
  end
173
201
  end
174
202
 
175
- private
176
- def list_tests_wiht_simctl(list_folder, test_bundle, test_bundle_name, extra_environment_variables)
203
+ def list_tests_with_simctl(list_folder, test_bundle, test_bundle_name, extra_environment_variables)
177
204
  env_variables = test_bundle["EnvironmentVariables"]
178
205
  testing_env_variables = test_bundle["TestingEnvironmentVariables"]
179
- outpath = "#{list_folder}/#{test_bundle_name}"
206
+ outpath = File.join(list_folder, test_bundle_name)
180
207
  test_host = replace_vars(test_bundle["TestHostPath"])
181
208
  test_bundle_path = replace_vars(test_bundle["TestBundlePath"], test_host)
182
209
  test_dumper_path = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'TestDumper', 'TestDumper.dylib'))
183
210
  unless File.exist?(test_dumper_path)
184
- warn "Could not find TestDumpber.dylib on #{test_dumper_path}"
185
- exit 1
211
+ raise TestDumpError, "Could not find TestDumper.dylib on #{test_dumper_path}"
186
212
  end
187
213
 
188
214
  is_logic_test = test_bundle["TestHostBundleIdentifier"].nil?
189
215
  env = simctl_child_attrs(
190
- "XCTEST_TYPE" => is_logic_test ? "LOGICTEST" : "APPTEST",
216
+ "XCTEST_TYPE" => xctest_type(test_bundle),
191
217
  "XCTEST_TARGET" => test_bundle_name,
192
218
  "TestDumperOutputPath" => outpath,
193
219
  "IDE_INJECTION_PATH" => testing_env_variables["DYLD_INSERT_LIBRARIES"],
@@ -206,7 +232,7 @@ module XCKnife
206
232
  )
207
233
  env.merge!(simctl_child_attrs(extra_environment_variables))
208
234
  inject_vars(env, test_host)
209
- FileUtils.remove(outpath) if File.exists?(outpath)
235
+ FileUtils.rm_f(outpath)
210
236
  logger.info { "Temporary TestDumper file for #{test_bundle_name} is #{outpath}" }
211
237
  if is_logic_test
212
238
  run_logic_test(env, test_host, test_bundle_path)
@@ -218,6 +244,46 @@ module XCKnife
218
244
  return TestSpecification.new outpath, discover_tests_to_skip(test_bundle)
219
245
  end
220
246
 
247
+ def list_tests_with_nm(list_folder, test_bundle, test_bundle_name)
248
+ output_methods(list_folder, test_bundle, test_bundle_name) do |test_bundle_path|
249
+ methods = []
250
+ swift_demangled_nm(test_bundle_path) do |output|
251
+ output.each_line do |line|
252
+ next unless method = method_from_nm_line(line)
253
+ methods << method
254
+ end
255
+ end
256
+ methods
257
+ end
258
+ end
259
+
260
+ def list_single_test(list_folder, test_bundle, test_bundle_name)
261
+ output_methods(list_folder, test_bundle, test_bundle_name) do
262
+ [{ class: test_bundle_name, method: 'test' }]
263
+ end
264
+ end
265
+
266
+ def output_methods(list_folder, test_bundle, test_bundle_name)
267
+ outpath = File.join(list_folder, test_bundle_name)
268
+ logger.info { "Writing out TestDumper file for #{test_bundle_name} to #{outpath}" }
269
+ test_specification = TestSpecification.new outpath, discover_tests_to_skip(test_bundle)
270
+
271
+ test_bundle_path = replace_vars(test_bundle["TestBundlePath"], replace_vars(test_bundle["TestHostPath"]))
272
+ methods = yield(test_bundle_path)
273
+
274
+ test_type = xctest_type(test_bundle)
275
+ File.open test_specification.json_stream_file, 'a' do |f|
276
+ f << JSON.dump(message: "Starting Test Dumper", event: "begin-test-suite", testType: test_type) << "\n"
277
+ f << JSON.dump(event: 'begin-ocunit', bundleName: File.basename(test_bundle_path), targetName: test_bundle_name) << "\n"
278
+ methods.map { |method| method[:class] }.uniq.each do |class_name|
279
+ f << JSON.dump(test: '1', className: class_name, event: "end-test", totalDuration: "0") << "\n"
280
+ end
281
+ f << JSON.dump(message: "Completed Test Dumper", event: "end-action", testType: test_type) << "\n"
282
+ end
283
+
284
+ test_specification
285
+ end
286
+
221
287
  def discover_tests_to_skip(test_bundle)
222
288
  identifier_for_test_method = "/"
223
289
  skip_test_identifiers = test_bundle["SkipTestIdentifiers"] || []
@@ -231,7 +297,7 @@ module XCKnife
231
297
  def replace_vars(str, testhost = "<UNKNOWN>")
232
298
  str.gsub("__PLATFORMS__", @platforms_path).
233
299
  gsub("__TESTHOST__", testhost).
234
- gsub("__TESTROOT__", @testroot)
300
+ gsub("__TESTROOT__", testroot)
235
301
  end
236
302
 
237
303
  def inject_vars(env, test_host)
@@ -247,9 +313,19 @@ module XCKnife
247
313
  end
248
314
 
249
315
  def install_app(test_host_path)
250
- until system("#{simctl} install #{@device_id} '#{test_host_path}'")
251
- sleep 0.1
316
+ retries_count = 0
317
+ max_retry_count = 3
318
+ until (retries_count > max_retry_count) or call_simctl(["install", @device_id, test_host_path])
319
+ retries_count += 1
320
+ call_simctl ['shutdown', @device_id]
321
+ call_simctl ['boot', @device_id]
322
+ sleep 1.0
252
323
  end
324
+
325
+ if retries_count > max_retry_count
326
+ raise TestDumpError, "Installing #{test_host_path} failed"
327
+ end
328
+
253
329
  end
254
330
 
255
331
  def wait_test_dumper_completion(file)
@@ -257,8 +333,7 @@ module XCKnife
257
333
  until has_test_dumper_terminated?(file) do
258
334
  retries_count += 1
259
335
  if retries_count == @max_retry_count
260
- puts "Timeout error on: #{file}"
261
- exit 1
336
+ raise TestDumpError, "Timeout error on: #{file}"
262
337
  end
263
338
  sleep 0.1
264
339
  end
@@ -267,29 +342,71 @@ module XCKnife
267
342
  def has_test_dumper_terminated?(file)
268
343
  return false unless File.exists?(file)
269
344
  last_line = `tail -n 1 "#{file}"`
270
- return /Completed Test Dumper/.match(last_line)
345
+ return last_line.include?("Completed Test Dumper")
271
346
  end
272
347
 
273
348
  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}'"
349
+ unless call_simctl(["launch", @device_id, test_host_bundle_identifier, '-XCTest', 'All', dylib_logfile_path, test_bundle_path], env: env)
350
+ raise TestDumpError, "Launching #{test_bundle_path} in #{test_host_bundle_identifier} failed"
351
+ end
275
352
  end
276
353
 
277
354
  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 '' if @debug
283
- ' 2> /dev/null'
355
+ opts = @debug ? {} : { err: "/dev/null" }
356
+ unless call_simctl(["spawn", @device_id, test_host, '-XCTest', 'All', dylib_logfile_path, test_bundle_path], env: env, **opts)
357
+ raise TestDumpError, "Spawning #{test_bundle_path} in #{test_host} failed"
358
+ end
284
359
  end
285
360
 
286
- def call_simctl(env, string_args)
287
- cmd = "#{simctl} #{string_args}"
361
+ def call_simctl(args, env: {}, **spawn_opts)
362
+ args = [simctl] + args
363
+ cmd = Shellwords.shelljoin(args)
288
364
  puts "Running:\n$ #{cmd}"
289
365
  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}"
366
+
367
+ ret = system(env, *args, **spawn_opts)
368
+ puts "Simctl errored with the following env:\n #{env.pretty_print_inspect}" unless ret
369
+ ret
370
+ end
371
+
372
+ def dylib_logfile_path
373
+ if @dylib_logfile_path then
374
+ @dylib_logfile_path
375
+ else
376
+ '/tmp/xcknife_testdumper_dylib.log'
377
+ end
378
+ end
379
+
380
+ def xctest_type(test_bundle)
381
+ if test_bundle["TestHostBundleIdentifier"].nil?
382
+ "LOGICTEST"
383
+ else
384
+ "APPTEST"
292
385
  end
293
386
  end
387
+
388
+ def swift_demangled_nm(test_bundle_path)
389
+ Open3.pipeline_r([@nm_path, File.join(test_bundle_path, File.basename(test_bundle_path, '.xctest'))], [@swift_path, 'demangle']) do |o, _ts|
390
+ yield(o)
391
+ end
392
+ end
393
+
394
+ def method_from_nm_line(line)
395
+ return unless line.strip =~ %r{^
396
+ [\da-f]+\s # address
397
+ [tT]\s # symbol type
398
+ (?: # method
399
+ -\[(.+)\s(test.+)\] # objc instance method
400
+ | # or swift instance method
401
+ _? # only present on Xcode 10.0 and below
402
+ (?:@objc\s)? # optional objc annotation
403
+ (?:[^\.]+\.)? # module name
404
+ (.+) # class name
405
+ \.(test.+)\s->\s\(\) # method signature
406
+ )
407
+ $}ox
408
+
409
+ { class: $1 || $3, method: $2 || $4 }
410
+ end
294
411
  end
295
412
  end
@@ -25,12 +25,24 @@ module XCKnife
25
25
  end
26
26
 
27
27
  # only-testing is available since Xcode 8
28
- def xcodebuild_only_arguments(single_partition)
29
- 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
+
30
40
  classes.sort.map do |clazz|
31
41
  "-only-testing:#{test_target}/#{clazz}"
32
42
  end
33
- end
43
+ end.sort
44
+
45
+ only_target_arguments + only_class_arguments
34
46
  end
35
47
 
36
48
  # skip-testing is available since Xcode 8
@@ -50,4 +62,4 @@ module XCKnife
50
62
  partition_set.map { |partition| xctool_only_arguments(partition) }
51
63
  end
52
64
  end
53
- 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.6
4
+ version: 0.10.0
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-03-06 00:00:00.000000000 Z
11
+ date: 2019-03-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -38,13 +38,16 @@ extensions: []
38
38
  extra_rdoc_files: []
39
39
  files:
40
40
  - ".gitignore"
41
- - ".gitmodules"
41
+ - ".hubkins"
42
42
  - ".rspec"
43
43
  - ".ruby-version"
44
+ - ".sqiosbuild.json"
44
45
  - ".travis.yml"
45
46
  - CONTRIBUTING.md
46
47
  - Gemfile
48
+ - Gemfile.lock
47
49
  - LICENSE
50
+ - OWNERS.yml
48
51
  - README.md
49
52
  - Rakefile
50
53
  - TestDumper/README.md
@@ -75,8 +78,7 @@ files:
75
78
  homepage: https://github.com/square/xcknife
76
79
  licenses:
77
80
  - Apache-2.0
78
- metadata:
79
- allowed_push_host: https://rubygems.org
81
+ metadata: {}
80
82
  post_install_message:
81
83
  rdoc_options: []
82
84
  require_paths:
@@ -93,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
95
  version: '0'
94
96
  requirements: []
95
97
  rubyforge_project:
96
- rubygems_version: 2.4.5.1
98
+ rubygems_version: 2.7.4
97
99
  signing_key:
98
100
  specification_version: 4
99
101
  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