xcknife 0.1.0 → 0.5.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
2
  SHA1:
3
- metadata.gz: ef0dca6ea2940fdd267c0d6a1343c6cca06cd061
4
- data.tar.gz: 743037f45ece209c60620bda130cd52554370a0c
3
+ metadata.gz: 0b6205c8d36ae8e54613311d15ca1819c311e091
4
+ data.tar.gz: fee83a2de8da43399901a1b1dd90185b6dcbf8e1
5
5
  SHA512:
6
- metadata.gz: 5739e9f4a633136067a27dca08c1c383e73d166cc20cf0d55e36e9d2a63dc23160c58aca97eb5f0dc5a506fffa446b850f371166966d181e6efa38ce8bc137eb
7
- data.tar.gz: e73093a4a3ddaedc77868f545e0aecf66496940aac1782d4bc0280f6148171c39d2848a104dd3668003f6aa1360bdb6f4a565cda9ef89ff85f054576e863c5de
6
+ metadata.gz: 938970059eaeb4463ee976b0b22a05b0912098817f007dc22e2ac1401fb7dac8fc148ed8c4c1da8bbb99dfd82951fcf51d19873d8d14c116f38b695d3f554667
7
+ data.tar.gz: 9bf514e7e67fdf21ddbf358bdf9fc58569a0eef1e25a0c4715aa6524b0cfdae0fa14214db30eee8c50e880b366905686316d53f6acf3ae9a85d1eb27648d6de9
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,15 @@
1
+ ### Sign the CLA
2
+
3
+ Any contributors to the master *xcknife* repository must sign the
4
+ [Individual Contributor License Agreement (CLA)]. It's a short form that covers
5
+ our bases and makes sure you're eligible to contribute.
6
+
7
+ ### Submitting a Pull Request
8
+
9
+ When you have a change you'd like to see in the master repository, send a
10
+ [pull request]. Before we merge your request, we'll make sure you're in the list
11
+ of people who have signed a CLA.
12
+
13
+ [Individual Contributor License Agreement (CLA)]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1
14
+ [pull request]: https://github.com/square/xcknife/pulls
15
+
data/README.md CHANGED
@@ -1 +1,218 @@
1
- WIP
1
+ # XCKnife
2
+ [![Apache 2 licensed](https://img.shields.io/badge/license-Apache2-blue.svg)](https://github.com/square/xcknife/blob/master/LICENSE)
3
+
4
+ XCKnife is a tool that partitions XCTestCase tests in a way that minimizes total execution time (particularly useful for distributed test executions).
5
+
6
+ It works by leveraging [xctool's](https://github.com/facebook/xctool) [json-stream](https://github.com/facebook/xctool#included-reporters) reporter output.
7
+
8
+ XCKnife generates a list of only arguments meant to be pass to Xctool's [*-only* test arguments](https://github.com/facebook/xctool#testing), but alternatively could used to generate multiple xcschemes with the proper test partitions.
9
+
10
+ ## Install
11
+
12
+ `$ gem install xcknife`
13
+
14
+ ## Using as command line tool
15
+
16
+ ```
17
+ $ xcknife -h
18
+ Usage: xcknife [options] worker-count historical-timings-json-stream-file [current-tests-json-stream-file]
19
+ -p, --partition TARGETS Comma separated list of targets. Can be used multiple times
20
+ -o, --output FILENAME Output file. Defaults to STDOUT
21
+ -a, --abbrev Results are abbreviated
22
+ -h, --help Show this message
23
+ ```
24
+
25
+ ## Example
26
+
27
+ The data provided on the [example](https://github.com/square/xcknife/tree/master/example) folder:
28
+
29
+ `$ xcknife -p iPhoneTestTarget 3 example/xcknife-exemplar-historical-data.json-stream example/xcknife-exemplar.json-stream`
30
+
31
+ This will balance the tests onthe `iPhoneTestTarget` into 3 machines. The output is:
32
+
33
+ ```json
34
+ {
35
+ "metadata": {
36
+ "worker_count": 3,
37
+ "partition_set_count": 1,
38
+ "total_time_in_ms": 910,
39
+ "historical_total_tests": 5,
40
+ "current_total_tests": 5,
41
+ "class_extrapolations": 0,
42
+ "target_extrapolations": 0
43
+ },
44
+ "partition_set_data": [
45
+ {
46
+ "partition_set": "iPhoneTestTarget",
47
+ "size": 3,
48
+ "imbalance_ratio": 1.0,
49
+ "partitions": [
50
+ {
51
+ "shard_number": 1,
52
+ "cli_arguments": [ "-only", "iPhoneTestTarget:iPhoneTestClassGama" ],
53
+ "partition_imbalance_ratio": 0.9923076923076923
54
+ },
55
+ {
56
+ "shard_number": 2,
57
+ "cli_arguments": [ "-only", "iPhoneTestTarget:iPhoneTestClassAlpha,iPhoneTestClassDelta" ],
58
+ "partition_imbalance_ratio": 1.0054945054945055
59
+ },
60
+ {
61
+ "shard_number": 3,
62
+ "cli_arguments": [ "-only", "iPhoneTestTarget:iPhoneTestClassBeta,iPhoneTestClassOmega" ],
63
+ "partition_imbalance_ratio": 1.0021978021978022
64
+ }]}]}
65
+ ```
66
+
67
+ This provides a lot of data about the partitions and their imabalances (both internal to the partition sets, and amongst them).
68
+
69
+ If you only want the *-only* arguments, run with the `-a` flag:
70
+
71
+ `$ xcknife -p iPhoneTestTarget 3 example/xcknife-exemplar-historical-data.json-stream example/xcknife-exemplar.json-stream -a`
72
+
73
+ outputing:
74
+
75
+ ```json
76
+ [
77
+ [
78
+ [
79
+ "-only",
80
+ "iPhoneTestTarget:iPhoneTestClassGama"
81
+ ],
82
+ [
83
+ "-only",
84
+ "iPhoneTestTarget:iPhoneTestClassAlpha,iPhoneTestClassDelta"
85
+ ],
86
+ [
87
+ "-only",
88
+ "iPhoneTestTarget:iPhoneTestClassBeta,iPhoneTestClassOmega"
89
+ ]
90
+ ]
91
+ ]
92
+ ```
93
+
94
+ ## Example: Multiple partition Sets
95
+
96
+ You can pass the partition flag mutliple times, so that XCKnife will do two level partiotioning: inside each partition, and then for all partitions.
97
+
98
+ This is useful if each partition is tested against multiple devices, simulator versions or configurations. On the following example picture `CommonTestTarget` being tested against iPhones only, while `CommonTestTarget,iPadTestTarget` is tested against iPads.
99
+
100
+ `$ xcknife -p CommonTestTarget -p CommonTestTarget,iPadTestTarget 6 example/xcknife-exemplar-historical-data.json-stream example/xcknife-exemplar.json-stream`
101
+
102
+ This will balance two partition sets: `CommonTestTarget` and `CommonTestTarget,iPadTestTarget` into 6 machines. The output is:
103
+
104
+ ```json
105
+ {
106
+ "metadata": {
107
+ "worker_count": 6,
108
+ "partition_set_count": 2,
109
+ "total_time_in_ms": 8733,
110
+ "historical_total_tests": 6,
111
+ "current_total_tests": 7,
112
+ "class_extrapolations": 1,
113
+ "target_extrapolations": 0
114
+ },
115
+ "partition_set_data": [
116
+ {
117
+ "partition_set": "CommonTestTarget",
118
+ "size": 1,
119
+ "imbalance_ratio": 0.5480554313813143,
120
+ "partitions": [
121
+ {
122
+ "shard_number": 1,
123
+ "cli_arguments": [
124
+ "-only",
125
+ "CommonTestTarget:CommonTestClass"
126
+ ],
127
+ "partition_imbalance_ratio": 1.0
128
+ }
129
+ ]
130
+ },
131
+ {
132
+ "partition_set": "CommonTestTarget,iPadTestTarget",
133
+ "size": 5,
134
+ "imbalance_ratio": 1.4519445686186858,
135
+ "partitions": [
136
+ {
137
+ "shard_number": 2,
138
+ "cli_arguments": [
139
+ "-only",
140
+ "iPadTestTarget:iPadTestClassTwo"
141
+ ],
142
+ "partition_imbalance_ratio": 3.0800492610837438
143
+ },
144
+ {
145
+ "shard_number": 3,
146
+ "cli_arguments": [
147
+ "-only",
148
+ "iPadTestTarget:iPadTestClassOne"
149
+ ],
150
+ "partition_imbalance_ratio": 0.6169950738916257
151
+ },
152
+ {
153
+ "shard_number": 4,
154
+ "cli_arguments": [
155
+ "-only",
156
+ "iPadTestTarget:iPadTestClassFour"
157
+ ],
158
+ "partition_imbalance_ratio": 0.6169950738916257
159
+ },
160
+ {
161
+ "shard_number": 5,
162
+ "cli_arguments": [
163
+ "-only",
164
+ "CommonTestTarget:CommonTestClass"
165
+ ],
166
+ "partition_imbalance_ratio": 0.3774630541871921
167
+ },
168
+ {
169
+ "shard_number": 6,
170
+ "cli_arguments": [
171
+ "-only",
172
+ "iPadTestTarget:iPadTestClassThree"
173
+ ],
174
+ "partition_imbalance_ratio": 0.30849753694581283
175
+ }
176
+ ]
177
+ }
178
+ ]
179
+ }
180
+ ```
181
+
182
+ ## Using as Ruby gem
183
+
184
+ Described [here](https://github.com/square/xcknife/tree/master/example).
185
+
186
+ ## Minimizing json-stream files
187
+
188
+ XCKnife uses only a few attributes of a json-stream file. If you are storing the files in repository, you may want to remove unecessary data with `xcknife-min`. For example:
189
+
190
+ `$ xcknife-min example/xcknife-exemplar-historical-data.json-stream minified.json-stream`
191
+
192
+ ## Contributing
193
+
194
+ Any contributors to the master *xcknife* repository must sign the
195
+ [Individual Contributor License Agreement (CLA)]. It's a short form that covers
196
+ our bases and makes sure you're eligible to contribute.
197
+
198
+ When you have a change you'd like to see in the master repository, send a
199
+ [pull request]. Before we merge your request, we'll make sure you're in the list
200
+ of people who have signed a CLA.
201
+
202
+ [Individual Contributor License Agreement (CLA)]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1
203
+ [pull request]: https://github.com/square/xcknife/pulls
204
+
205
+
206
+ ## License
207
+
208
+ Copyright 2016 Square Inc.
209
+
210
+ Licensed under the Apache License, Version 2.0 (the "License");
211
+ you may not use this file except in compliance with the License.
212
+ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
213
+
214
+ Unless required by applicable law or agreed to in writing, software
215
+ distributed under the License is distributed on an "AS IS" BASIS,
216
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
217
+ See the License for the specific language governing permissions and
218
+ limitations under the License.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'rspec/core/rake_task'
2
+ require "bundler/gem_tasks"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
data/bin/xcknife ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'xcknife'
3
+
4
+ XCKnife::Runner.invoke
data/bin/xcknife-min ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ # Script for cleaning up historical events, so that we only store the ones we need
5
+ require 'set'
6
+ require 'json' unless defined?(::JSON)
7
+
8
+ INTERESTING_EVENTS = %w[begin-ocunit end-test].to_set
9
+ def cleanup(input_file_name, output_file_name)
10
+ if input_file_name.nil? or output_file_name.nil?
11
+ return puts "Usage: xcknife_clean [input] [output]"
12
+ end
13
+ lines = IO.readlines(input_file_name)
14
+ lines_written = 0
15
+ total = lines.size
16
+ File.open(output_file_name, "w") do |f|
17
+ lines.each do |line|
18
+ data = JSON.load(line)
19
+ if INTERESTING_EVENTS.include?(data["event"])
20
+ lines_written += 1
21
+ %w[output sdk timestamp exceptions result succeeded methodName].each do |k|
22
+ data.delete(k)
23
+ end
24
+ if data["test"]
25
+ data["test"] = "1"
26
+ end
27
+ f.puts(data.to_json)
28
+ end
29
+ end
30
+ end
31
+ lines_removed = total - lines_written
32
+ percent = (100.0 * lines_removed / total).round(2)
33
+ puts "Done. Removed #{lines_removed} lines (#{percent}%) out of #{total}"
34
+ puts "Written new json-stream file to: #{output_file_name}"
35
+ end
36
+
37
+ input_file_name, output_file_name = ARGV
38
+ cleanup input_file_name, output_file_name
data/example/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # Example
2
+
3
+ Data gathered from the [xcknife-exemplar sample project](https://github.com/danielribeiro/xcknife-exemplar).
4
+
5
+ To run the example run:
6
+
7
+ ```
8
+ $ ruby run_example.rb
9
+ ```
10
+
11
+ This is equivalent to the example of:
12
+
13
+ ```
14
+ $ xcknife -p CommonTestTarget -p CommonTestTarget,iPadTestTarget 6 example/xcknife-exemplar-historical-data.json-stream example/xcknife-exemplar.json-stream
15
+ ```
@@ -0,0 +1,34 @@
1
+ require_relative '../lib/xcknife'
2
+ require 'pp'
3
+
4
+ # Gem usage of xcknife. Functionaly equivalent to
5
+ # $ xcknife -p CommonTestTarget -p CommonTestTarget,iPadTestTarget 6 example/xcknife-exemplar-historical-data.json-stream example/xcknife-exemplar.json-stream
6
+ include XCKnife::XCToolCmdHelper
7
+ TARGET_PARTITIONS = {
8
+ "AllTests" => ["CommonTestTarget", "iPadTestTarget"],
9
+ "OnlyCommon" => ["CommonTestTarget"]
10
+ }
11
+
12
+ def run(historical_file, current_file)
13
+ test_target_names = TARGET_PARTITIONS.keys
14
+ knife = XCKnife::StreamParser.new(6, TARGET_PARTITIONS.values)
15
+ result = knife.compute_shards_for_file(historical_file, current_file)
16
+ partition_sets = result.test_maps
17
+ puts "total = #{result.total_test_time}"
18
+ puts "test times = #{result.test_times.inspect}"
19
+ puts "stats = #{result.stats.to_h.pretty_inspect}"
20
+ puts "imbalances = #{result.test_time_imbalances.to_h.inspect}"
21
+ shard_number = 0
22
+ puts "size = #{partition_sets.size}"
23
+ puts "sizes = #{partition_sets.map(&:size).join(", ")}"
24
+ partition_sets.each_with_index do |partition_set, i|
25
+ target_name = test_target_names[i]
26
+ partition_set.each do |partition|
27
+ puts "target name for worker #{shard_number} = #{target_name}"
28
+ puts "only is: #{xctool_only_arguments(partition).inspect}"
29
+ shard_number += 1
30
+ end
31
+ end
32
+ end
33
+
34
+ run("xcknife-exemplar-historical-data.json-stream", "xcknife-exemplar.json-stream")
@@ -0,0 +1,14 @@
1
+ {"bundleName":"CommonTestTarget.xctest","deviceName":"iPad Air","event":"begin-ocunit","targetName":"CommonTestTarget","testType":"logic-test","sdkName":"iphonesimulator9.2"}
2
+ {"test":"1","className":"CommonTestClass","event":"end-test","totalDuration":0.1075069904327393}
3
+ {"test":"1","className":"CommonTestClass","event":"end-test","totalDuration":0.303464949131012}
4
+ {"test":"1","className":"CommonTestClass","event":"end-test","totalDuration":0.2003870010375977}
5
+ {"bundleName":"iPadTestTarget.xctest","deviceName":"iPad Air","event":"begin-ocunit","targetName":"iPadTestTarget","testType":"application-test","sdkName":"iphonesimulator9.2"}
6
+ {"test":"1","className":"iPadTestClassOne","event":"end-test","totalDuration":1.001249969005585}
7
+ {"test":"1","className":"iPadTestClassThree","event":"end-test","totalDuration":0.5002140402793884}
8
+ {"test":"1","className":"iPadTestClassTwo","event":"end-test","totalDuration":5.001157999038696}
9
+ {"bundleName":"iPhoneTestTarget.xctest","deviceName":"iPad Air","event":"begin-ocunit","targetName":"iPhoneTestTarget","testType":"application-test","sdkName":"iphonesimulator9.2"}
10
+ {"test":"1","className":"iPhoneTestClassAlpha","event":"end-test","totalDuration":0.2037490010261536}
11
+ {"test":"1","className":"iPhoneTestClassBeta","event":"end-test","totalDuration":0.2012439966201782}
12
+ {"test":"1","className":"iPhoneTestClassDelta","event":"end-test","totalDuration":0.1004489660263062}
13
+ {"test":"1","className":"iPhoneTestClassGama","event":"end-test","totalDuration":0.3001730442047119}
14
+ {"test":"1","className":"iPhoneTestClassOmega","event":"end-test","totalDuration":0.101157009601593}
@@ -0,0 +1,15 @@
1
+ {"bundleName":"iPadTestTarget.xctest","deviceName":"iPad Air","event":"begin-ocunit","targetName":"iPadTestTarget","testType":"application-test","sdkName":"iphonesimulator9.2"}
2
+ {"test":"1","className":"iPadTestClassFour","event":"end-test","totalDuration":"0"}
3
+ {"test":"1","className":"iPadTestClassOne","event":"end-test","totalDuration":"0"}
4
+ {"test":"1","className":"iPadTestClassThree","event":"end-test","totalDuration":"0"}
5
+ {"test":"1","className":"iPadTestClassTwo","event":"end-test","totalDuration":"0"}
6
+ {"bundleName":"iPhoneTestTarget.xctest","deviceName":"iPad Air","event":"begin-ocunit","targetName":"iPhoneTestTarget","testType":"application-test","sdkName":"iphonesimulator9.2"}
7
+ {"test":"1","className":"iPhoneTestClassAlpha","event":"end-test","totalDuration":"0"}
8
+ {"test":"1","className":"iPhoneTestClassBeta","event":"end-test","totalDuration":"0"}
9
+ {"test":"1","className":"iPhoneTestClassDelta","event":"end-test","totalDuration":"0"}
10
+ {"test":"1","className":"iPhoneTestClassGama","event":"end-test","totalDuration":"0"}
11
+ {"test":"1","className":"iPhoneTestClassOmega","event":"end-test","totalDuration":"0"}
12
+ {"bundleName":"CommonTestTarget.xctest","deviceName":"iPad Air","event":"begin-ocunit","targetName":"CommonTestTarget","testType":"logic-test","sdkName":"iphonesimulator9.2"}
13
+ {"test":"1","className":"CommonTestClass","event":"end-test","totalDuration":"0"}
14
+ {"test":"1","className":"CommonTestClass","event":"end-test","totalDuration":"0"}
15
+ {"test":"1","className":"CommonTestClass","event":"end-test","totalDuration":"0"}
data/lib/xcknife.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'xcknife/events_analyzer'
2
+ require 'xcknife/stream_parser'
3
+ require 'xcknife/xctool_cmd_helper'
4
+ require 'xcknife/runner'
5
+ require 'xcknife/exceptions'
6
+
7
+ module XCKnife
8
+ VERSION = '0.5.0'
9
+ end
@@ -0,0 +1,55 @@
1
+ require 'xcknife/json_stream_parser_helper'
2
+ require 'set'
3
+
4
+ module XCKnife
5
+ class EventsAnalyzer
6
+ include JsonStreamParserHelper
7
+ attr_reader :target_class_map, :total_tests
8
+
9
+ def self.for(events, relevant_partitions)
10
+ return NullEventsAnalyzer.new if events.nil?
11
+ new(events, relevant_partitions)
12
+ end
13
+
14
+ def initialize(events, relevant_partitions)
15
+ @total_tests = 0
16
+ @relevant_partitions = relevant_partitions
17
+ @target_class_map = analyze_events(events)
18
+ end
19
+
20
+ def has_test_target?(target)
21
+ target_class_map.has_key?(target)
22
+ end
23
+
24
+ def has_test_class?(target, clazz)
25
+ has_test_target?(target) and target_class_map[target].include?(clazz)
26
+ end
27
+
28
+ private
29
+ def analyze_events(events)
30
+ ret = Hash.new { |h, key| h[key] = Set.new }
31
+ each_test_event(events) do |target_name, result|
32
+ next unless @relevant_partitions.include?(target_name)
33
+ @total_tests += 1
34
+ ret[target_name] << result.className
35
+ end
36
+ ret
37
+ end
38
+ end
39
+
40
+ # Null object for EventsAnalyzer
41
+ # @ref https://en.wikipedia.org/wiki/Null_Object_pattern
42
+ class NullEventsAnalyzer
43
+ def has_test_target?(target)
44
+ true
45
+ end
46
+
47
+ def has_test_class?(target, clazz)
48
+ true
49
+ end
50
+
51
+ def total_tests
52
+ 0
53
+ end
54
+ end
55
+ end