minitest-distributed 0.1.2 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -0
- data/Gemfile +2 -2
- data/README.md +35 -13
- data/bin/setup +0 -2
- data/lib/minitest/distributed/configuration.rb +66 -4
- data/lib/minitest/distributed/coordinators/coordinator_interface.rb +3 -0
- data/lib/minitest/distributed/coordinators/memory_coordinator.rb +30 -9
- data/lib/minitest/distributed/coordinators/redis_coordinator.rb +259 -154
- data/lib/minitest/distributed/enqueued_runnable.rb +197 -40
- data/lib/minitest/distributed/filters/exclude_file_filter.rb +18 -0
- data/lib/minitest/distributed/filters/exclude_filter.rb +4 -4
- data/lib/minitest/distributed/filters/file_filter_base.rb +29 -0
- data/lib/minitest/distributed/filters/filter_interface.rb +3 -3
- data/lib/minitest/distributed/filters/include_file_filter.rb +18 -0
- data/lib/minitest/distributed/filters/include_filter.rb +4 -4
- data/lib/minitest/distributed/reporters/distributed_progress_reporter.rb +13 -5
- data/lib/minitest/distributed/reporters/distributed_summary_reporter.rb +49 -10
- data/lib/minitest/distributed/reporters/junitxml_reporter.rb +150 -0
- data/lib/minitest/distributed/reporters/redis_coordinator_warnings_reporter.rb +11 -16
- data/lib/minitest/distributed/result_aggregate.rb +38 -9
- data/lib/minitest/distributed/result_type.rb +76 -2
- data/lib/minitest/distributed/test_selector.rb +12 -6
- data/lib/minitest/distributed/version.rb +1 -1
- data/lib/minitest/distributed.rb +3 -0
- data/lib/minitest/distributed_plugin.rb +1 -25
- data/lib/minitest/junitxml_plugin.rb +21 -0
- data/sorbet/rbi/minitest.rbi +29 -11
- data/sorbet/rbi/redis.rbi +19 -4
- metadata +11 -7
- data/.travis.yml +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6a8b0499822e88334b7ce7d06210c43d3054485d4dbbc3170f97e485c5d78c3d
|
4
|
+
data.tar.gz: 2e4c720af2bf722152ace13867b0cf66c58aafbf712ac1a5d4b9e308f0c2fd8f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c974a610c8770a9a0ff8c944780182a178fbe1d22737a53a474b9f8f2abcd90765aac1f6a0ee9134cddb1e19dca235bb124b7950585337579b7dba5d4ebf956
|
7
|
+
data.tar.gz: 8337bf3b5c849d386ccbdb13e4cba6d725a274ead9893c60f3d60f9599e5ba7cde0e12ceea035219483ebd9aee0fc06ee9cc9e6a4747c08663f133dab30fd4d0
|
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
source "https://rubygems.org"
|
3
3
|
|
4
|
-
# Specify your gem's dependencies in minitest-
|
4
|
+
# Specify your gem's dependencies in minitest-distributed.gemspec
|
5
5
|
gemspec
|
6
6
|
|
7
|
-
gem "rake"
|
7
|
+
gem "rake"
|
8
8
|
gem "minitest", "~> 5.0"
|
9
9
|
gem "sorbet"
|
10
10
|
gem "rubocop"
|
data/README.md
CHANGED
@@ -63,8 +63,8 @@ them to fail.
|
|
63
63
|
|
64
64
|
### Other optional command line arguments
|
65
65
|
|
66
|
-
- `--test-timeout=SECONDS` or `ENV[
|
67
|
-
maximum amount a test is allowed to run before it times out. In a distributed
|
66
|
+
- `--test-timeout=SECONDS` or `ENV[MINITEST_TEST_TIMEOUT_SECONDS]` (default: 30s):
|
67
|
+
the maximum amount a test is allowed to run before it times out. In a distributed
|
68
68
|
system, it's impossible to differentiate between a worker being slow and a
|
69
69
|
worker being broken. When the timeout passes, the other workers will assume
|
70
70
|
that the worker running the test has crashed, and will attempt to claim this
|
@@ -80,6 +80,12 @@ them to fail.
|
|
80
80
|
- `--worker-id=IDENTIFIER` or `ENV[MINITEST_WORKER_ID]`: The ID of the worker,
|
81
81
|
which should be unique to the cluster. We will default to a UUID if this is
|
82
82
|
not set, which generally is fine.
|
83
|
+
- `--exclude-file=PATH_TO_FILE`: Specify a file of tests to be excluded
|
84
|
+
from running. The file should include test identifiers seperated by
|
85
|
+
newlines.
|
86
|
+
- `--include-file=PATH_TO_FILE`: Specify a file of tests to be included in
|
87
|
+
the test run. The file should include test identifiers seperated by
|
88
|
+
newlines.
|
83
89
|
|
84
90
|
## Limitations
|
85
91
|
|
@@ -92,24 +98,40 @@ other tests.
|
|
92
98
|
|
93
99
|
## Development
|
94
100
|
|
95
|
-
|
96
|
-
run `rake test` to run the tests. You can also run `bin/console` for an
|
97
|
-
interactive prompt that will allow you to experiment.
|
101
|
+
To bootstrap a local development environment:
|
98
102
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
103
|
+
- Run `bin/setup` to install dependencies.
|
104
|
+
- Start a Redis server by running `redis-server`, assuming you have Redis
|
105
|
+
installed locally and the binary is on your `PATH`. Alternatively, you can
|
106
|
+
set the `REDIS_URL` environment variable to point to a Redis instance running
|
107
|
+
elsewhere.
|
108
|
+
- Now, run `bin/rake test` to run the tests, and verify everything is working.
|
109
|
+
- You can also run `bin/console` for an interactive prompt that will allow you
|
110
|
+
to experiment.
|
111
|
+
|
112
|
+
### Releasing a new version
|
113
|
+
|
114
|
+
- To install this gem onto your local machine, run `bin/rake install`.
|
115
|
+
- Only people at Shopify can release a new version to
|
116
|
+
[rubygems.org](https://rubygems.org). To do so, update the `VERSION` constant
|
117
|
+
in `version.rb`, and merge to master. Shipit will take care of building the
|
118
|
+
`.gem` bundle, and pushing it to rubygems.org.
|
104
119
|
|
105
120
|
## Contributing
|
106
121
|
|
107
|
-
Bug reports and pull requests are welcome on GitHub at
|
122
|
+
Bug reports and pull requests are welcome on GitHub at
|
123
|
+
https://github.com/Shopify/minitest-distributed. This project is intended to
|
124
|
+
be a safe, welcoming space for collaboration, and contributors are expected to
|
125
|
+
adhere to the [code of
|
126
|
+
conduct](https://github.com/Shopify/minitest-distributed/blob/master/CODE_OF_CONDUCT.md).
|
108
127
|
|
109
128
|
## License
|
110
129
|
|
111
|
-
The gem is available as open source under the terms of the [MIT
|
130
|
+
The gem is available as open source under the terms of the [MIT
|
131
|
+
License](https://opensource.org/licenses/MIT).
|
112
132
|
|
113
133
|
## Code of Conduct
|
114
134
|
|
115
|
-
Everyone interacting in the
|
135
|
+
Everyone interacting in the `minitest-distributed` project's codebases, issue
|
136
|
+
trackers, chat rooms and mailing lists is expected to follow the [code of
|
137
|
+
conduct](https://github.com/Shopify/minitest-distributed/blob/master/CODE_OF_CONDUCT.md).
|
data/bin/setup
CHANGED
@@ -8,8 +8,8 @@ module Minitest
|
|
8
8
|
module Distributed
|
9
9
|
class Configuration < T::Struct
|
10
10
|
DEFAULT_BATCH_SIZE = 10
|
11
|
-
DEFAULT_MAX_ATTEMPTS =
|
12
|
-
|
11
|
+
DEFAULT_MAX_ATTEMPTS = 1
|
12
|
+
DEFAULT_TEST_TIMEOUT_SECONDS = 30.0 # seconds
|
13
13
|
|
14
14
|
class << self
|
15
15
|
extend T::Sig
|
@@ -20,21 +20,83 @@ module Minitest
|
|
20
20
|
coordinator_uri: URI(env['MINITEST_COORDINATOR'] || 'memory:'),
|
21
21
|
run_id: env['MINITEST_RUN_ID'] || SecureRandom.uuid,
|
22
22
|
worker_id: env['MINITEST_WORKER_ID'] || SecureRandom.uuid,
|
23
|
-
|
23
|
+
test_timeout_seconds: Float(env['MINITEST_TEST_TIMEOUT_SECONDS'] || DEFAULT_TEST_TIMEOUT_SECONDS),
|
24
24
|
test_batch_size: Integer(env['MINITEST_TEST_BATCH_SIZE'] || DEFAULT_BATCH_SIZE),
|
25
25
|
max_attempts: Integer(env['MINITEST_MAX_ATTEMPTS'] || DEFAULT_MAX_ATTEMPTS),
|
26
|
+
max_failures: (max_failures_env = env['MINITEST_MAX_FAILURES']) ? Integer(max_failures_env) : nil,
|
26
27
|
)
|
27
28
|
end
|
29
|
+
|
30
|
+
sig { params(opts: OptionParser, options: T::Hash[Symbol, T.untyped]).returns(T.attached_class) }
|
31
|
+
def from_command_line_options(opts, options)
|
32
|
+
configuration = from_env
|
33
|
+
configuration.progress = options[:io].tty?
|
34
|
+
|
35
|
+
opts.on('--coordinator=URI', "The URI pointing to the coordinator") do |uri|
|
36
|
+
configuration.coordinator_uri = URI.parse(uri)
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on('--test-timeout=TIMEOUT', "The maximum run time for a single test in seconds") do |timeout|
|
40
|
+
configuration.test_timeout_seconds = Float(timeout)
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on('--max-attempts=ATTEMPTS', "The maximum number of attempts to run a test") do |attempts|
|
44
|
+
configuration.max_attempts = Integer(attempts)
|
45
|
+
end
|
46
|
+
|
47
|
+
opts.on('--test-batch-size=NUMBER', "The number of tests to process per batch") do |batch_size|
|
48
|
+
configuration.test_batch_size = Integer(batch_size)
|
49
|
+
end
|
50
|
+
|
51
|
+
opts.on('--max-failures=FAILURES', "The maximum allowed failure before aborting a run") do |failures|
|
52
|
+
configuration.max_failures = Integer(failures)
|
53
|
+
end
|
54
|
+
|
55
|
+
opts.on('--run-id=ID', "The ID for this run shared between coordinated workers") do |id|
|
56
|
+
configuration.run_id = id
|
57
|
+
end
|
58
|
+
|
59
|
+
opts.on('--worker-id=ID', "The unique ID for this worker") do |id|
|
60
|
+
configuration.worker_id = id
|
61
|
+
end
|
62
|
+
|
63
|
+
opts.on(
|
64
|
+
'--[no-]retry-failures', "Retry failed and errored tests from a previous run attempt " \
|
65
|
+
"with the same run ID (default: enabled)"
|
66
|
+
) do |enabled|
|
67
|
+
configuration.retry_failures = enabled
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on('--[no-]progress', "Show progress during the test run") do |enabled|
|
71
|
+
configuration.progress = enabled
|
72
|
+
end
|
73
|
+
|
74
|
+
opts.on('--exclude-file=FILE_PATH', "Specify a file of tests to be excluded from running") do |file_path|
|
75
|
+
configuration.exclude_file = file_path
|
76
|
+
end
|
77
|
+
|
78
|
+
opts.on('--include-file=FILE_PATH', "Specify a file of tests to be included in the test run") do |file_path|
|
79
|
+
configuration.include_file = file_path
|
80
|
+
end
|
81
|
+
|
82
|
+
configuration
|
83
|
+
end
|
28
84
|
end
|
29
85
|
|
30
86
|
extend T::Sig
|
31
87
|
|
88
|
+
# standard minitest options don't need to be specified
|
32
89
|
prop :coordinator_uri, URI::Generic, default: URI('memory:')
|
33
90
|
prop :run_id, String, factory: -> { SecureRandom.uuid }
|
34
91
|
prop :worker_id, String, factory: -> { SecureRandom.uuid }
|
35
|
-
prop :
|
92
|
+
prop :test_timeout_seconds, Float, default: DEFAULT_TEST_TIMEOUT_SECONDS
|
36
93
|
prop :test_batch_size, Integer, default: DEFAULT_BATCH_SIZE
|
37
94
|
prop :max_attempts, Integer, default: DEFAULT_MAX_ATTEMPTS
|
95
|
+
prop :max_failures, T.nilable(Integer)
|
96
|
+
prop :retry_failures, T::Boolean, default: true
|
97
|
+
prop :progress, T::Boolean, default: false
|
98
|
+
prop :exclude_file, T.nilable(String)
|
99
|
+
prop :include_file, T.nilable(String)
|
38
100
|
|
39
101
|
sig { returns(Coordinators::CoordinatorInterface) }
|
40
102
|
def coordinator
|
@@ -25,7 +25,8 @@ module Minitest
|
|
25
25
|
|
26
26
|
@leader = T.let(Mutex.new, Mutex)
|
27
27
|
@queue = T.let(Queue.new, Queue)
|
28
|
-
@local_results = T.let(ResultAggregate.new, ResultAggregate)
|
28
|
+
@local_results = T.let(ResultAggregate.new(max_failures: configuration.max_failures), ResultAggregate)
|
29
|
+
@aborted = T.let(false, T::Boolean)
|
29
30
|
end
|
30
31
|
|
31
32
|
sig { override.params(reporter: Minitest::CompositeReporter, options: T::Hash[Symbol, T.untyped]).void }
|
@@ -33,6 +34,11 @@ module Minitest
|
|
33
34
|
# No need for any additional reporters
|
34
35
|
end
|
35
36
|
|
37
|
+
sig { override.returns(T::Boolean) }
|
38
|
+
def aborted?
|
39
|
+
@aborted
|
40
|
+
end
|
41
|
+
|
36
42
|
sig { override.params(test_selector: TestSelector).void }
|
37
43
|
def produce(test_selector:)
|
38
44
|
if @leader.try_lock
|
@@ -41,24 +47,39 @@ module Minitest
|
|
41
47
|
if tests.empty?
|
42
48
|
queue.close
|
43
49
|
else
|
44
|
-
tests.each
|
50
|
+
tests.each do |runnable|
|
51
|
+
queue << EnqueuedRunnable.new(
|
52
|
+
class_name: T.must(runnable.class.name),
|
53
|
+
method_name: runnable.name,
|
54
|
+
test_timeout_seconds: configuration.test_timeout_seconds,
|
55
|
+
max_attempts: configuration.max_attempts,
|
56
|
+
)
|
57
|
+
end
|
45
58
|
end
|
46
59
|
end
|
47
60
|
end
|
48
61
|
|
49
62
|
sig { override.params(reporter: AbstractReporter).void }
|
50
63
|
def consume(reporter:)
|
51
|
-
until queue.
|
52
|
-
enqueued_runnable = queue.pop
|
64
|
+
until queue.closed?
|
65
|
+
enqueued_runnable = T.let(queue.pop, EnqueuedRunnable)
|
66
|
+
|
53
67
|
reporter.prerecord(enqueued_runnable.runnable_class, enqueued_runnable.method_name)
|
54
|
-
result = enqueued_runnable.run
|
55
68
|
|
56
|
-
|
57
|
-
|
69
|
+
initial_result = enqueued_runnable.run
|
70
|
+
enqueued_result = enqueued_runnable.commit_result(initial_result) do |result_to_commit|
|
71
|
+
if ResultType.of(result_to_commit) == ResultType::Requeued
|
72
|
+
queue << enqueued_runnable.next_attempt
|
73
|
+
end
|
74
|
+
EnqueuedRunnable::Result::Commit.success
|
75
|
+
end
|
58
76
|
|
59
|
-
reporter.record(
|
77
|
+
reporter.record(enqueued_result.committed_result)
|
78
|
+
local_results.update_with_result(enqueued_result)
|
60
79
|
|
61
|
-
|
80
|
+
# We abort a run if we reach the maximum number of failures
|
81
|
+
queue.close if combined_results.abort?
|
82
|
+
queue.close if combined_results.complete?
|
62
83
|
end
|
63
84
|
end
|
64
85
|
end
|