mighty_test 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +257 -0
- data/exe/mt +4 -0
- data/lib/mighty_test/cli.rb +99 -0
- data/lib/mighty_test/console.rb +58 -0
- data/lib/mighty_test/file_system.rb +41 -0
- data/lib/mighty_test/minitest_runner.rb +22 -0
- data/lib/mighty_test/option_parser.rb +77 -0
- data/lib/mighty_test/sharder.rb +46 -0
- data/lib/mighty_test/test_parser.rb +33 -0
- data/lib/mighty_test/version.rb +3 -0
- data/lib/mighty_test/watcher.rb +140 -0
- data/lib/mighty_test.rb +11 -0
- metadata +132 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 83c44a15f432d51e09d5c772fd11bb8e6054eaf98cbcc76e08e8f8050c1e03bd
|
4
|
+
data.tar.gz: 5908e6428920eb6aedcc993a24ad44587b7e6355f9002b07358c095717f865f6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 912d2cc77b7b732439f129ce082d140d15216221aa8fd8c4e0a6a9082c3129c238bb29f8841303d12dfe6c43b96f4c37f76090bada21114843b5c4db933ea9b2
|
7
|
+
data.tar.gz: 4d1468bc39fa04b7445b97bb58e559d798e3978067a29ad841ba82d3fbe2b6c893592231116f5fa41df3d567cbd352aea49b57fd098b0cf04db3d11bfb2f58e5
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Matt Brictson
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,257 @@
|
|
1
|
+
# mighty_test
|
2
|
+
|
3
|
+
[![Gem Version](https://img.shields.io/gem/v/mighty_test)](https://rubygems.org/gems/mighty_test)
|
4
|
+
[![Gem Downloads](https://img.shields.io/gem/dt/mighty_test)](https://www.ruby-toolbox.com/projects/mighty_test)
|
5
|
+
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/mattbrictson/mighty_test/ci.yml)](https://github.com/mattbrictson/mighty_test/actions/workflows/ci.yml)
|
6
|
+
[![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/mattbrictson/mighty_test)](https://codeclimate.com/github/mattbrictson/mighty_test)
|
7
|
+
|
8
|
+
mighty_test (`mt`) is a TDD-friendly Minitest runner for Ruby projects. It includes a Jest-inspired interactive watch mode, focus mode, CI sharding, run by directory/file/line number, fail-fast, and color formatting.
|
9
|
+
|
10
|
+
---
|
11
|
+
|
12
|
+
**Quick Start**
|
13
|
+
|
14
|
+
- [Install](#install)
|
15
|
+
- [Requirements](#requirements)
|
16
|
+
- [Usage](#usage)
|
17
|
+
|
18
|
+
**Features**
|
19
|
+
|
20
|
+
- ⚙️ [CI Mode](#%EF%B8%8F-ci-mode)
|
21
|
+
- 🧑🔬 [Watch Mode](#-watch-mode)
|
22
|
+
- 🔬 [Focus Mode](#-focus-mode)
|
23
|
+
- 🛑 [Fail Fast](#-fail-fast)
|
24
|
+
- 🚥 [Color Output](#-color-output)
|
25
|
+
- 💬 [More Options](#-more-options)
|
26
|
+
|
27
|
+
**Community**
|
28
|
+
|
29
|
+
- [Support](#support)
|
30
|
+
- [License](#license)
|
31
|
+
- [Code of conduct](#code-of-conduct)
|
32
|
+
- [Contribution guide](#contribution-guide)
|
33
|
+
|
34
|
+
## Install
|
35
|
+
|
36
|
+
The mighty_test gem provides an `mt` binary. To install it into a Ruby project, first add the gem to your Gemfile and run `bundle install`.
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
gem "mighty_test"
|
40
|
+
```
|
41
|
+
|
42
|
+
Then generate a binstub:
|
43
|
+
|
44
|
+
```sh
|
45
|
+
bundle binstub mighty_test
|
46
|
+
```
|
47
|
+
|
48
|
+
Now you can run mighty_test with `bin/mt`.
|
49
|
+
|
50
|
+
> [!TIP]
|
51
|
+
> **When installing mighty_test in a Rails project, make sure to put the gem in the `:test` Gemfile group.** Although Rails has a built-in test runner (`bin/rails test`) that already provides a lot of what mighty_test offers, you can still use `bin/mt` with Rails projects for its unique `--watch` mode and CI `--shard` feature.
|
52
|
+
|
53
|
+
## Requirements
|
54
|
+
|
55
|
+
mighty_test requires modern versions of Minitest and Ruby.
|
56
|
+
|
57
|
+
- Minitest 5.15+
|
58
|
+
- Ruby 3.1+
|
59
|
+
|
60
|
+
Support for older Ruby versions will be dropped when they reach EOL. The EOL schedule can be found here: https://endoflife.date/ruby
|
61
|
+
|
62
|
+
> [!NOTE]
|
63
|
+
> mighty_test currently assumes that your tests are stored in `test/` and are named `*_test.rb`. Watch mode expects implementation files to be in `app/` and/or `lib/`.
|
64
|
+
|
65
|
+
## Usage
|
66
|
+
|
67
|
+
`mt` defaults to running all tests, excluding slow tests (see the explanation of slow tests below). You can also run tests by directory, file, or line number.
|
68
|
+
|
69
|
+
```sh
|
70
|
+
# Run all tests, excluding slow tests
|
71
|
+
bin/mt
|
72
|
+
|
73
|
+
# Run all tests, slow tests included
|
74
|
+
bin/mt --all
|
75
|
+
|
76
|
+
# Run a specific test file
|
77
|
+
bin/mt test/cli_test.rb
|
78
|
+
|
79
|
+
# Run a test by line number
|
80
|
+
bin/mt test/importer_test.rb:43
|
81
|
+
|
82
|
+
# Run a directory of tests
|
83
|
+
bin/mt test/commands
|
84
|
+
```
|
85
|
+
|
86
|
+
> [!TIP]
|
87
|
+
> mighty_test is optimized for TDD, and excludes slow tests by default. **Slow tests** are defined as those found in `test/{e2e,feature,features,integration,system}` directories. You can run slow tests with `--all` or by specifying a slow test file or directory explicitly, like `bin/mt test/system`.
|
88
|
+
|
89
|
+
## ⚙️ CI Mode
|
90
|
+
|
91
|
+
If the `CI` environment variable is set, mighty_test defaults to running _all_ tests, including slow tests. This is equivalent to passing `--all`.
|
92
|
+
|
93
|
+
mighty_test can also distribute test files evenly across parallel CI jobs, using the `--shard` option. The _shard_ nomenclature has been borrowed from similar features in [Jest](https://jestjs.io/docs/cli#--shard) and [Playwright](https://playwright.dev/docs/test-sharding).
|
94
|
+
|
95
|
+
```sh
|
96
|
+
# Run the 1st group of tests out of 4 total groups
|
97
|
+
bin/mt --shard 1/4
|
98
|
+
```
|
99
|
+
|
100
|
+
In GitHub Actions, for example, you can use `--shard` with a matrix strategy to easily divide tests across N jobs.
|
101
|
+
|
102
|
+
```yaml
|
103
|
+
jobs:
|
104
|
+
test:
|
105
|
+
strategy:
|
106
|
+
matrix:
|
107
|
+
shard:
|
108
|
+
- "1/4"
|
109
|
+
- "2/4"
|
110
|
+
- "3/4"
|
111
|
+
- "4/4"
|
112
|
+
steps:
|
113
|
+
- uses: actions/checkout@v4
|
114
|
+
- uses: ruby/setup-ruby@v1
|
115
|
+
with:
|
116
|
+
bundler-cache: true
|
117
|
+
- run: bin/mt --shard ${{ matrix.shard }}
|
118
|
+
```
|
119
|
+
|
120
|
+
In CircleCI, you can use the `parallelism` setting, which automatically injects `$CIRCLE_NODE_INDEX` and `$CIRCLE_NODE_TOTAL` environment variables. Note that `$CIRCLE_NODE_INDEX` is zero-indexed, so it needs to be incremented by 1.
|
121
|
+
|
122
|
+
```yaml
|
123
|
+
jobs:
|
124
|
+
test:
|
125
|
+
parallelism: 4
|
126
|
+
steps:
|
127
|
+
- checkout
|
128
|
+
- ruby/install-deps
|
129
|
+
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; bin/mt --shard ${SHARD}/${CIRCLE_NODE_TOTAL}
|
130
|
+
```
|
131
|
+
|
132
|
+
> [!TIP]
|
133
|
+
> `--shard` will shuffle tests and automatically distribute slow tests evenly across jobs.
|
134
|
+
|
135
|
+
## 🧑🔬 Watch Mode
|
136
|
+
|
137
|
+
mighty_test includes a Jest-style watch mode, which can be started with `--watch`. This is ideal for TDD.
|
138
|
+
|
139
|
+
```sh
|
140
|
+
# Start watch mode
|
141
|
+
bin/mt --watch
|
142
|
+
```
|
143
|
+
|
144
|
+
In watch mode, mighty_test will listen for file system activity and run a test file whenever it is modified.
|
145
|
+
|
146
|
+
When you modify an implementation file, mighty_test will find the corresponding test file and run it automatically. This works as long as your implementation and test files follow a standard path naming convention: e.g. `lib/commands/init.rb` is expected to have a corresponding test file named `test/commands/init_test.rb`.
|
147
|
+
|
148
|
+
Watch mode also offers a menu of interactive commands:
|
149
|
+
|
150
|
+
```
|
151
|
+
> Press Enter to run all tests.
|
152
|
+
> Press "a" to run all tests, including slow tests.
|
153
|
+
> Press "d" to run tests for files diffed or added since the last git commit.
|
154
|
+
> Press "h" to show this help menu.
|
155
|
+
> Press "q" to quit.
|
156
|
+
```
|
157
|
+
|
158
|
+
## 🔬 Focus Mode
|
159
|
+
|
160
|
+
You can focus a specific test by annotating the method definition with `focus`.
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
class MyTest < Minitest::Test
|
164
|
+
focus def test_something_important
|
165
|
+
assert # ...
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
Now running `bin/mt` will execute only the focused test:
|
170
|
+
|
171
|
+
```sh
|
172
|
+
# Only runs MyTest#test_something_important
|
173
|
+
bin/mt
|
174
|
+
```
|
175
|
+
|
176
|
+
In Rails projects that use the `test` syntax, `focus` must be placed on the previous line.
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
class MyTest < ActiveSupport::TestCase
|
180
|
+
focus
|
181
|
+
test "something important" do
|
182
|
+
assert # ...
|
183
|
+
end
|
184
|
+
```
|
185
|
+
|
186
|
+
This functionality is provided by the [minitest-focus](https://github.com/minitest/minitest-focus) plugin, which is included with mighty_test.
|
187
|
+
|
188
|
+
## 🛑 Fail Fast
|
189
|
+
|
190
|
+
By default, mighty_test runs the entire test suite to completion. With the `--fail-fast` option, it will stop on the first failed test.
|
191
|
+
|
192
|
+
```sh
|
193
|
+
# Stop immediately on first test failure
|
194
|
+
bin/mt --fail-fast
|
195
|
+
|
196
|
+
# Use with watch mode for even faster TDD
|
197
|
+
bin/mt --watch --fail-fast
|
198
|
+
```
|
199
|
+
|
200
|
+
This functionality is provided by the [minitest-fail-fast](https://github.com/teoljungberg/minitest-fail-fast) plugin, which is included with mighty_test.
|
201
|
+
|
202
|
+
## 🚥 Color Output
|
203
|
+
|
204
|
+
Successes, failures, errors, and skips are colored appropriately by default.
|
205
|
+
|
206
|
+
```sh
|
207
|
+
# Run tests with color output (if terminal supports it)
|
208
|
+
bin/mt
|
209
|
+
|
210
|
+
# Disable color
|
211
|
+
bin/mt --no-rg
|
212
|
+
```
|
213
|
+
|
214
|
+
(image goes here)
|
215
|
+
|
216
|
+
This functionality is provided by the [minitest-rg](https://github.com/minitest/minitest-rg) plugin, which is included with mighty_test.
|
217
|
+
|
218
|
+
## 💬 More Options
|
219
|
+
|
220
|
+
Minitest options are passed through to Minitest.
|
221
|
+
|
222
|
+
```sh
|
223
|
+
# Run tests with Minitest pride color output
|
224
|
+
bin/mt --pride
|
225
|
+
|
226
|
+
# Run tests with an explicit seed value for test ordering
|
227
|
+
bin/mt --seed 4519
|
228
|
+
|
229
|
+
# Run tests with detailed progress and explanation of skipped tests
|
230
|
+
bin/mt --verbose
|
231
|
+
|
232
|
+
# Show the full list of possible options
|
233
|
+
bin/mt --help
|
234
|
+
```
|
235
|
+
|
236
|
+
If you have Minitest extensions installed, like [minitest-snapshots](https://github.com/mattbrictson/minitest-snapshots), the command line options of those extensions are supported as well.
|
237
|
+
|
238
|
+
```sh
|
239
|
+
# Update snapshots
|
240
|
+
bin/mt -u
|
241
|
+
```
|
242
|
+
|
243
|
+
## Support
|
244
|
+
|
245
|
+
If you want to report a bug, or have ideas, feedback or questions about the gem, [let me know via GitHub issues](https://github.com/mattbrictson/mighty_test/issues/new) and I will do my best to provide a helpful answer. Happy hacking!
|
246
|
+
|
247
|
+
## License
|
248
|
+
|
249
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
250
|
+
|
251
|
+
## Code of conduct
|
252
|
+
|
253
|
+
Everyone interacting in this project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
|
254
|
+
|
255
|
+
## Contribution guide
|
256
|
+
|
257
|
+
Pull requests are welcome!
|
data/exe/mt
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
module MightyTest
|
2
|
+
class CLI
|
3
|
+
def initialize(file_system: FileSystem.new, env: ENV, option_parser: OptionParser.new, runner: MinitestRunner.new)
|
4
|
+
@file_system = file_system
|
5
|
+
@env = env.to_h
|
6
|
+
@option_parser = option_parser
|
7
|
+
@runner = runner
|
8
|
+
end
|
9
|
+
|
10
|
+
def run(argv: ARGV)
|
11
|
+
@path_args, @extra_args, @options = option_parser.parse(argv)
|
12
|
+
|
13
|
+
if options[:help]
|
14
|
+
print_help
|
15
|
+
elsif options[:version]
|
16
|
+
puts VERSION
|
17
|
+
elsif options[:watch]
|
18
|
+
watch
|
19
|
+
elsif path_args.grep(/.:\d+$/).any?
|
20
|
+
run_test_by_line_number
|
21
|
+
else
|
22
|
+
run_tests_by_path
|
23
|
+
end
|
24
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
25
|
+
handle_exception(e)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :file_system, :env, :path_args, :extra_args, :options, :option_parser, :runner
|
31
|
+
|
32
|
+
def print_help
|
33
|
+
# Minitest already prints the `-h, --help` option, so omit mighty_test's
|
34
|
+
puts option_parser.to_s.sub(/^\s*-h.*?\n/, "")
|
35
|
+
puts
|
36
|
+
runner.print_help_and_exit!
|
37
|
+
end
|
38
|
+
|
39
|
+
def watch
|
40
|
+
Watcher.new(extra_args:).run
|
41
|
+
end
|
42
|
+
|
43
|
+
def run_test_by_line_number
|
44
|
+
path, line = path_args.first.match(/^(.+):(\d+)$/).captures
|
45
|
+
test_name = TestParser.new(path).test_name_at_line(line.to_i)
|
46
|
+
|
47
|
+
if test_name
|
48
|
+
run_tests_and_exit!(path, flags: ["-n", "/^#{Regexp.quote(test_name)}$/"])
|
49
|
+
else
|
50
|
+
run_tests_and_exit!
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def run_tests_by_path
|
55
|
+
test_paths = find_test_paths
|
56
|
+
test_paths = excluding_slow_paths(test_paths) unless path_args.any? || ci? || options[:all]
|
57
|
+
test_paths = Sharder.from_argv(options[:shard], env:, file_system:).shard(test_paths) if options[:shard]
|
58
|
+
|
59
|
+
run_tests_and_exit!(*test_paths)
|
60
|
+
end
|
61
|
+
|
62
|
+
def excluding_slow_paths(test_paths)
|
63
|
+
test_paths.reject { |path| file_system.slow_test_path?(path) }
|
64
|
+
end
|
65
|
+
|
66
|
+
def ci?
|
67
|
+
!env["CI"].to_s.strip.empty?
|
68
|
+
end
|
69
|
+
|
70
|
+
def find_test_paths
|
71
|
+
return file_system.find_test_paths if path_args.empty?
|
72
|
+
|
73
|
+
path_args.flat_map do |path|
|
74
|
+
if Dir.exist?(path)
|
75
|
+
file_system.find_test_paths(path)
|
76
|
+
elsif File.exist?(path)
|
77
|
+
[path]
|
78
|
+
else
|
79
|
+
raise ArgumentError, "#{path} does not exist"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def run_tests_and_exit!(*test_paths, flags: [])
|
85
|
+
runner.run_inline_and_exit!(*test_paths, args: extra_args + flags)
|
86
|
+
end
|
87
|
+
|
88
|
+
def handle_exception(e) # rubocop:disable Naming/MethodParameterName
|
89
|
+
case e
|
90
|
+
when SignalException
|
91
|
+
exit(128 + e.signo)
|
92
|
+
when Errno::EPIPE
|
93
|
+
# pass
|
94
|
+
else
|
95
|
+
raise e
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require "io/console"
|
2
|
+
|
3
|
+
module MightyTest
|
4
|
+
class Console
|
5
|
+
def initialize(stdin: $stdin, sound_player: "/usr/bin/afplay", sound_paths: SOUNDS)
|
6
|
+
@stdin = stdin
|
7
|
+
@sound_player = sound_player
|
8
|
+
@sound_paths = sound_paths
|
9
|
+
end
|
10
|
+
|
11
|
+
def clear
|
12
|
+
return false unless tty?
|
13
|
+
|
14
|
+
$stdout.clear_screen
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
def wait_for_keypress
|
19
|
+
return stdin.getc unless stdin.respond_to?(:raw) && tty?
|
20
|
+
|
21
|
+
stdin.raw(intr: true) { stdin.getc }
|
22
|
+
end
|
23
|
+
|
24
|
+
def play_sound(name, wait: false)
|
25
|
+
return false unless tty?
|
26
|
+
|
27
|
+
paths = sound_paths.fetch(name) { raise ArgumentError, "Unknown sound name #{name}" }
|
28
|
+
path = paths.find { |p| File.exist?(p) }
|
29
|
+
return false unless path && File.executable?(sound_player)
|
30
|
+
|
31
|
+
thread = Thread.new { system(sound_player, path) }
|
32
|
+
thread.join if wait
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# rubocop:disable Layout/LineLength
|
39
|
+
SOUNDS = {
|
40
|
+
pass: %w[
|
41
|
+
/System/Library/PrivateFrameworks/ToneLibrary.framework/Versions/A/Resources/AlertTones/EncoreInfinitum/Milestone-EncoreInfinitum.caf
|
42
|
+
/System/Library/Sounds/Glass.aiff
|
43
|
+
],
|
44
|
+
fail: %w[
|
45
|
+
/System/Library/PrivateFrameworks/ToneLibrary.framework/Versions/A/Resources/AlertTones/EncoreInfinitum/Rebound-EncoreInfinitum.caf
|
46
|
+
/System/Library/Sounds/Bottle.aiff
|
47
|
+
]
|
48
|
+
}.freeze
|
49
|
+
private_constant :SOUNDS
|
50
|
+
# rubocop:enable Layout/LineLength
|
51
|
+
|
52
|
+
attr_reader :sound_player, :sound_paths, :stdin
|
53
|
+
|
54
|
+
def tty?
|
55
|
+
$stdout.respond_to?(:tty?) && $stdout.tty?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "open3"
|
2
|
+
|
3
|
+
module MightyTest
|
4
|
+
class FileSystem
|
5
|
+
def listen(&)
|
6
|
+
require "listen"
|
7
|
+
Listen.to(*%w[app lib test].select { |p| Dir.exist?(p) }, relative: true, &).tap(&:start)
|
8
|
+
end
|
9
|
+
|
10
|
+
def find_matching_test_path(path) # rubocop:disable Metrics/CyclomaticComplexity
|
11
|
+
return nil unless path && File.exist?(path) && !Dir.exist?(path)
|
12
|
+
return path if path.match?(%r{^test/.*_test.rb$})
|
13
|
+
|
14
|
+
test_path = path[%r{^(?:app|lib)/(.+)\.[^\.]+$}, 1]&.then { "test/#{_1}_test.rb" }
|
15
|
+
test_path if test_path && File.exist?(test_path)
|
16
|
+
end
|
17
|
+
|
18
|
+
def find_test_paths(directory="test")
|
19
|
+
glob = File.join(directory, "**/*_test.rb")
|
20
|
+
Dir[glob]
|
21
|
+
end
|
22
|
+
|
23
|
+
def slow_test_path?(path)
|
24
|
+
return false if path.nil?
|
25
|
+
|
26
|
+
path.match?(%r{^test/(e2e|feature|features|integration|system)/})
|
27
|
+
end
|
28
|
+
|
29
|
+
def find_new_and_changed_paths
|
30
|
+
out, _err, status = Open3.capture3(*%w[git status --porcelain=1 -uall -z --no-renames -- test app lib])
|
31
|
+
return [] unless status.success?
|
32
|
+
|
33
|
+
out
|
34
|
+
.split("\x0")
|
35
|
+
.filter_map { |line| line[/^.. (.+)/, 1] }
|
36
|
+
.uniq
|
37
|
+
rescue SystemCallError
|
38
|
+
[]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module MightyTest
|
2
|
+
class MinitestRunner
|
3
|
+
def print_help_and_exit!
|
4
|
+
require "minitest"
|
5
|
+
Minitest.run(["--help"])
|
6
|
+
exit
|
7
|
+
end
|
8
|
+
|
9
|
+
def run_inline_and_exit!(*test_files, args: [])
|
10
|
+
$LOAD_PATH.unshift "test"
|
11
|
+
ARGV.replace(Array(args))
|
12
|
+
|
13
|
+
require "minitest/focus"
|
14
|
+
require "minitest/rg"
|
15
|
+
|
16
|
+
test_files.flatten.each { |file| require File.expand_path(file.to_s) }
|
17
|
+
|
18
|
+
require "minitest/autorun"
|
19
|
+
exit
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require "shellwords"
|
2
|
+
|
3
|
+
module MightyTest
|
4
|
+
class OptionParser
|
5
|
+
def parse(argv)
|
6
|
+
argv, literal_args = split(argv, "--")
|
7
|
+
options = parse_options!(argv)
|
8
|
+
minitest_flags = parse_minitest_flags!(argv) unless options[:help]
|
9
|
+
|
10
|
+
[argv + literal_args, minitest_flags || [], options]
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
<<~USAGE
|
15
|
+
Usage: mt [--all]
|
16
|
+
mt [test file...] [test dir...]
|
17
|
+
mt --watch
|
18
|
+
USAGE
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def parse_options!(argv)
|
24
|
+
options = {}
|
25
|
+
options[:all] = true if argv.delete("--all")
|
26
|
+
options[:watch] = true if argv.delete("--watch")
|
27
|
+
options[:version] = true if argv.delete("--version")
|
28
|
+
options[:help] = true if argv.delete("--help") || argv.delete("-h")
|
29
|
+
parse_shard(argv, options)
|
30
|
+
options
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse_minitest_flags!(argv)
|
34
|
+
return [] if argv.grep(/\A-/).none?
|
35
|
+
|
36
|
+
require "minitest"
|
37
|
+
orig_argv = argv.dup
|
38
|
+
|
39
|
+
Minitest.load_plugins unless argv.delete("--no-plugins") || ENV["MT_NO_PLUGINS"]
|
40
|
+
minitest_options = Minitest.process_args(argv)
|
41
|
+
|
42
|
+
minitest_args = Shellwords.split(minitest_options[:args] || "")
|
43
|
+
remove_seed_flag(minitest_args) unless orig_argv.include?("--seed")
|
44
|
+
|
45
|
+
minitest_args
|
46
|
+
end
|
47
|
+
|
48
|
+
def split(array, delim)
|
49
|
+
delim_at = array.index(delim)
|
50
|
+
return [array.dup, []] if delim_at.nil?
|
51
|
+
|
52
|
+
[
|
53
|
+
array[0...delim_at],
|
54
|
+
array[(delim_at + 1)..]
|
55
|
+
]
|
56
|
+
end
|
57
|
+
|
58
|
+
def parse_shard(argv, options)
|
59
|
+
argv.delete_if { |arg| options[:shard] = Regexp.last_match(1) if arg =~ /\A--shard=(.*)/ }
|
60
|
+
|
61
|
+
argv.each_with_index do |flag, i|
|
62
|
+
value = argv[i + 1]
|
63
|
+
next unless flag == "--shard"
|
64
|
+
raise "missing shard value" if value.nil? || value.start_with?("-")
|
65
|
+
|
66
|
+
options[:shard] = value
|
67
|
+
argv.slice!(i, 2)
|
68
|
+
break
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def remove_seed_flag(parsed_argv)
|
73
|
+
index = parsed_argv.index("--seed")
|
74
|
+
parsed_argv.slice!(index, 2) if index
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module MightyTest
|
2
|
+
class Sharder
|
3
|
+
DEFAULT_SEED = 123_456_789
|
4
|
+
|
5
|
+
def self.from_argv(value, env: ENV, file_system: FileSystem.new)
|
6
|
+
index, total = value.to_s.match(%r{\A(\d+)/(\d+)\z})&.captures&.map(&:to_i)
|
7
|
+
raise ArgumentError, "shard: value must be in the form INDEX/TOTAL (e.g. 2/8)" if total.nil?
|
8
|
+
|
9
|
+
git_sha = env.values_at("GITHUB_SHA", "CIRCLE_SHA1").find { |sha| !sha.to_s.strip.empty? }
|
10
|
+
seed = git_sha&.unpack1("l_")
|
11
|
+
|
12
|
+
new(index:, total:, seed:, file_system:)
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :index, :total, :seed
|
16
|
+
|
17
|
+
def initialize(index:, total:, seed: nil, file_system: FileSystem.new)
|
18
|
+
raise ArgumentError, "shard: total shards must be a number greater than 0" unless total > 0
|
19
|
+
|
20
|
+
valid_group = index > 0 && index <= total
|
21
|
+
raise ArgumentError, "shard: shard index must be > 0 and <= #{total}" unless valid_group
|
22
|
+
|
23
|
+
@index = index
|
24
|
+
@total = total
|
25
|
+
@seed = seed || DEFAULT_SEED
|
26
|
+
@file_system = file_system
|
27
|
+
end
|
28
|
+
|
29
|
+
def shard(*test_paths)
|
30
|
+
random = Random.new(seed)
|
31
|
+
|
32
|
+
# Shuffle slow and normal paths separately so that slow ones get evenly distributed
|
33
|
+
shuffled_paths = test_paths
|
34
|
+
.flatten
|
35
|
+
.partition { |path| !file_system.slow_test_path?(path) }
|
36
|
+
.flat_map { |paths| paths.shuffle(random:) }
|
37
|
+
|
38
|
+
slices = shuffled_paths.each_slice(total)
|
39
|
+
slices.filter_map { |slice| slice[index - 1] }
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
attr_reader :file_system
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module MightyTest
|
2
|
+
class TestParser
|
3
|
+
def initialize(test_path)
|
4
|
+
@path = test_path.to_s
|
5
|
+
end
|
6
|
+
|
7
|
+
def test_name_at_line(number)
|
8
|
+
method_name = nil
|
9
|
+
lines = File.read(path).lines
|
10
|
+
lines[2...number].reverse_each.find do |line|
|
11
|
+
method_name =
|
12
|
+
match_minitest_method_name(line) ||
|
13
|
+
match_active_support_test_string(line)&.then { "test_#{_1.gsub(/\s+/, '_')}" }
|
14
|
+
end
|
15
|
+
method_name
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :path
|
21
|
+
|
22
|
+
def match_minitest_method_name(line)
|
23
|
+
line[/^\s*(?:focus\s+)?def\s+(test_\w+)/, 1]
|
24
|
+
end
|
25
|
+
|
26
|
+
def match_active_support_test_string(line)
|
27
|
+
match = line.match(/^\s*test\s+(?:"(.+?)"|'(.+?)')\s*do\s*(?:#.*?)?$/)
|
28
|
+
return unless match
|
29
|
+
|
30
|
+
match.captures.compact.first
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module MightyTest
|
2
|
+
class Watcher # rubocop:disable Metrics/ClassLength
|
3
|
+
WATCHING_FOR_CHANGES = 'Watching for changes to source and test files. Press "h" for help or "q" to quit.'.freeze
|
4
|
+
|
5
|
+
def initialize(console: Console.new, extra_args: [], file_system: FileSystem.new, system_proc: method(:system))
|
6
|
+
@queue = Thread::Queue.new
|
7
|
+
@console = console
|
8
|
+
@extra_args = extra_args
|
9
|
+
@file_system = file_system
|
10
|
+
@system_proc = system_proc
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(iterations: :indefinitely) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
14
|
+
start_file_system_listener
|
15
|
+
start_keypress_listener
|
16
|
+
puts WATCHING_FOR_CHANGES
|
17
|
+
|
18
|
+
loop_for(iterations) do
|
19
|
+
case await_next_event
|
20
|
+
in [:file_system_changed, [_, *] => paths]
|
21
|
+
run_matching_test_files(paths)
|
22
|
+
in [:keypress, "\r" | "\n"]
|
23
|
+
run_all_tests
|
24
|
+
in [:keypress, "a"]
|
25
|
+
run_all_tests(flags: ["--all"])
|
26
|
+
in [:keypress, "d"]
|
27
|
+
run_matching_test_files_from_git_diff
|
28
|
+
in [:keypress, "h"]
|
29
|
+
show_help
|
30
|
+
in [:keypress, "q"]
|
31
|
+
break
|
32
|
+
else
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
ensure
|
37
|
+
puts "\nExiting."
|
38
|
+
file_system_listener&.stop
|
39
|
+
keypress_listener&.kill
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
attr_reader :console, :extra_args, :file_system, :file_system_listener, :keypress_listener, :system_proc
|
45
|
+
|
46
|
+
def show_help
|
47
|
+
console.clear
|
48
|
+
puts <<~MENU
|
49
|
+
`mt --watch` is watching file system activity and will automatically run
|
50
|
+
test files when they are added or modified. If you modify a source file,
|
51
|
+
mt will find and run the corresponding tests.
|
52
|
+
|
53
|
+
You can also trigger test runs with the following interactive commands.
|
54
|
+
|
55
|
+
> Press Enter to run all tests.
|
56
|
+
> Press "a" to run all tests, including slow tests.
|
57
|
+
> Press "d" to run tests for files diffed or added since the last git commit.
|
58
|
+
> Press "h" to show this help menu.
|
59
|
+
> Press "q" to quit.
|
60
|
+
|
61
|
+
MENU
|
62
|
+
end
|
63
|
+
|
64
|
+
def run_all_tests(flags: [])
|
65
|
+
console.clear
|
66
|
+
puts flags.any? ? "Running tests with #{flags.join(' ')}..." : "Running tests..."
|
67
|
+
puts
|
68
|
+
mt(flags:)
|
69
|
+
end
|
70
|
+
|
71
|
+
def run_matching_test_files(paths)
|
72
|
+
test_paths = paths.flat_map { |path| file_system.find_matching_test_path(path) }.compact.uniq
|
73
|
+
return false if test_paths.empty?
|
74
|
+
|
75
|
+
console.clear
|
76
|
+
puts test_paths.join("\n")
|
77
|
+
puts
|
78
|
+
mt(*test_paths)
|
79
|
+
true
|
80
|
+
end
|
81
|
+
|
82
|
+
def run_matching_test_files_from_git_diff
|
83
|
+
return if run_matching_test_files(file_system.find_new_and_changed_paths)
|
84
|
+
|
85
|
+
console.clear
|
86
|
+
puts "No affected test files detected since the last git commit."
|
87
|
+
puts WATCHING_FOR_CHANGES
|
88
|
+
end
|
89
|
+
|
90
|
+
def mt(*test_paths, flags: [])
|
91
|
+
command = ["mt", *extra_args, *flags]
|
92
|
+
command.append("--", *test_paths.flatten) if test_paths.any?
|
93
|
+
|
94
|
+
success = system_proc.call(*command)
|
95
|
+
|
96
|
+
console.play_sound(success ? :pass : :fail)
|
97
|
+
puts "\n#{WATCHING_FOR_CHANGES}"
|
98
|
+
$stdout.flush
|
99
|
+
rescue Interrupt
|
100
|
+
# Pressing ctrl-c kills the fs_event background process, so we have to manually restart it.
|
101
|
+
restart_file_system_listener
|
102
|
+
end
|
103
|
+
|
104
|
+
def start_file_system_listener
|
105
|
+
file_system_listener.stop if file_system_listener && !file_system_listener.stopped?
|
106
|
+
|
107
|
+
@file_system_listener = file_system.listen do |modified, added, _removed|
|
108
|
+
# Pause listener so that subsequent changes are queued up while we are running the tests
|
109
|
+
file_system_listener.pause unless file_system_listener.stopped?
|
110
|
+
post_event(:file_system_changed, [*modified, *added].uniq)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
alias restart_file_system_listener start_file_system_listener
|
114
|
+
|
115
|
+
def start_keypress_listener
|
116
|
+
@keypress_listener = Thread.new do
|
117
|
+
loop do
|
118
|
+
key = console.wait_for_keypress
|
119
|
+
break if key.nil?
|
120
|
+
|
121
|
+
post_event(:keypress, key)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
@keypress_listener.abort_on_exception = true
|
125
|
+
end
|
126
|
+
|
127
|
+
def loop_for(iterations, &)
|
128
|
+
iterations == :indefinitely ? loop(&) : iterations.times(&)
|
129
|
+
end
|
130
|
+
|
131
|
+
def await_next_event
|
132
|
+
file_system_listener.start if file_system_listener.paused?
|
133
|
+
@queue.pop
|
134
|
+
end
|
135
|
+
|
136
|
+
def post_event(*event)
|
137
|
+
@queue << event
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
data/lib/mighty_test.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
module MightyTest
|
2
|
+
autoload :VERSION, "mighty_test/version"
|
3
|
+
autoload :CLI, "mighty_test/cli"
|
4
|
+
autoload :Console, "mighty_test/console"
|
5
|
+
autoload :FileSystem, "mighty_test/file_system"
|
6
|
+
autoload :MinitestRunner, "mighty_test/minitest_runner"
|
7
|
+
autoload :OptionParser, "mighty_test/option_parser"
|
8
|
+
autoload :Sharder, "mighty_test/sharder"
|
9
|
+
autoload :TestParser, "mighty_test/test_parser"
|
10
|
+
autoload :Watcher, "mighty_test/watcher"
|
11
|
+
end
|
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mighty_test
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matt Brictson
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-02-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: listen
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.5'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5.15'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '5.15'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest-fail-fast
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.1.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.1.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest-focus
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.4'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.4'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest-rg
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '5.3'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '5.3'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- opensource@mattbrictson.com
|
86
|
+
executables:
|
87
|
+
- mt
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- LICENSE.txt
|
92
|
+
- README.md
|
93
|
+
- exe/mt
|
94
|
+
- lib/mighty_test.rb
|
95
|
+
- lib/mighty_test/cli.rb
|
96
|
+
- lib/mighty_test/console.rb
|
97
|
+
- lib/mighty_test/file_system.rb
|
98
|
+
- lib/mighty_test/minitest_runner.rb
|
99
|
+
- lib/mighty_test/option_parser.rb
|
100
|
+
- lib/mighty_test/sharder.rb
|
101
|
+
- lib/mighty_test/test_parser.rb
|
102
|
+
- lib/mighty_test/version.rb
|
103
|
+
- lib/mighty_test/watcher.rb
|
104
|
+
homepage: https://github.com/mattbrictson/mighty_test
|
105
|
+
licenses:
|
106
|
+
- MIT
|
107
|
+
metadata:
|
108
|
+
bug_tracker_uri: https://github.com/mattbrictson/mighty_test/issues
|
109
|
+
changelog_uri: https://github.com/mattbrictson/mighty_test/releases
|
110
|
+
source_code_uri: https://github.com/mattbrictson/mighty_test
|
111
|
+
homepage_uri: https://github.com/mattbrictson/mighty_test
|
112
|
+
rubygems_mfa_required: 'true'
|
113
|
+
post_install_message:
|
114
|
+
rdoc_options: []
|
115
|
+
require_paths:
|
116
|
+
- lib
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '3.1'
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
requirements: []
|
128
|
+
rubygems_version: 3.5.5
|
129
|
+
signing_key:
|
130
|
+
specification_version: 4
|
131
|
+
summary: A modern Minitest runner for TDD, with watch mode and more
|
132
|
+
test_files: []
|