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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/Gemfile +2 -2
  4. data/README.md +35 -13
  5. data/bin/setup +0 -2
  6. data/lib/minitest/distributed/configuration.rb +66 -4
  7. data/lib/minitest/distributed/coordinators/coordinator_interface.rb +3 -0
  8. data/lib/minitest/distributed/coordinators/memory_coordinator.rb +30 -9
  9. data/lib/minitest/distributed/coordinators/redis_coordinator.rb +259 -154
  10. data/lib/minitest/distributed/enqueued_runnable.rb +197 -40
  11. data/lib/minitest/distributed/filters/exclude_file_filter.rb +18 -0
  12. data/lib/minitest/distributed/filters/exclude_filter.rb +4 -4
  13. data/lib/minitest/distributed/filters/file_filter_base.rb +29 -0
  14. data/lib/minitest/distributed/filters/filter_interface.rb +3 -3
  15. data/lib/minitest/distributed/filters/include_file_filter.rb +18 -0
  16. data/lib/minitest/distributed/filters/include_filter.rb +4 -4
  17. data/lib/minitest/distributed/reporters/distributed_progress_reporter.rb +13 -5
  18. data/lib/minitest/distributed/reporters/distributed_summary_reporter.rb +49 -10
  19. data/lib/minitest/distributed/reporters/junitxml_reporter.rb +150 -0
  20. data/lib/minitest/distributed/reporters/redis_coordinator_warnings_reporter.rb +11 -16
  21. data/lib/minitest/distributed/result_aggregate.rb +38 -9
  22. data/lib/minitest/distributed/result_type.rb +76 -2
  23. data/lib/minitest/distributed/test_selector.rb +12 -6
  24. data/lib/minitest/distributed/version.rb +1 -1
  25. data/lib/minitest/distributed.rb +3 -0
  26. data/lib/minitest/distributed_plugin.rb +1 -25
  27. data/lib/minitest/junitxml_plugin.rb +21 -0
  28. data/sorbet/rbi/minitest.rbi +29 -11
  29. data/sorbet/rbi/redis.rbi +19 -4
  30. metadata +11 -7
  31. data/.travis.yml +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfef30635f4dd487b913e94e04a1098f27d27278af1f1adbb794a1f91f54e880
4
- data.tar.gz: cff09c3e45440e77a7f39dabcdc6faf3a99336cc05362ea1740c810c91bb227b
3
+ metadata.gz: 6a8b0499822e88334b7ce7d06210c43d3054485d4dbbc3170f97e485c5d78c3d
4
+ data.tar.gz: 2e4c720af2bf722152ace13867b0cf66c58aafbf712ac1a5d4b9e308f0c2fd8f
5
5
  SHA512:
6
- metadata.gz: b9a05e26c4b0a432d3b7c2d8aec342d141b0fc5b4a1b7a355b6e5e14609a85e834c60fde5d6bf32a85529d9678c0ce19e2a8e9feb1d6da49c761b77289c340b0
7
- data.tar.gz: 430c1c0da35af7e4db05580676de9ae4d911ecb0673d99535b07c00a72f600dd71e3e279a127e254bba2be13352579d1867ff376edaf1b91fd241b5b6da0b408
6
+ metadata.gz: 8c974a610c8770a9a0ff8c944780182a178fbe1d22737a53a474b9f8f2abcd90765aac1f6a0ee9134cddb1e19dca235bb124b7950585337579b7dba5d4ebf956
7
+ data.tar.gz: 8337bf3b5c849d386ccbdb13e4cba6d725a274ead9893c60f3d60f9599e5ba7cde0e12ceea035219483ebd9aee0fc06ee9cc9e6a4747c08663f133dab30fd4d0
data/.rubocop.yml CHANGED
@@ -11,6 +11,10 @@ AllCops:
11
11
  Exclude:
12
12
  - minitest-distributed.gemspec
13
13
 
14
+ # This cop is broken when using assignments
15
+ Layout/RescueEnsureAlignment:
16
+ Enabled: false
17
+
14
18
  ##### Sorbet cops
15
19
 
16
20
  Sorbet:
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-stateful.gemspec
4
+ # Specify your gem's dependencies in minitest-distributed.gemspec
5
5
  gemspec
6
6
 
7
- gem "rake", "~> 12.0"
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[MINITEST_TEST_TIMEOUT]` (default: 30s): the
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
- After checking out the repo, run `bin/setup` to install dependencies. Then,
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
- To install this gem onto your local machine, run `bundle exec rake install`.
100
- To release a new version, update the version number in `version.rb`, and then
101
- run `bundle exec rake release`, which will create a git tag for the version,
102
- push git commits and tags, and push the `.gem` file to
103
- [rubygems.org](https://rubygems.org).
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 https://github.com/Shopify/minitest-distributed. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/Shopify/minitest-distributed/blob/master/CODE_OF_CONDUCT.md).
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 License](https://opensource.org/licenses/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 Minitest::Stateful project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/Shopify/minitest-distributed/blob/master/CODE_OF_CONDUCT.md).
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
@@ -4,5 +4,3 @@ IFS=$'\n\t'
4
4
  set -vx
5
5
 
6
6
  bundle install
7
-
8
- # Do any other automated setup that you need to do here
@@ -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 = 3
12
- DEFAULT_TEST_TIMEOUT = 30.0 # seconds
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
- test_timeout: Float(env['MINITEST_TEST_TIMEOUT'] || DEFAULT_TEST_TIMEOUT),
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 :test_timeout, Float, default: DEFAULT_TEST_TIMEOUT
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
@@ -18,6 +18,9 @@ module Minitest
18
18
  sig { abstract.returns(ResultAggregate) }
19
19
  def combined_results; end
20
20
 
21
+ sig { abstract.returns(T::Boolean) }
22
+ def aborted?; end
23
+
21
24
  sig { abstract.params(test_selector: TestSelector).void }
22
25
  def produce(test_selector:); end
23
26
 
@@ -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 { |test| queue << test }
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.empty? && queue.closed?
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
- local_results.update_with_result(result)
57
- local_results.acks += 1
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(result)
77
+ reporter.record(enqueued_result.committed_result)
78
+ local_results.update_with_result(enqueued_result)
60
79
 
61
- queue.close if local_results.completed?
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