minitest-distributed 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ruby.yml +48 -0
  3. data/.gitignore +8 -0
  4. data/.rubocop.yml +63 -0
  5. data/.travis.yml +6 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +12 -0
  8. data/Gemfile.lock +53 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +115 -0
  11. data/Rakefile +12 -0
  12. data/bin/console +15 -0
  13. data/bin/rake +29 -0
  14. data/bin/rubocop +29 -0
  15. data/bin/setup +8 -0
  16. data/bin/srb +29 -0
  17. data/lib/minitest/distributed.rb +36 -0
  18. data/lib/minitest/distributed/configuration.rb +53 -0
  19. data/lib/minitest/distributed/coordinators/coordinator_interface.rb +29 -0
  20. data/lib/minitest/distributed/coordinators/memory_coordinator.rb +67 -0
  21. data/lib/minitest/distributed/coordinators/redis_coordinator.rb +387 -0
  22. data/lib/minitest/distributed/enqueued_runnable.rb +88 -0
  23. data/lib/minitest/distributed/filters/exclude_filter.rb +35 -0
  24. data/lib/minitest/distributed/filters/filter_interface.rb +25 -0
  25. data/lib/minitest/distributed/filters/include_filter.rb +35 -0
  26. data/lib/minitest/distributed/reporters/distributed_progress_reporter.rb +76 -0
  27. data/lib/minitest/distributed/reporters/distributed_summary_reporter.rb +48 -0
  28. data/lib/minitest/distributed/reporters/redis_coordinator_warnings_reporter.rb +61 -0
  29. data/lib/minitest/distributed/result_aggregate.rb +67 -0
  30. data/lib/minitest/distributed/result_type.rb +28 -0
  31. data/lib/minitest/distributed/test_runner.rb +37 -0
  32. data/lib/minitest/distributed/test_selector.rb +54 -0
  33. data/lib/minitest/distributed/version.rb +8 -0
  34. data/lib/minitest/distributed_plugin.rb +51 -0
  35. data/minitest-distributed.gemspec +50 -0
  36. data/sorbet/config +2 -0
  37. data/sorbet/rbi/minitest.rbi +238 -0
  38. data/sorbet/rbi/rbconfig.rbi +6 -0
  39. data/sorbet/rbi/redis.rbi +70 -0
  40. data/sorbet/rbi/winsize.rbi +7 -0
  41. metadata +142 -0
