minitest-distributed 0.1.2 → 0.2.2
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 +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
|