tldr 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 71122b11466d7a2682ace7dbba2258d2ead4a4aec723882b0c543bcbc3c12f39
4
- data.tar.gz: efe76c0ab2ee09ba20921d56922630557ffb11704e3839a8a7322329b8fb5889
3
+ metadata.gz: 32f7b9dbcf9e09433eef762eaafe19804643aa543ac2c0a9599303ea2e960f25
4
+ data.tar.gz: 789c07fb2db1fad39b234976999b575a1bea27c4796a4d062a373df11d3716e5
5
5
  SHA512:
6
- metadata.gz: 8a3dbacc10c845d976b89775ad762b5e37d2b6c2dbd657dc02422f7d356f98770b16ea485f242c7065ba688d8d69db078801735b41812f1f91a139bcd6d1ac31
7
- data.tar.gz: ccc10568c6c506d7656fee1619f678dc32da38298d0ef33c7e2b613e849f9a323dc3105c49aea7ecf108bd705c92fd90dd6cab88fb57e69f0fb4a53e119144ab
6
+ metadata.gz: 7975ec24ae3f080ed564666c5319928964c970794a740a21253551be1be9b945374394b2f860cdd8737151f8e21fdb0d592c126895dd02b91dd21c899824226a
7
+ data.tar.gz: b11f3c52624b63a7aa44eadd4f17aa662d134648d61eb55eb7124e7956d0b28f20a04545552f9af8710330ffa98c94e85cec87dc80715a532522ccdeb15a8b42
data/CHANGELOG.md CHANGED
@@ -1,4 +1,18 @@
1
- ## [Unreleased]
1
+ ## [0.5.0]
2
+
3
+ * Define your own Rake tasks with `TLDR::Task` and pass in a custom configuration
4
+ * Any tests with `--prepend` AND marked thread-unsafe with `dont_run_these_in_parallel`
5
+ will be run BEFORE the parallel tests start running. This way if you're working
6
+ on a non-parallelizable test, running `tldr` will automatically run it first
7
+ thing
8
+ * Stop printing `--seed` in run commands, since it can be confusing to discover
9
+ that will disable `--parallel`. Instead, print the seed option beneath
10
+
11
+ ## [0.4.0]
12
+
13
+ * Add `TLDR.dont_run_these_in_parallel!` method to allow tests to indicate that they
14
+ must be run in isolation and not concurrently with any other tests
15
+ * Add support for `around` hooks (similar to [minitest-around](https://github.com/splattael/minitest-around))
2
16
 
3
17
  ## [0.3.0]
4
18
 
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # TLDR - for people who don't have time for slow tests
2
2
 
3
- **tl;dr, this is a very nice test runner for Ruby that fails after 1.8 seconds**
3
+ Okay, you might need to sit down for this:
4
+
5
+ **tl;dr, TLDR is a Ruby test framework that stops running your tests after 1.8 seconds.**
4
6
 
5
7
  We initially meant this as a joke [while
6
8
  pairin'](https://www.youtube.com/live/bmi-SWeH4MA?si=p5g1j1FQZrbYEOCg&t=63), but
@@ -9,16 +11,28 @@ in addition to being funny, it was also a pretty good idea. So we fleshed out
9
11
  [Minitest-compatible](#minitest-compatibility), and downright pleasant test
10
12
  framework for Ruby.
11
13
 
14
+ The "big idea" here is TLDR is designed for users to run the `tldr` command
15
+ repeatedly as they work—as opposed to only running the tests for whatever is
16
+ being worked on. Even if the suite run over the 1.8 second time limit. Because
17
+ TLDR shuffles and runs in parallel and is guaranteed to take less than two
18
+ seconds,
19
+ **you'll actually wind up running _all_ of your tests quite often as you work**,
20
+ catching any problems much earlier than if you had waited until the end of the
21
+ day to push your work and let a continuous integration server run the full
22
+ suite.
23
+
12
24
  Some stuff you might like:
13
25
 
14
26
  * A CLI that can run tests by line number(s) (e.g. `foo.rb:5 bar.rb:3:10`) and
15
27
  by names or patterns (e.g. `--name test_fail,test_error --name "/_\d/"`)
16
- * Everything is **parallel by default**, and seems pretty fast (you can disable with `--no-parallel`)
17
- * Surprisingly delightful color diff output when two things fail to equal one another, care of [@mcmire's super_diff gem](https://github.com/mcmire/super_diff)
28
+ * Everything is **parallel by default**, and seems pretty darn fast; TLDR
29
+ also provides [several escape hatches to sequester tests that aren't thread-safe](#parallel-by-default-is-nice-in-theory-but-half-my-tests-are-failing-wat)
30
+ * Surprisingly delightful color diff output when two things fail to equal one
31
+ another, care of [@mcmire's super_diff gem](https://github.com/mcmire/super_diff)
18
32
  * By default, the CLI will prepend your most-recently-edited test file to the
19
- front of your suite so its tests will run first. The tests you're working on are
20
- the most likely you care about running, so TLDR runs them first (see the
21
- `--prepend` option)
33
+ front of your suite so its tests will run first. The test you worked on most recently
34
+ is the one you most likely want to ensure runs, so TLDR runs it first (see the
35
+ `--prepend` option for how to control this behavior)
22
36
  * And, of course, our signature feature: your test suite will never grow into
23
37
  a glacially slow, soul-sucking albatross around your neck, because **after 1.8
24
38
  seconds, it stops running your tests**, with a report on what it _was_ able to
@@ -50,7 +64,7 @@ end
50
64
  ```
51
65
 
52
66
  A TLDR subclass defines its tests with instance methods that begin with
53
- `_test`. They can define `setup` and/or `teardown` methods which will run before
67
+ `test_`. They can define `setup` and/or `teardown` methods which will run before
54
68
  and after each test, respectively.
55
69
 
56
70
  If you place your tests in `test/**/*_test.rb` (and/or `test/**/test_*.rb`)
@@ -82,68 +96,9 @@ flags:
82
96
  $ tldr --name FooTest#test_foo -n test_bar,test_baz -n /_qux/
83
97
  ```
84
98
 
85
- (The above will translate to this array of name fiilters internally:
99
+ (The above will translate to this array of name filters internally:
86
100
  `["FooTest#test_foo", "test_bar", "test_baz", "/_qux/"]`.)
87
101
 
88
- ### Running tests without the CLI
89
-
90
- If you'd rather use TLDR by running Ruby files instead of the `tldr` CLI
91
- (similar to `require "minitest/autorun"`), here's how to do it!
92
-
93
- Given a file `test/some_test.rb`:
94
-
95
- ```ruby
96
- require "tldr"
97
- TLDR::Run.at_exit! TLDR::Config.new(no_emoji: true)
98
-
99
- class SomeTest < TLDR
100
- def test_truth
101
- assert true
102
- end
103
- end
104
- ```
105
-
106
- You could run the test with:
107
-
108
- ```
109
- $ ruby test/some_test.rb
110
- ```
111
-
112
- To maximize control and to avoid running code accidentally (and _unlike_ the
113
- `tldr` CLI), running `at_exit!` will not set default values to the `paths`,
114
- `helper`, `load_paths`, and `prepend_tests` config properties. You'll have to
115
- pass any values you want to set on a [Config object](/lib/tldr/value/config.rb)
116
- and pass it to `at_exit!`.
117
-
118
- To avoid running multiple suites accidentally, if `TLDR::Run.at_exit!` is
119
- encountered multiple times, only the first hook will be registered. If the
120
- `tldr` CLI is running and encounters a call to `at_exit!`, it will be ignored.
121
-
122
- #### Setting up the load path
123
-
124
- When running TLDR from a Ruby script, one thing the framework can't help you with
125
- is setting up load paths for you.
126
-
127
- If you want to require code in `test/` or `lib/` without using
128
- `require_relative`, you'll need to add those directories to the load path. You
129
- can do this programmatically by prepending the path to `$LOAD_PATH`, like
130
- this:
131
-
132
- ```ruby
133
- $LOAD_PATH.unshift "test"
134
-
135
- require "tldr"
136
- TLDR::Run.at_exit! TLDR::Config.new(no_emoji: true)
137
-
138
- require "helper"
139
- ```
140
-
141
- Or by using Ruby's `-I` flag to include it:
142
-
143
- ```
144
- $ ruby -Itest test/some_test.rb
145
- ```
146
-
147
102
  ### Options
148
103
 
149
104
  Here is the full list of CLI options:
@@ -192,7 +147,8 @@ well as some [internal tests](/tests/dotfile_test.rb) demonstrating its behavior
192
147
  Tests you write with tldr are designed to be mostly-compatible with
193
148
  [Minitest](https://github.com/minitest/minitest) tests. Some notes:
194
149
 
195
- * `setup` and `teardown` hook methods should work as you expect
150
+ * `setup` and `teardown` hook methods should work as you expect. (We even threw
151
+ in [an `around` hook](https://github.com/splattael/minitest-around) as a bonus!)
196
152
  * All of Minitest's assertions (e.g. `assert`, `assert_equals`) are provided,
197
153
  with these caveats:
198
154
  * To retain the `expected, actual` argument ordering, `tldr` defines
@@ -201,7 +157,7 @@ with these caveats:
201
157
  * If you want to maximize compatibility and mix in `assert_includes` and the
202
158
  deprecated `assert_send`, just `include
203
159
  TLDR::Assertions::MinitestCompatibility` into the `TLDR` base class or
204
- individual test classes
160
+ individual test classesJust set it
205
161
 
206
162
  ### Running TLDR with Rake
207
163
 
@@ -225,7 +181,92 @@ You could then run the task with:
225
181
  $ TLDR_OPTS="--no-parallel" bundle exec rake tldr
226
182
  ```
227
183
 
228
- ### Parallel-by-default was a bold choice—also my tests are failing now, thanks
184
+ One reason you'd want to invoke TLDR with Rake is because you have multiple
185
+ test suites that you want to be able to conveniently run separately ([this
186
+ talk](https://blog.testdouble.com/talks/2014-05-25-breaking-up-with-your-test-suite/)
187
+ discussed a few reasons why this can be useful).
188
+
189
+ To create a custom TLDR Rake test, just instantiate `TLDR::Task` like this:
190
+
191
+ ```ruby
192
+ require "tldr/rake"
193
+
194
+ TLDR::Task.new(name: :safe_tests, config: TLDR::Config.new(
195
+ paths: FileList["safe/**/*_test.rb"],
196
+ helper: "safe/helper.rb",
197
+ load_paths: ["lib", "safe"]
198
+ ))
199
+ ```
200
+
201
+ The above will create a second Rake task named `safe_tests` running a different
202
+ set of tests than the default `tldr` task. Here's [an
203
+ example](/example/b/Rakefile).
204
+
205
+ ### Running tests without the CLI
206
+
207
+ If you'd rather use TLDR by running Ruby files instead of the `tldr` CLI
208
+ (similar to `require "minitest/autorun"`), here's how to do it!
209
+
210
+ Given a file `test/some_test.rb`:
211
+
212
+ ```ruby
213
+ require "tldr"
214
+ TLDR::Run.at_exit! TLDR::Config.new(no_emoji: true)
215
+
216
+ class SomeTest < TLDR
217
+ def test_truth
218
+ assert true
219
+ end
220
+ end
221
+ ```
222
+
223
+ You could run the test with:
224
+
225
+ ```
226
+ $ ruby test/some_test.rb
227
+ ```
228
+
229
+ To maximize control and to avoid running code accidentally (and _unlike_ the
230
+ `tldr` CLI), running `at_exit!` will not set default values to the `paths`,
231
+ `helper`, `load_paths`, and `prepend_paths` config properties. You'll have to
232
+ pass any values you want to set on a [Config object](/lib/tldr/value/config.rb)
233
+ and pass it to `at_exit!`.
234
+
235
+ To avoid running multiple suites accidentally, if `TLDR::Run.at_exit!` is
236
+ encountered multiple times, only the first hook will be registered. If the
237
+ `tldr` CLI is running and encounters a call to `at_exit!`, it will be ignored.
238
+
239
+ #### Setting up the load path
240
+
241
+ When running TLDR from a Ruby script, one thing the framework can't help you with
242
+ is setting up load paths for you.
243
+
244
+ If you want to require code in `test/` or `lib/` without using
245
+ `require_relative`, you'll need to add those directories to the load path. You
246
+ can do this programmatically by prepending the path to `$LOAD_PATH`, like
247
+ this:
248
+
249
+ ```ruby
250
+ $LOAD_PATH.unshift "test"
251
+
252
+ require "tldr"
253
+ TLDR::Run.at_exit! TLDR::Config.new(no_emoji: true)
254
+
255
+ require "helper"
256
+ ```
257
+
258
+ Or by using Ruby's `-I` flag to include it:
259
+
260
+ ```
261
+ $ ruby -Itest test/some_test.rb
262
+ ```
263
+
264
+ ## Questions you might be asking
265
+
266
+ TLDR is very similar to Minitest in API, but different in enough ways that you
267
+ probably have some questions.
268
+
269
+ ### Parallel-by-default is nice in theory but half my tests are failing. Wat?
229
270
 
230
271
  **Read this before you add `--no-parallel` because some tests are failing when
231
272
  you run `tldr`.**
@@ -234,17 +275,17 @@ The vast majority of test suites in the wild are not parallelized and the vast
234
275
  majority of _those_ will only parallelize by forking processes as opposed to
235
276
  using a thread pool. We wanted to encourage more people to save time (after all,
236
277
  you only get 1.8 seconds here) by making your test suite run as fast as it can,
237
- so your tests run in parallel by default.
278
+ so your tests run in parallel threads by default.
238
279
 
239
280
  If you're writing new code and tests with TLDR and dutifully running `tldr`
240
281
  constantly for fast feedback, odds are that this will help you catch thread
241
- safety issues early—this is a good thing, because it gives you a chance to fix
242
- them! But maybe you're porting an existing test suite to TLDR and running in
243
- parallel for the first time, or maybe you need to test something that simply
244
- _can't_ be exercised in a thread-safe way. For those cases, TLDR's goal is to
245
- give you some tools to prevent you from giving up and adding `--no-parallel` to
246
- your entire test suite and **slowing everything down for the sake of a few
247
- tests**.
282
+ safety issues early—this is a good thing, because it gives you a chance to
283
+ address them before they're too hard to fix! But maybe you're porting an
284
+ existing test suite to TLDR and running in parallel for the first time, or maybe
285
+ you need to test something that simply _can't_ be exercised in a thread-safe
286
+ way. For those cases, TLDR's goal is to give you some tools to prevent you from
287
+ giving up and adding `--no-parallel` to your entire test suite and **slowing
288
+ everything down for the sake of a few tests**.
248
289
 
249
290
  So, when you see a test that is failing when run in parallel with the rest of your
250
291
  suite, here is what we recommend doing, in priority order:
@@ -254,11 +295,17 @@ thread-safe. Modern versions of Ruby provide a number of tools to make this
254
295
  easier than it used to be, and it may be as simple as making an instance
255
296
  variable thread-local
256
297
  2. If the problem is that a subset of your tests depend on the same resource,
257
- try using [TLDR.run_these_together!](/lib/tldr/run_these_together.rb) class to
298
+ try using [TLDR.run_these_together!](lib/tldr/parallel_controls.rb) class to
258
299
  group the tests together. This will ensure that those tests run in the same
259
- thread in sequence (here's a a [simple
300
+ thread in sequence (here's a [simple
260
301
  example](/tests/fixture/run_these_together.rb))
261
- 3. Give up and make the whole suite `--no-parallel`. If you find that you need
302
+ 3. For tests that affect process-wide resources like setting the system clock or
303
+ changing the process's working directory (i.e. `Dir.chdir`), you can sequester
304
+ them to run sequentially _after_ all parallel tests in your suite have run with
305
+ [TLDR.dont_run_these_in_parallel!](lib/tldr/parallel_controls.rb), which takes
306
+ the same arguments as `run_these_together!`
307
+ ([example](/tests/fixture/dont_run_these_in_parallel.rb))
308
+ 4. Give up and make the whole suite `--no-parallel`. If you find that you need
262
309
  to resort to this, you might save some keystrokes by adding `parallel: false` in
263
310
  a [.tldr.yml](#setting-defaults-in-tldryml) file
264
311
 
@@ -267,11 +314,23 @@ your suite without slowing down the rest of your tests, so stay tuned!
267
314
 
268
315
  ### How will I run all my tests in CI without the time bomb going off?
269
316
 
270
- TLDR will run all your tests in CI without the time bomb going off.
317
+ TLDR will run all your tests in CI without the time bomb going off. If
318
+ `tldr` is run in a non-interactive shell and a `CI` environment variable is set
319
+ (as it is on virtually every CI service), then the bomb will be defused.
320
+
321
+ ### Is there a plugin system?
322
+
323
+ There is not.
324
+
325
+ Currently, the only pluggable aspect of TLDR are reporters, which can be set
326
+ with the `--reporter` command line option. It can be set to any fully-qualified
327
+ class name that extends from
328
+ [TLDR::Reporters::Base](/lib/tldr/reporters/base.rb).
271
329
 
272
330
  ### What about mocking?
273
331
 
274
- TLDR is laser-focused on running tests. Might we interest you in a refreshing
332
+ TLDR is laser-focused on running tests, so it doesn't provide a built-in mocking
333
+ facility. Might we interest you in a refreshing
275
334
  [mocktail](https://github.com/testdouble/mocktail), instead?
276
335
 
277
336
  ## Acknowledgements
@@ -4,11 +4,7 @@ class TLDR
4
4
  class ArgvParser
5
5
  PATTERN_FRIENDLY_SPLITTER = /,(?=(?:[^\/]*\/[^\/]*\/)*[^\/]*$)/
6
6
 
7
- def parse(args)
8
- options = {
9
- cli_mode: true
10
- }
11
-
7
+ def parse(args, options = {cli_defaults: true})
12
8
  OptionParser.new do |opts|
13
9
  opts.banner = "Usage: tldr [options] some_tests/**/*.rb some/path.rb:13 ..."
14
10
 
@@ -47,9 +43,9 @@ class TLDR
47
43
  options[:no_helper] = true
48
44
  end
49
45
 
50
- opts.on "#{CONFLAGS[:prepend_tests]} PATH", Array, "Prepend one or more paths to run before the rest (Default: most recently modified test)" do |prepend|
51
- options[:prepend_tests] ||= []
52
- options[:prepend_tests] += prepend
46
+ opts.on "#{CONFLAGS[:prepend_paths]} PATH", Array, "Prepend one or more paths to run before the rest (Default: most recently modified test)" do |prepend|
47
+ options[:prepend_paths] ||= []
48
+ options[:prepend_paths] += prepend
53
49
  end
54
50
 
55
51
  opts.on CONFLAGS[:no_prepend], "Don't prepend any tests before the rest of the suite" do
@@ -0,0 +1,47 @@
1
+ class TLDR
2
+ # If it's not safe to run a set of tests in parallel, you can force them to
3
+ # run in a group together (in a single worker) with `run_these_together!` in
4
+ # your test.
5
+ #
6
+ # This method takes an array of tuples, where the first element is the class
7
+ # (or its name as a string, if the class is not yet defined in the current
8
+ # file) and the second element is the method name. If the second element is
9
+ # nil, then all the tests on the class will be run together.
10
+ #
11
+ # Examples:
12
+ # - `run_these_together!` will run all the tests defined on the current
13
+ # class to be run in a group
14
+ # - `run_these_together!([[ClassA, nil], ["ClassB", :test_1], [ClassB, :test_2]])`
15
+ # will run all the tests defined on ClassA, and test_1 and test_2 from ClassB
16
+ #
17
+ GROUPED_TESTS = Concurrent::Array.new
18
+ def self.run_these_together! klass_method_tuples = [[self, nil]]
19
+ GROUPED_TESTS << TestGroup.new(klass_method_tuples)
20
+ end
21
+
22
+ # This is a similar API to run_these_together! but its effect is more drastic
23
+ # Rather than running the provided (class, method) tuples in a group within a
24
+ # thread as part of a parallel run, it will reserve all tests specified by
25
+ # all calls to `dont_run_these_in_parallel!` to be run after all parallel tests have
26
+ # finished.
27
+ #
28
+ # This has an important implication! If your test suite is over TLDR's time
29
+ # limit, it means that these tests will never be run outside of CI unless you
30
+ # run them manually.
31
+ #
32
+ # Like `run_these_together!`, `dont_run_these_in_parallel!` takes an array of
33
+ # tuples, where the first element is the class (or its fully-qualified name as
34
+ # a string) and the second element is `nil` (matching all the class's test
35
+ # methods) or else one of the methods on the class.
36
+ #
37
+ # Examples:
38
+ # - `dont_run_these_in_parallel!` will run all the tests defined on the current
39
+ # class after all parallel tests have finished
40
+ # - `dont_run_these_in_parallel!([[ClassA, nil], ["ClassB", :test_1], [ClassB, :test_2]])`
41
+ # will run all the tests defined on ClassA, and test_1 and test_2 from ClassB
42
+ #
43
+ THREAD_UNSAFE_TESTS = Concurrent::Array.new
44
+ def self.dont_run_these_in_parallel! klass_method_tuples = [[self, nil]]
45
+ THREAD_UNSAFE_TESTS << TestGroup.new(klass_method_tuples)
46
+ end
47
+ end
@@ -2,35 +2,47 @@ class TLDR
2
2
  class Parallelizer
3
3
  def initialize
4
4
  @strategizer = Strategizer.new
5
- end
6
-
7
- def parallelize tests, parallel, &blk
8
- return tests.map(&blk) if tests.size < 2 || !parallel
9
- tldr_pool = Concurrent::ThreadPoolExecutor.new(
5
+ @thread_pool = Concurrent::ThreadPoolExecutor.new(
10
6
  name: "tldr",
11
7
  auto_terminate: true
12
8
  )
9
+ end
10
+
11
+ def parallelize all_tests, config, &blk
12
+ return run_in_sequence(all_tests, &blk) if all_tests.size < 2 || !config.parallel
13
+
14
+ strategy = @strategizer.strategize(
15
+ all_tests,
16
+ GROUPED_TESTS,
17
+ THREAD_UNSAFE_TESTS,
18
+ (config.no_prepend ? [] : config.prepend_paths)
19
+ )
13
20
 
14
- strategy = @strategizer.strategize tests, GROUPED_TESTS
21
+ run_in_sequence(strategy.prepend_thread_unsafe_tests, &blk) +
22
+ run_in_parallel(strategy.parallel_tests_and_groups, &blk) +
23
+ run_in_sequence(strategy.thread_unsafe_tests, &blk)
24
+ end
15
25
 
16
- strategy.tests_and_groups.map { |test_or_group|
26
+ private
27
+
28
+ def run_in_sequence tests, &blk
29
+ tests.map(&blk)
30
+ end
31
+
32
+ def run_in_parallel tests_and_groups, &blk
33
+ tests_and_groups.map { |test_or_group|
17
34
  tests_to_run = if test_or_group.group?
18
- test_or_group.tests.select { |test| tests.include? test }
35
+ test_or_group.tests
19
36
  else
20
37
  [test_or_group]
21
38
  end
22
39
 
23
40
  unless tests_to_run.empty?
24
- Concurrent::Promises.future_on(tldr_pool) {
41
+ Concurrent::Promises.future_on(@thread_pool) {
25
42
  tests_to_run.map(&blk)
26
43
  }
27
44
  end
28
45
  }.compact.flat_map(&:value)
29
46
  end
30
-
31
- private
32
-
33
- def substitute_tests_grouped_by_run_these_together! tests
34
- end
35
47
  end
36
48
  end
@@ -0,0 +1,41 @@
1
+ class TLDR
2
+ module PathUtil
3
+ def self.expand_search_locations path_strings
4
+ path_strings.flat_map { |path_string|
5
+ File.directory?(path_string) ? Dir["#{path_string}/**/*.rb"] : path_string
6
+ }.flat_map { |path_string|
7
+ absolute_path = File.expand_path(path_string.gsub(/:.*$/, ""), Dir.pwd)
8
+ line_numbers = path_string.scan(/:(\d+)/).flatten.map(&:to_i)
9
+
10
+ if line_numbers.any?
11
+ line_numbers.map { |line_number| Location.new absolute_path, line_number }
12
+ else
13
+ [Location.new(absolute_path, nil)]
14
+ end
15
+ }.uniq
16
+ end
17
+
18
+ # Because search paths to TLDR can include line numbers (e.g. a.rb:4), we
19
+ # can't just pass everything to Dir.glob. Instead, we have to check whether
20
+ # a user-provided search path looks like a glob, and if so, expand it
21
+ #
22
+ # Globby characters specified here:
23
+ # https://ruby-doc.org/3.2.2/Dir.html#method-c-glob
24
+ def self.expand_globs search_paths
25
+ search_paths.flat_map { |search_path|
26
+ if search_path.match?(/[*?\[\]{}]/)
27
+ raise Error, "Can't combine globs and line numbers in: #{search_path}" if search_path.match?(/:(\d+)$/)
28
+ Dir[search_path]
29
+ else
30
+ search_path
31
+ end
32
+ }
33
+ end
34
+
35
+ def self.locations_include_test? locations, test
36
+ locations.any? { |location|
37
+ location.file == test.file && (location.line.nil? || test.covers_line?(location.line))
38
+ }
39
+ end
40
+ end
41
+ end
data/lib/tldr/planner.rb CHANGED
@@ -3,7 +3,7 @@ require "pathname"
3
3
  class TLDR
4
4
  class Planner
5
5
  def plan config
6
- search_locations = expand_search_locations config.paths
6
+ search_locations = PathUtil.expand_search_locations config.paths
7
7
 
8
8
  prepend_load_paths config
9
9
  require_test_helper config
@@ -32,21 +32,6 @@ class TLDR
32
32
 
33
33
  private
34
34
 
35
- def expand_search_locations path_strings
36
- path_strings.flat_map { |path_string|
37
- File.directory?(path_string) ? Dir["#{path_string}/**/*.rb"] : path_string
38
- }.flat_map { |path_string|
39
- absolute_path = File.expand_path(path_string.gsub(/:.*$/, ""), Dir.pwd)
40
- line_numbers = path_string.scan(/:(\d+)/).flatten.map(&:to_i)
41
-
42
- if line_numbers.any?
43
- line_numbers.map { |line_number| Location.new absolute_path, line_number }
44
- else
45
- [Location.new(absolute_path, nil)]
46
- end
47
- }.uniq
48
- end
49
-
50
35
  def gather_tests
51
36
  gather_descendants(TLDR).flat_map { |subklass|
52
37
  subklass.instance_methods.grep(/^test_/).sort.map { |method|
@@ -57,9 +42,9 @@ class TLDR
57
42
 
58
43
  def prepend tests, config
59
44
  return tests if config.no_prepend
60
- prepended_locations = expand_search_locations expand_globs config.prepend_tests
45
+ prepended_locations = PathUtil.expand_search_locations PathUtil.expand_globs config.prepend_paths
61
46
  prepended, rest = tests.partition { |test|
62
- locations_include_test? prepended_locations, test
47
+ PathUtil.locations_include_test? prepended_locations, test
63
48
  }
64
49
  prepended + rest
65
50
  end
@@ -69,11 +54,11 @@ class TLDR
69
54
  end
70
55
 
71
56
  def exclude_by_path tests, exclude_paths
72
- excluded_locations = expand_search_locations expand_globs exclude_paths
57
+ excluded_locations = PathUtil.expand_search_locations PathUtil.expand_globs exclude_paths
73
58
  return tests if excluded_locations.empty?
74
59
 
75
60
  tests.reject { |test|
76
- locations_include_test? excluded_locations, test
61
+ PathUtil.locations_include_test? excluded_locations, test
77
62
  }
78
63
  end
79
64
 
@@ -94,7 +79,7 @@ class TLDR
94
79
  return tests if line_specific_locations.empty?
95
80
 
96
81
  tests.select { |test|
97
- locations_include_test? line_specific_locations, test
82
+ PathUtil.locations_include_test? line_specific_locations, test
98
83
  }
99
84
  end
100
85
 
@@ -133,29 +118,6 @@ class TLDR
133
118
  }
134
119
  end
135
120
 
136
- def locations_include_test? locations, test
137
- locations.any? { |location|
138
- location.file == test.file && (location.line.nil? || test.covers_line?(location.line))
139
- }
140
- end
141
-
142
- # Because search paths to TLDR can include line numbers (e.g. a.rb:4), we
143
- # can't just pass everything to Dir.glob. Instead, we have to check whether
144
- # a user-provided search path looks like a glob, and if so, expand it
145
- #
146
- # Globby characters specified here:
147
- # https://ruby-doc.org/3.2.2/Dir.html#method-c-glob
148
- def expand_globs search_paths
149
- search_paths.flat_map { |search_path|
150
- if search_path.match?(/[*?\[\]{}]/)
151
- raise Error, "Can't combine globs and line numbers in: #{search_path}" if search_path.match?(/:(\d+)$/)
152
- Dir[search_path]
153
- else
154
- search_path
155
- end
156
- }
157
- end
158
-
159
121
  def expand_names_with_patterns names
160
122
  names.map { |name|
161
123
  if name.is_a?(String) && name =~ /^\/(.*)\/$/
data/lib/tldr/rake.rb CHANGED
@@ -1,4 +1,41 @@
1
- desc "Run tests with TLDR (use TLDR_OPTS or .tldr.yml to configure)"
2
- task :tldr do
3
- fail unless system "#{"bundle exec " if defined?(Bundler)}tldr #{ENV["TLDR_OPTS"]}"
1
+ require "rake"
2
+ require "shellwords"
3
+
4
+ require "tldr"
5
+
6
+ class TLDR
7
+ class Task
8
+ include Rake::DSL
9
+
10
+ def initialize(name: "tldr", config: Config.new)
11
+ define name, config
12
+ end
13
+
14
+ private
15
+
16
+ def define name, task_config
17
+ desc "Run #{name} tests (use TLDR_OPTS or .tldr.yml to configure)"
18
+ task name do
19
+ cli_args = build_cli_args(task_config)
20
+ fail unless system "#{"bundle exec " if defined?(Bundler)}tldr #{cli_args}"
21
+ end
22
+ end
23
+
24
+ def build_cli_args task_config
25
+ config = if ENV["TLDR_OPTS"]
26
+ env_argv = Shellwords.shellwords(ENV["TLDR_OPTS"])
27
+ opts_config = ArgvParser.new.parse(env_argv, {
28
+ config_intended_for_merge_only: true
29
+ })
30
+ task_config.merge(opts_config)
31
+ else
32
+ task_config
33
+ end
34
+
35
+ config.to_full_args
36
+ end
37
+ end
4
38
  end
39
+
40
+ # Create the default tldr task for users
41
+ TLDR::Task.new
@@ -9,7 +9,7 @@ class TLDR
9
9
  def before_suite tests
10
10
  @suite_start_time = Process.clock_gettime Process::CLOCK_MONOTONIC, :microsecond
11
11
  @out.print <<~MSG
12
- Command: #{tldr_command} #{@config.to_full_args}
12
+ Command: #{tldr_command} #{@config.to_full_args}#{"\n#{@icons.seed} #{CONFLAGS[:seed]} #{@config.seed}" unless @config.seed_set_intentionally}
13
13
 
14
14
  #{@icons.run} Running:
15
15
 
@@ -17,12 +17,17 @@ class TLDR
17
17
  end
18
18
 
19
19
  def after_test result
20
- @out.print case result.type
20
+ output = case result.type
21
21
  when :success then @icons.success
22
22
  when :skip then @icons.skip
23
23
  when :failure then @icons.failure
24
24
  when :error then @icons.error
25
25
  end
26
+ if @config.verbose
27
+ @out.puts "#{output} #{result.type.capitalize} - #{describe(result.test, result.relevant_location)}"
28
+ else
29
+ @out.print output
30
+ end
26
31
  end
27
32
 
28
33
  def time_diff start, stop = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
@@ -43,6 +43,10 @@ module IconProvider
43
43
  def rock_on
44
44
  ""
45
45
  end
46
+
47
+ def seed
48
+ ""
49
+ end
46
50
  end
47
51
 
48
52
  class Emoji < Base
@@ -89,5 +93,9 @@ module IconProvider
89
93
  def rock_on
90
94
  "🤘"
91
95
  end
96
+
97
+ def seed
98
+ "🌱"
99
+ end
92
100
  end
93
101
  end
data/lib/tldr/runner.rb CHANGED
@@ -34,7 +34,7 @@ class TLDR
34
34
  end
35
35
  }
36
36
 
37
- results = @parallelizer.parallelize(plan.tests, config.parallel) { |test|
37
+ results = @parallelizer.parallelize(plan.tests, config) { |test|
38
38
  e = nil
39
39
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
40
40
  wip_test = WIPTest.new test, start_time
@@ -42,7 +42,16 @@ class TLDR
42
42
  runtime = time_it(start_time) do
43
43
  instance = test.klass.new
44
44
  instance.setup if instance.respond_to? :setup
45
- instance.send(test.method)
45
+ if instance.respond_to? :around
46
+ did_run = false
47
+ instance.around {
48
+ did_run = true
49
+ instance.send(test.method)
50
+ }
51
+ raise Error, "#{test.klass}#around failed to yield or call the passed test block" unless did_run
52
+ else
53
+ instance.send(test.method)
54
+ end
46
55
  instance.teardown if instance.respond_to? :teardown
47
56
  rescue Skip, Failure, StandardError => e
48
57
  end
@@ -1,6 +1,6 @@
1
1
  class TLDR
2
2
  class Strategizer
3
- Strategy = Struct.new :tests, :tests_and_groups
3
+ Strategy = Struct.new :prepend_thread_unsafe_tests, :parallel_tests_and_groups, :thread_unsafe_tests
4
4
 
5
5
  # Combine all discovered test methods with any methods grouped by run_these_together!
6
6
  #
@@ -8,10 +8,13 @@ class TLDR
8
8
  # - Map over tests to build out groups in order to retain shuffle order
9
9
  # (group will run in position of first test in the group)
10
10
  # - If a test is in multiple groups, only run it once
11
- def strategize tests, grouped_tests
12
- already_included_groups = []
11
+ def strategize all_tests, run_these_together_groups, thread_unsafe_test_groups, prepend_paths
12
+ thread_unsafe_tests, thread_safe_tests = partition_unsafe(all_tests, thread_unsafe_test_groups)
13
+ prepend_thread_unsafe_tests, thread_unsafe_tests = partition_prepend(thread_unsafe_tests, prepend_paths)
13
14
 
14
- Strategy.new tests, tests.map { |test|
15
+ grouped_tests = prepare_run_together_groups run_these_together_groups, thread_safe_tests, thread_unsafe_tests
16
+ already_included_groups = []
17
+ Strategy.new prepend_thread_unsafe_tests, thread_safe_tests.map { |test|
15
18
  if (group = grouped_tests.find { |group| group.tests.include? test })
16
19
  if already_included_groups.include? group
17
20
  next
@@ -25,7 +28,37 @@ class TLDR
25
28
  else
26
29
  test
27
30
  end
28
- }.compact
31
+ }.compact, thread_unsafe_tests
32
+ end
33
+
34
+ private
35
+
36
+ def partition_unsafe tests, thread_unsafe_test_groups
37
+ tests.partition { |test|
38
+ thread_unsafe_test_groups.any? { |group| group.tests.include? test }
39
+ }
40
+ end
41
+
42
+ # Sadly duplicative with Planner.rb, necessitating the extraction of PathUtil
43
+ # Suboptimal, but we do indeed need to do this work in two places ¯\_(ツ)_/¯
44
+ def partition_prepend thread_unsafe_tests, prepend_paths
45
+ locations = PathUtil.expand_search_locations PathUtil.expand_globs prepend_paths
46
+
47
+ thread_unsafe_tests.partition { |test|
48
+ PathUtil.locations_include_test? locations, test
49
+ }
50
+ end
51
+
52
+ def prepare_run_together_groups run_these_together_groups, thread_safe_tests, thread_unsafe_tests
53
+ grouped_tests = run_these_together_groups.map(&:dup)
54
+
55
+ grouped_tests.each do |group|
56
+ group.tests = group.tests.select { |test|
57
+ thread_safe_tests.include?(test) && !thread_unsafe_tests.include?(test)
58
+ }
59
+ end
60
+
61
+ grouped_tests.reject { |group| group.tests.size < 2 }
29
62
  end
30
63
  end
31
64
  end
@@ -10,7 +10,7 @@ class TLDR
10
10
  names: "--name",
11
11
  fail_fast: "--fail-fast",
12
12
  no_emoji: "--no-emoji",
13
- prepend_tests: "--prepend",
13
+ prepend_paths: "--prepend",
14
14
  no_prepend: "--no-prepend",
15
15
  exclude_paths: "--exclude-path",
16
16
  exclude_names: "--exclude-name",
@@ -19,25 +19,35 @@ class TLDR
19
19
  paths: nil
20
20
  }.freeze
21
21
 
22
- PATH_FLAGS = [:paths, :helper, :load_paths, :prepend_tests, :exclude_paths].freeze
22
+ PATH_FLAGS = [:paths, :helper, :load_paths, :prepend_paths, :exclude_paths].freeze
23
23
  MOST_RECENTLY_MODIFIED_TAG = "MOST_RECENTLY_MODIFIED".freeze
24
24
 
25
25
  Config = Struct.new :paths, :seed, :no_helper, :verbose, :reporter,
26
26
  :helper, :load_paths, :parallel, :names, :fail_fast, :no_emoji,
27
- :prepend_tests, :no_prepend, :exclude_paths, :exclude_names, :base_path,
27
+ :prepend_paths, :no_prepend, :exclude_paths, :exclude_names, :base_path,
28
28
  :no_dotfile,
29
- :seed_set_intentionally, :cli_mode, keyword_init: true do
29
+ # Internal properties
30
+ :config_intended_for_merge_only, :seed_set_intentionally, :cli_defaults,
31
+ keyword_init: true do
30
32
  def initialize(**args)
31
- change_working_directory_because_i_am_bad_and_i_should_feel_bad!(args[:base_path])
32
- args = merge_dotfile_args(args) unless args[:no_dotfile]
33
+ unless args[:config_intended_for_merge_only]
34
+ change_working_directory_because_i_am_bad_and_i_should_feel_bad!(args[:base_path])
35
+ args = merge_dotfile_args(args) unless args[:no_dotfile]
36
+ end
37
+ args = undefault_parallel_if_seed_set(args)
38
+ unless args[:config_intended_for_merge_only]
39
+ args = merge_defaults(args)
40
+ end
33
41
 
34
- super(**merge_defaults(args))
42
+ super(**args)
35
43
  end
36
44
 
37
- # Must be set when the Config is first initialized
38
- undef_method :cli_mode=, :no_dotfile=, :base_path=
45
+ # These are for internal tracking and resolved at initialization-time
46
+ undef_method :config_intended_for_merge_only=, :seed_set_intentionally=,
47
+ # These must be set when the Config is first initialized
48
+ :cli_defaults=, :no_dotfile=, :base_path=
39
49
 
40
- def self.build_defaults(cli_mode = false)
50
+ def self.build_defaults cli_defaults: true
41
51
  common = {
42
52
  seed: rand(10_000),
43
53
  no_helper: false,
@@ -53,36 +63,36 @@ class TLDR
53
63
  base_path: nil
54
64
  }
55
65
 
56
- if cli_mode
66
+ if cli_defaults
57
67
  common.merge(
58
68
  paths: Dir["test/**/*_test.rb", "test/**/test_*.rb"],
59
69
  helper: "test/helper.rb",
60
70
  load_paths: ["test"],
61
- prepend_tests: [MOST_RECENTLY_MODIFIED_TAG]
71
+ prepend_paths: [MOST_RECENTLY_MODIFIED_TAG]
62
72
  )
63
73
  else
64
74
  common.merge(
65
75
  paths: [],
66
76
  helper: nil,
67
77
  load_paths: [],
68
- prepend_tests: []
78
+ prepend_paths: []
69
79
  )
70
80
  end
71
81
  end
72
82
 
73
- def merge_defaults(user_args)
74
- merged_args = user_args.dup
75
- defaults = Config.build_defaults(merged_args[:cli_mode])
76
- merged_args[:seed_set_intentionally] = !merged_args[:seed].nil?
83
+ def undefault_parallel_if_seed_set args
84
+ args.merge(
85
+ seed_set_intentionally: !args[:seed].nil?,
86
+ parallel: (args[:parallel].nil? ? args[:seed].nil? : args[:parallel])
87
+ )
88
+ end
77
89
 
78
- # Special cases
79
- if merged_args[:parallel].nil?
80
- # Disable parallelization if seed is set
81
- merged_args[:parallel] = merged_args[:seed].nil?
82
- end
90
+ def merge_defaults user_args
91
+ merged_args = user_args.dup
92
+ defaults = Config.build_defaults(cli_defaults: merged_args[:cli_defaults])
83
93
 
84
94
  # Arrays
85
- [:paths, :load_paths, :names, :prepend_tests, :exclude_paths, :exclude_names].each do |key|
95
+ [:paths, :load_paths, :names, :prepend_paths, :exclude_paths, :exclude_names].each do |key|
86
96
  merged_args[key] = defaults[key] if merged_args[key].nil? || merged_args[key].empty?
87
97
  end
88
98
 
@@ -99,13 +109,21 @@ class TLDR
99
109
  merged_args
100
110
  end
101
111
 
112
+ def merge other
113
+ this_config = to_h
114
+ kwargs = this_config.merge(
115
+ other.to_h.compact.except(:config_intended_for_merge_only)
116
+ )
117
+ Config.new(**kwargs)
118
+ end
119
+
102
120
  # We needed this hook (to be called by the planner), because we can't know
103
121
  # the default prepend location until we have all the resolved test paths,
104
122
  # so we have to mutate it after the fact.
105
123
  def update_after_gathering_tests! tests
106
- return unless prepend_tests.include?(MOST_RECENTLY_MODIFIED_TAG)
124
+ return unless prepend_paths.include?(MOST_RECENTLY_MODIFIED_TAG)
107
125
 
108
- self.prepend_tests = prepend_tests.map { |path|
126
+ self.prepend_paths = prepend_paths.map { |path|
109
127
  if path == MOST_RECENTLY_MODIFIED_TAG
110
128
  most_recently_modified_test_file tests
111
129
  else
@@ -115,12 +133,17 @@ class TLDR
115
133
  end
116
134
 
117
135
  def to_full_args(exclude: [])
118
- to_cli_argv(CONFLAGS.keys - exclude).join(" ")
136
+ to_cli_argv(
137
+ CONFLAGS.keys -
138
+ exclude - [
139
+ (:seed unless seed_set_intentionally)
140
+ ]
141
+ ).join(" ")
119
142
  end
120
143
 
121
144
  def to_single_path_args(path)
122
145
  argv = to_cli_argv(CONFLAGS.keys - [
123
- :seed, :parallel, :names, :fail_fast, :paths, :prepend_tests,
146
+ :seed, :parallel, :names, :fail_fast, :paths, :prepend_paths,
124
147
  :no_prepend, :exclude_paths
125
148
  ])
126
149
 
@@ -129,14 +152,14 @@ class TLDR
129
152
 
130
153
  private
131
154
 
132
- def to_cli_argv(options = CONFLAGS.keys)
133
- defaults = Config.build_defaults(cli_mode)
155
+ def to_cli_argv options = CONFLAGS.keys
156
+ defaults = Config.build_defaults(cli_defaults: true)
134
157
  options.map { |key|
135
158
  flag = CONFLAGS[key]
136
159
 
137
160
  # Special cases
138
- if key == :prepend_tests
139
- if prepend_tests.map { |s| stringify(key, s) }.sort == paths.map { |s| stringify(:paths, s) }.sort
161
+ if key == :prepend_paths
162
+ if prepend_paths.map { |s| stringify(key, s) }.sort == paths.map { |s| stringify(:paths, s) }.sort
140
163
  # Don't print prepended tests if they're the same as the test paths
141
164
  next
142
165
  elsif no_prepend
data/lib/tldr/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class TLDR
2
- VERSION = "0.3.0"
2
+ VERSION = "0.5.0"
3
3
  end
data/lib/tldr.rb CHANGED
@@ -4,10 +4,11 @@ require_relative "tldr/argv_parser"
4
4
  require_relative "tldr/assertions"
5
5
  require_relative "tldr/backtrace_filter"
6
6
  require_relative "tldr/error"
7
+ require_relative "tldr/parallel_controls"
7
8
  require_relative "tldr/parallelizer"
9
+ require_relative "tldr/path_util"
8
10
  require_relative "tldr/planner"
9
11
  require_relative "tldr/reporters"
10
- require_relative "tldr/run_these_together"
11
12
  require_relative "tldr/runner"
12
13
  require_relative "tldr/skippable"
13
14
  require_relative "tldr/sorbet_compatibility"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tldr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Searls
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2023-09-26 00:00:00.000000000 Z
12
+ date: 2023-09-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: super_diff
@@ -60,14 +60,15 @@ files:
60
60
  - lib/tldr/assertions/minitest_compatibility.rb
61
61
  - lib/tldr/backtrace_filter.rb
62
62
  - lib/tldr/error.rb
63
+ - lib/tldr/parallel_controls.rb
63
64
  - lib/tldr/parallelizer.rb
65
+ - lib/tldr/path_util.rb
64
66
  - lib/tldr/planner.rb
65
67
  - lib/tldr/rake.rb
66
68
  - lib/tldr/reporters.rb
67
69
  - lib/tldr/reporters/base.rb
68
70
  - lib/tldr/reporters/default.rb
69
71
  - lib/tldr/reporters/icon_provider.rb
70
- - lib/tldr/run_these_together.rb
71
72
  - lib/tldr/runner.rb
72
73
  - lib/tldr/skippable.rb
73
74
  - lib/tldr/sorbet_compatibility.rb
@@ -1,23 +0,0 @@
1
- class TLDR
2
- GROUPED_TESTS = Concurrent::Array.new
3
-
4
- # If it's not safe to run a set of tests in parallel, you can force them to
5
- # run in a group together (in a single worker) with `run_these_together!` in
6
- # your test.
7
- #
8
- # This method takes an array of tuples, where the first element is the class
9
- # (or its name as a string, if the class is not yet defined in the current
10
- # file) and the second element is the method name. If the second element is
11
- # nil, then all the tests on the class will be run together.
12
- #
13
- # Examples:
14
- # - `run_these_together!` will run all the tests defined on the current
15
- # class to be run in a group
16
- # - `run_these_together!([[ClassA, nil], ["ClassB", :test_1], [ClassB, :test_2]])`
17
- # will run all the tests defined on ClassA, and test_1 and test_2 from ClassB
18
- # (nil in the second position means "all the tests on this class")
19
- #
20
- def self.run_these_together! klass_method_tuples = [[self, nil]]
21
- GROUPED_TESTS << TestGroup.new(klass_method_tuples)
22
- end
23
- end