xcknife 0.1.0 → 0.5.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
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