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.
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