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 +5 -5
- data/.gitignore +2 -1
- data/.hubkins +5 -0
- data/.ruby-version +1 -1
- data/.sqiosbuild.json +6 -0
- data/Gemfile +4 -1
- data/Gemfile.lock +41 -0
- data/OWNERS.yml +2 -0
- data/Rakefile +3 -2
- data/TestDumper/TestDumper/Initialize.m +69 -26
- data/lib/xcknife.rb +1 -1
- data/lib/xcknife/exceptions.rb +4 -2
- data/lib/xcknife/stream_parser.rb +48 -14
- data/lib/xcknife/test_dumper.rb +161 -44
- data/lib/xcknife/xctool_cmd_helper.rb +16 -4
- data/xcknife.gemspec +0 -3
- metadata +8 -6
- data/.gitmodules +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: aca898703e491f17c9923c8c7e28496e0cbac2e2ad1f9b82ae1a632142bacad9
|
4
|
+
data.tar.gz: ea35d7ce95bd76c7d5011683047245a457668acf2b49c070808e99eb06be6da3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 548661d771e7e367e9fd4cee9687761ecd8b1a504702816e096efd1a0b41c60454deda107f56e73096bd851b20d054931ef3b8605c90c87948d4af59aecad157
|
7
|
+
data.tar.gz: 052d69c88e71f0ad388c676ee3f6f374c814b1acec74c090f27acb54541db02f189d3e18bf8e578947b5fa227931bbcfd9aa16ac087e0aee78652c9032c5441a
|
data/.gitignore
CHANGED
data/.hubkins
ADDED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.3.1
|
data/.sqiosbuild.json
ADDED
data/Gemfile
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
-
source 'https://
|
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
|
data/Gemfile.lock
ADDED
@@ -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
|
data/OWNERS.yml
ADDED
data/Rakefile
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'rspec/core/rake_task'
|
2
|
-
require
|
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
|
-
|
146
|
-
|
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 = [
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
|
data/lib/xcknife.rb
CHANGED
data/lib/xcknife/exceptions.rb
CHANGED
@@ -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
|
-
|
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.
|
108
|
-
|
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
|
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
|
-
|
144
|
-
|
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]
|
173
|
+
assignemnt.test_time_map[test_target].concat class_names
|
147
174
|
assignemnt.total_time += duration_in_milliseconds
|
148
175
|
end
|
149
|
-
|
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
|
|
data/lib/xcknife/test_dumper.rb
CHANGED
@@ -5,6 +5,9 @@ require 'tmpdir'
|
|
5
5
|
require 'ostruct'
|
6
6
|
require 'set'
|
7
7
|
require 'logger'
|
8
|
+
require 'shellwords'
|
9
|
+
require 'open3'
|
10
|
+
require 'xcknife/exceptions'
|
8
11
|
|
9
12
|
module XCKnife
|
10
13
|
class TestDumper
|
@@ -14,21 +17,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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
@
|
150
|
-
@
|
151
|
-
@
|
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 =
|
161
|
-
xctestrun_file = Dir[
|
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
|
-
|
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)
|
169
|
-
|
170
|
-
|
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
|
-
|
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 =
|
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
|
-
|
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" =>
|
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.
|
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__",
|
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
|
-
|
251
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
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,
|
287
|
-
|
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
|
-
|
291
|
-
|
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
|
-
|
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
|
data/xcknife.gemspec
CHANGED
@@ -17,9 +17,6 @@ Gem::Specification.new do |s|
|
|
17
17
|
Works by leveraging xctool's json-streams timing and test data.
|
18
18
|
DESCRIPTION
|
19
19
|
|
20
|
-
# Only allow gem to be pushed to https://rubygems.org
|
21
|
-
s.metadata["allowed_push_host"] = 'https://rubygems.org'
|
22
|
-
|
23
20
|
s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR).reject { |f| f =~ /^spec/} + ["TestDumper/TestDumper.dylib"]
|
24
21
|
s.bindir = 'bin'
|
25
22
|
s.executables = ['xcknife', 'xcknife-min', 'xcknife-test-dumper']
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: xcknife
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.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:
|
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
|
-
- ".
|
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
|
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
|
data/.gitmodules
DELETED