@@ -0,0 +1,37 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Minitest
5
+ module Distributed
6
+ class TestRunner
7
+ extend T::Sig
8
+
9
+ sig { returns(T::Hash[Symbol, T.untyped]) }
10
+ attr_reader :options
11
+
12
+ sig { returns(Configuration) }
13
+ attr_reader :configuration
14
+
15
+ sig { returns(TestSelector) }
16
+ attr_reader :test_selector
17
+
18
+ sig { returns(Coordinators::CoordinatorInterface) }
19
+ attr_reader :coordinator
20
+
21
+ sig { params(options: T::Hash[Symbol, T.untyped]).void }
22
+ def initialize(options)
23
+ @options = options
24
+
25
+ @configuration = T.let(@options[:distributed], Configuration)
26
+ @coordinator = T.let(configuration.coordinator, Coordinators::CoordinatorInterface)
27
+ @test_selector = T.let(TestSelector.new(options), TestSelector)
28
+ end
29
+
30
+ sig { params(reporter: AbstractReporter).void }
31
+ def run(reporter)
32
+ coordinator.produce(test_selector: test_selector)
33
+ coordinator.consume(reporter: reporter)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,54 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Minitest
5
+ module Distributed
6
+ class TestSelector
7
+ extend T::Sig
8
+
9
+ sig { returns(T::Hash[Symbol, T.untyped]) }
10
+ attr_reader :options
11
+
12
+ sig { returns(T::Array[Filters::FilterInterface]) }
13
+ attr_reader :filters
14
+
15
+ sig { params(options: T::Hash[Symbol, T.untyped]).void }
16
+ def initialize(options)
17
+ @options = options
18
+
19
+ @filters = T.let([], T::Array[Filters::FilterInterface])
20
+ initialize_filters
21
+ end
22
+
23
+ sig { void }
24
+ def initialize_filters
25
+ @filters << Filters::IncludeFilter.new(options[:filter]) if options[:filter]
26
+ @filters << Filters::ExcludeFilter.new(options[:exclude]) if options[:exclude]
27
+ end
28
+
29
+ sig { returns(T::Array[EnqueuedRunnable]) }
30
+ def discover_tests
31
+ Minitest::Runnable.runnables.flat_map do |runnable|
32
+ runnable.runnable_methods.map do |method_name|
33
+ EnqueuedRunnable.new(class_name: runnable.name, method_name: method_name)
34
+ end
35
+ end
36
+ end
37
+
38
+ sig { params(tests: T::Array[EnqueuedRunnable]).returns(T::Array[EnqueuedRunnable]) }
39
+ def select_tests(tests)
40
+ return tests if filters.empty?
41
+ tests.flat_map do |runnable_method|
42
+ filters.flat_map do |filter|
43
+ filter.call(runnable_method)
44
+ end
45
+ end.compact
46
+ end
47
+
48
+ sig { returns(T::Array[EnqueuedRunnable]) }
49
+ def tests
50
+ select_tests(discover_tests)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,8 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Minitest
5
+ module Distributed
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
@@ -0,0 +1,51 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require_relative './distributed'
5
+
6
+ module Minitest
7
+ class << self
8
+ extend T::Sig
9
+
10
+ def plugin_distributed_options(opts, options)
11
+ options[:distributed] = Minitest::Distributed::Configuration.from_env
12
+
13
+ opts.on('--coordinator=URI', "The URI pointing to the coordinator") do |uri|
14
+ options[:distributed].coordinator_uri = URI.parse(uri)
15
+ end
16
+
17
+ opts.on('--test-timeout=TIMEOUT', "The maximum run time for a single test in seconds") do |timeout|
18
+ options[:distributed].test_timeout = Integer(timeout)
19
+ end
20
+
21
+ opts.on('--max-attempts=ATTEMPTS', "The maximum number of attempts to run a test") do |attempts|
22
+ options[:distributed].max_attempts = Integer(attempts)
23
+ end
24
+
25
+ opts.on('--test-batch-size=NUMBER', "The number of tests to process per batch") do |batch_size|
26
+ options[:distributed].test_batch_size = Integer(batch_size)
27
+ end
28
+
29
+ opts.on('--run-id=ID', "The ID for this run shared between coordinated workers") do |id|
30
+ options[:distributed].run_id = id
31
+ end
32
+
33
+ opts.on('--worker-id=ID', "The unique ID for this worker") do |id|
34
+ options[:distributed].worker_id = id
35
+ end
36
+ end
37
+
38
+ def plugin_distributed_init(options)
39
+ Minitest.singleton_class.prepend(Minitest::Distributed::TestRunnerPatch)
40
+ options[:distributed].coordinator.register_reporters(reporter: reporter, options: options)
41
+
42
+ if reporter.reporters.reject! { |reporter| reporter.is_a?(Minitest::ProgressReporter) }
43
+ reporter << Minitest::Distributed::Reporters::DistributedPogressReporter.new(options[:io], options)
44
+ end
45
+
46
+ if reporter.reporters.reject! { |reporter| reporter.is_a?(Minitest::SummaryReporter) }
47
+ reporter << Minitest::Distributed::Reporters::DistributedSummaryReporter.new(options[:io], options)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,50 @@
1
+ require_relative 'lib/minitest/distributed/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "minitest-distributed"
5
+ spec.version = Minitest::Distributed::VERSION
6
+ spec.authors = ["Willem van Bergen"]
7
+ spec.email = ["willem@vanbergen.org"]
8
+
9
+ spec.summary = "Distributed test executor plugin for Minitest"
10
+ spec.description = <<~EOD
11
+ minitest-distributed is a plugin for minitest for executing tests on a
12
+ distributed set of unreliable workers.
13
+
14
+ When a test suite grows large enough, it inevitable gets too slow to run
15
+ on a single machine to give timely feedback to developers. This plugins
16
+ combats this issue by distributing the full test suite to a set of workers.
17
+ Every worker is a consuming from a single queue, so the tests get evenly
18
+ distributed and all workers will finish around the same time. Redis is used
19
+ as coordinator, but when using this plugin without having access to Redis,
20
+ it will use an in-memory coordinator.
21
+
22
+ Using multiple (virtual) machines for a test run is an (additional) source
23
+ of flakiness. To combat flakiness, minitest-distributed implements resiliency
24
+ patterns, like re-running a test on a different worker on failure, and
25
+ a circuit breaker for misbehaving workers.
26
+ EOD
27
+
28
+ spec.homepage = "https://github.com/Shopify/minitest-distributed"
29
+ spec.license = "MIT"
30
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
31
+
32
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
33
+
34
+ spec.metadata["homepage_uri"] = spec.homepage
35
+ spec.metadata["source_code_uri"] = "https://github.com/Shopify/minitest-distributed"
36
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
37
+
38
+ # Specify which files should be added to the gem when it is released.
39
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
40
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
41
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
42
+ end
43
+ spec.bindir = "exe"
44
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
45
+ spec.require_paths = ["lib"]
46
+
47
+ spec.add_dependency('minitest', '~> 5.12')
48
+ spec.add_dependency('redis', '~> 4.1')
49
+ spec.add_dependency('sorbet-runtime')
50
+ end
@@ -0,0 +1,2 @@
1
+ --dir
2
+ .
@@ -0,0 +1,238 @@
1
+ # This file is autogenerated. Do not edit it by hand. Regenerate it with:
2
+ # srb rbi sorbet-typed
3
+ #
4
+ # If you would like to make changes to this file, great! Please upstream any changes you make here:
5
+ #
6
+ # https://github.com/sorbet/sorbet-typed/edit/master/lib/minitest/all/minitest.rbi
7
+ #
8
+ # typed: strong
9
+
10
+ module Minitest
11
+ class Runnable
12
+ def self.run_one_method(klass, method_name, reporter); end
13
+ def self.runnables; end
14
+
15
+ def name; end
16
+ def time; end
17
+ def time=(duration); end
18
+ def failures; end
19
+
20
+ def initialize(method_name); end
21
+ end
22
+
23
+ class Test < Runnable
24
+ include Minitest::Assertions
25
+ end
26
+
27
+ class Result < Runnable
28
+ sig { returns(String) }
29
+ def name; end
30
+
31
+ sig { returns(String) }
32
+ def klass; end
33
+
34
+ sig { returns(String) }
35
+ def class_name; end
36
+
37
+ sig { returns(T.nilable(Minitest::Assertion)) }
38
+ def failure; end
39
+
40
+ sig { returns(T::Boolean) }
41
+ def error?; end
42
+
43
+ sig { returns(T::Boolean) }
44
+ def skipped?; end
45
+
46
+ sig { returns(T::Boolean) }
47
+ def passed?; end
48
+
49
+ sig { returns(Integer) }
50
+ def assertions; end
51
+
52
+ sig { params(runnable: Runnable).returns(T.attached_class) }
53
+ def self.from(runnable); end
54
+ end
55
+
56
+ class Assertion < Exception
57
+ sig { returns(String) }
58
+ def result_label; end
59
+
60
+ sig { returns(String) }
61
+ def result_code; end
62
+ end
63
+
64
+ class Skip < Exception
65
+ end
66
+
67
+ class UnexpectedError < Assertion
68
+ end
69
+
70
+ class AbstractReporter
71
+ sig { void }
72
+ def start; end
73
+
74
+ sig { params(runnable: T.class_of(Runnable), method_name: String).void }
75
+ def prerecord(runnable, method_name); end
76
+
77
+ sig { params(result: Minitest::Result).void }
78
+ def record(result); end
79
+
80
+ sig { void }
81
+ def report; end
82
+
83
+ sig { returns(T::Boolean) }
84
+ def passed?; end
85
+ end
86
+
87
+ class Reporter < AbstractReporter
88
+ sig { params(io: IO, options: T::Hash[Symbol, T.untyped]).void }
89
+ def initialize(io, options); end
90
+
91
+ sig { returns(T::Hash[Symbol, T.untyped]) }
92
+ def options; end
93
+
94
+ sig { returns(IO) }
95
+ def io; end
96
+ end
97
+
98
+ class StatisticsReporter < Reporter
99
+ end
100
+
101
+ class SummaryReporter < StatisticsReporter
102
+ end
103
+
104
+ class ProgressReporter < Reporter
105
+ end
106
+
107
+ class CompositeReporter < AbstractReporter
108
+ sig { params(reporter: AbstractReporter).returns(T.self_type) }
109
+ def <<(reporter); end
110
+
111
+ sig { returns(T::Array[AbstractReporter]) }
112
+ def reporters; end
113
+ end
114
+
115
+ sig { returns(CompositeReporter) }
116
+ def self.reporter; end
117
+
118
+ sig { void }
119
+ def self.autorun; end
120
+
121
+ sig { params(args: T::Array[String]).returns(T::Boolean) }
122
+ def self.run(args = []); end
123
+
124
+ sig { params(klass: T.class_of(Runnable), method_name: String).returns(Minitest::Result) }
125
+ def self.run_one_method(klass, method_name); end
126
+
127
+ sig { returns(Float) }
128
+ def self.clock_time; end
129
+ end
130
+
131
+ module Minitest::Assertions
132
+ extend T::Sig
133
+
134
+ sig { params(msg: T.nilable(String)).returns(TrueClass) }
135
+ def pass(msg = nil); end
136
+
137
+ sig { params(msg: T.nilable(String)).returns(FalseClass) }
138
+ def flunk(msg = nil); end
139
+
140
+ sig { params(test: T.untyped, msg: T.nilable(String)).returns(TrueClass) }
141
+ def assert(test, msg = nil); end
142
+
143
+ sig do
144
+ params(
145
+ exp: BasicObject,
146
+ msg: T.nilable(String)
147
+ ).returns(TrueClass)
148
+ end
149
+ def assert_empty(exp, msg = nil); end
150
+
151
+ sig do
152
+ params(
153
+ exp: BasicObject,
154
+ act: BasicObject,
155
+ msg: T.nilable(String)
156
+ ).returns(TrueClass)
157
+ end
158
+ def assert_equal(exp, act, msg = nil); end
159
+
160
+ sig do
161
+ params(
162
+ collection: T::Enumerable[T.untyped],
163
+ obj: BasicObject,
164
+ msg: T.nilable(String)
165
+ ).returns(TrueClass)
166
+ end
167
+ def assert_includes(collection, obj, msg = nil); end
168
+
169
+ sig do
170
+ params(
171
+ obj: BasicObject,
172
+ msg: T.nilable(String)
173
+ ).returns(TrueClass)
174
+ end
175
+ def assert_nil(obj, msg = nil); end
176
+
177
+ sig do
178
+ params(
179
+ exp: T.untyped
180
+ ).returns(TrueClass)
181
+ end
182
+ def assert_raises(*exp); end
183
+
184
+ sig do
185
+ params(
186
+ obj: BasicObject,
187
+ predicate: Symbol,
188
+ msg: T.nilable(String)
189
+ ).returns(TrueClass)
190
+ end
191
+ def assert_predicate(obj, predicate, msg = nil); end
192
+
193
+ sig { params(test: T.untyped, msg: T.nilable(String)).returns(TrueClass) }
194
+ def refute(test, msg = nil); end
195
+
196
+ sig do
197
+ params(
198
+ exp: BasicObject,
199
+ msg: T.nilable(String)
200
+ ).returns(TrueClass)
201
+ end
202
+ def refute_empty(exp, msg = nil); end
203
+
204
+ sig do
205
+ params(
206
+ exp: BasicObject,
207
+ act: BasicObject,
208
+ msg: T.nilable(String)
209
+ ).returns(TrueClass)
210
+ end
211
+ def refute_equal(exp, act, msg = nil); end
212
+
213
+ sig do
214
+ params(
215
+ collection: T::Enumerable[T.untyped],
216
+ obj: BasicObject,
217
+ msg: T.nilable(String)
218
+ ).returns(TrueClass)
219
+ end
220
+ def refute_includes(collection, obj, msg = nil); end
221
+
222
+ sig do
223
+ params(
224
+ obj: BasicObject,
225
+ msg: T.nilable(String)
226
+ ).returns(TrueClass)
227
+ end
228
+ def refute_nil(obj, msg = nil); end
229
+
230
+ sig do
231
+ params(
232
+ obj: BasicObject,
233
+ predicate: Symbol,
234
+ msg: T.nilable(String)
235
+ ).returns(TrueClass)
236
+ end
237
+ def refute_predicate(obj, predicate, msg = nil); end
238
+ end