xcknife 0.6.6 → 0.10.0

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: 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