tldr 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +45 -0
- data/lib/tldr/parallelizer.rb +36 -0
- data/lib/tldr/planner.rb +1 -2
- data/lib/tldr/run_these_together.rb +23 -0
- data/lib/tldr/runner.rb +2 -16
- data/lib/tldr/strategizer.rb +31 -0
- data/lib/tldr/value/config.rb +0 -2
- data/lib/tldr/value/test.rb +8 -3
- data/lib/tldr/value/test_group.rb +22 -0
- data/lib/tldr/value.rb +1 -0
- data/lib/tldr/version.rb +1 -1
- data/lib/tldr.rb +16 -5
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 71122b11466d7a2682ace7dbba2258d2ead4a4aec723882b0c543bcbc3c12f39
|
4
|
+
data.tar.gz: efe76c0ab2ee09ba20921d56922630557ffb11704e3839a8a7322329b8fb5889
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8a3dbacc10c845d976b89775ad762b5e37d2b6c2dbd657dc02422f7d356f98770b16ea485f242c7065ba688d8d69db078801735b41812f1f91a139bcd6d1ac31
|
7
|
+
data.tar.gz: ccc10568c6c506d7656fee1619f678dc32da38298d0ef33c7e2b613e849f9a323dc3105c49aea7ecf108bd705c92fd90dd6cab88fb57e69f0fb4a53e119144ab
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.3.0]
|
4
|
+
|
5
|
+
* Add `TLDR.run_these_together!` method to allow tests that can't safely be run
|
6
|
+
concurrently to be grouped and run together
|
7
|
+
|
8
|
+
## [0.2.1]
|
9
|
+
|
10
|
+
* Define a default empty setup/teardown in the base class, to guard against
|
11
|
+
users getting `super: no superclass method `setup'` errors when they dutifully
|
12
|
+
call super from their hooks
|
13
|
+
|
3
14
|
## [0.2.0]
|
4
15
|
|
5
16
|
- Add a rake task "tldr"
|
data/README.md
CHANGED
@@ -225,10 +225,55 @@ You could then run the task with:
|
|
225
225
|
$ TLDR_OPTS="--no-parallel" bundle exec rake tldr
|
226
226
|
```
|
227
227
|
|
228
|
+
### Parallel-by-default was a bold choice—also my tests are failing now, thanks
|
229
|
+
|
230
|
+
**Read this before you add `--no-parallel` because some tests are failing when
|
231
|
+
you run `tldr`.**
|
232
|
+
|
233
|
+
The vast majority of test suites in the wild are not parallelized and the vast
|
234
|
+
majority of _those_ will only parallelize by forking processes as opposed to
|
235
|
+
using a thread pool. We wanted to encourage more people to save time (after all,
|
236
|
+
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.
|
238
|
+
|
239
|
+
If you're writing new code and tests with TLDR and dutifully running `tldr`
|
240
|
+
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**.
|
248
|
+
|
249
|
+
So, when you see a test that is failing when run in parallel with the rest of your
|
250
|
+
suite, here is what we recommend doing, in priority order:
|
251
|
+
|
252
|
+
1. Figure out a way to redesign the test (or the code under test) to be
|
253
|
+
thread-safe. Modern versions of Ruby provide a number of tools to make this
|
254
|
+
easier than it used to be, and it may be as simple as making an instance
|
255
|
+
variable thread-local
|
256
|
+
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
|
258
|
+
group the tests together. This will ensure that those tests run in the same
|
259
|
+
thread in sequence (here's a a [simple
|
260
|
+
example](/tests/fixture/run_these_together.rb))
|
261
|
+
3. Give up and make the whole suite `--no-parallel`. If you find that you need
|
262
|
+
to resort to this, you might save some keystrokes by adding `parallel: false` in
|
263
|
+
a [.tldr.yml](#setting-defaults-in-tldryml) file
|
264
|
+
|
265
|
+
We have a couple other ideas of ways to incorporate non-thread-safe tests into
|
266
|
+
your suite without slowing down the rest of your tests, so stay tuned!
|
267
|
+
|
228
268
|
### How will I run all my tests in CI without the time bomb going off?
|
229
269
|
|
230
270
|
TLDR will run all your tests in CI without the time bomb going off.
|
231
271
|
|
272
|
+
### What about mocking?
|
273
|
+
|
274
|
+
TLDR is laser-focused on running tests. Might we interest you in a refreshing
|
275
|
+
[mocktail](https://github.com/testdouble/mocktail), instead?
|
276
|
+
|
232
277
|
## Acknowledgements
|
233
278
|
|
234
279
|
Thanks to [George Sheppard](https://github.com/fuzzmonkey) for freeing up the
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class TLDR
|
2
|
+
class Parallelizer
|
3
|
+
def initialize
|
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(
|
10
|
+
name: "tldr",
|
11
|
+
auto_terminate: true
|
12
|
+
)
|
13
|
+
|
14
|
+
strategy = @strategizer.strategize tests, GROUPED_TESTS
|
15
|
+
|
16
|
+
strategy.tests_and_groups.map { |test_or_group|
|
17
|
+
tests_to_run = if test_or_group.group?
|
18
|
+
test_or_group.tests.select { |test| tests.include? test }
|
19
|
+
else
|
20
|
+
[test_or_group]
|
21
|
+
end
|
22
|
+
|
23
|
+
unless tests_to_run.empty?
|
24
|
+
Concurrent::Promises.future_on(tldr_pool) {
|
25
|
+
tests_to_run.map(&blk)
|
26
|
+
}
|
27
|
+
end
|
28
|
+
}.compact.flat_map(&:value)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def substitute_tests_grouped_by_run_these_together! tests
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/tldr/planner.rb
CHANGED
@@ -50,8 +50,7 @@ class TLDR
|
|
50
50
|
def gather_tests
|
51
51
|
gather_descendants(TLDR).flat_map { |subklass|
|
52
52
|
subklass.instance_methods.grep(/^test_/).sort.map { |method|
|
53
|
-
|
54
|
-
Test.new subklass, method, file, line
|
53
|
+
Test.new subklass, method
|
55
54
|
}
|
56
55
|
}
|
57
56
|
end
|
@@ -0,0 +1,23 @@
|
|
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
|
data/lib/tldr/runner.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
require "irb"
|
2
|
-
require "concurrent"
|
3
2
|
|
4
3
|
class TLDR
|
5
4
|
class Runner
|
6
5
|
def initialize
|
6
|
+
@parallelizer = Parallelizer.new
|
7
7
|
@wip = Concurrent::Array.new
|
8
8
|
@results = Concurrent::Array.new
|
9
9
|
@run_aborted = Concurrent::AtomicBoolean.new false
|
@@ -34,7 +34,7 @@ class TLDR
|
|
34
34
|
end
|
35
35
|
}
|
36
36
|
|
37
|
-
results = parallelize(plan.tests, config.parallel) { |test|
|
37
|
+
results = @parallelizer.parallelize(plan.tests, config.parallel) { |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
|
@@ -65,20 +65,6 @@ class TLDR
|
|
65
65
|
|
66
66
|
private
|
67
67
|
|
68
|
-
def parallelize tests, parallel, &blk
|
69
|
-
return tests.map(&blk) if tests.size < 2 || !parallel
|
70
|
-
tldr_pool = Concurrent::ThreadPoolExecutor.new(
|
71
|
-
name: "tldr",
|
72
|
-
auto_terminate: true
|
73
|
-
)
|
74
|
-
|
75
|
-
tests.map { |test|
|
76
|
-
Concurrent::Promises.future_on(tldr_pool) {
|
77
|
-
blk.call test
|
78
|
-
}
|
79
|
-
}.flat_map(&:value)
|
80
|
-
end
|
81
|
-
|
82
68
|
def fail_fast reporter, plan, fast_failed_result
|
83
69
|
unless @run_aborted.true?
|
84
70
|
@run_aborted.make_true
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class TLDR
|
2
|
+
class Strategizer
|
3
|
+
Strategy = Struct.new :tests, :tests_and_groups
|
4
|
+
|
5
|
+
# Combine all discovered test methods with any methods grouped by run_these_together!
|
6
|
+
#
|
7
|
+
# Priorities:
|
8
|
+
# - Map over tests to build out groups in order to retain shuffle order
|
9
|
+
# (group will run in position of first test in the group)
|
10
|
+
# - If a test is in multiple groups, only run it once
|
11
|
+
def strategize tests, grouped_tests
|
12
|
+
already_included_groups = []
|
13
|
+
|
14
|
+
Strategy.new tests, tests.map { |test|
|
15
|
+
if (group = grouped_tests.find { |group| group.tests.include? test })
|
16
|
+
if already_included_groups.include? group
|
17
|
+
next
|
18
|
+
elsif (other = already_included_groups.find { |other| (group.tests & other.tests).any? })
|
19
|
+
other.tests |= group.tests
|
20
|
+
next
|
21
|
+
else
|
22
|
+
already_included_groups << group
|
23
|
+
group
|
24
|
+
end
|
25
|
+
else
|
26
|
+
test
|
27
|
+
end
|
28
|
+
}.compact
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/tldr/value/config.rb
CHANGED
data/lib/tldr/value/test.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
class TLDR
|
2
|
-
Test = Struct.new :klass, :method
|
3
|
-
attr_reader :location
|
2
|
+
Test = Struct.new :klass, :method do
|
3
|
+
attr_reader :file, :line, :location
|
4
4
|
|
5
5
|
def initialize(*args)
|
6
6
|
super
|
7
|
-
@
|
7
|
+
@file, @line = SorbetCompatibility.unwrap_method(klass.instance_method(method)).source_location
|
8
|
+
@location = Location.new file, line
|
8
9
|
end
|
9
10
|
|
10
11
|
# Memoizing at call time, because re-parsing isn't free and isn't usually necessary
|
@@ -19,5 +20,9 @@ class TLDR
|
|
19
20
|
def covers_line? l
|
20
21
|
line == l || (l >= line && l <= end_line)
|
21
22
|
end
|
23
|
+
|
24
|
+
def group?
|
25
|
+
false
|
26
|
+
end
|
22
27
|
end
|
23
28
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class TLDR
|
2
|
+
TestGroup = Struct.new :configuration do
|
3
|
+
attr_writer :tests
|
4
|
+
|
5
|
+
def tests
|
6
|
+
@tests ||= configuration.flat_map { |(klass, method)|
|
7
|
+
klass = Kernel.const_get(klass) if klass.is_a? String
|
8
|
+
if method.nil?
|
9
|
+
klass.instance_methods.grep(/^test_/).sort.map { |method|
|
10
|
+
Test.new klass, method
|
11
|
+
}
|
12
|
+
else
|
13
|
+
Test.new klass, method
|
14
|
+
end
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def group?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/tldr/value.rb
CHANGED
data/lib/tldr/version.rb
CHANGED
data/lib/tldr.rb
CHANGED
@@ -1,19 +1,30 @@
|
|
1
|
-
|
1
|
+
require "concurrent-ruby"
|
2
|
+
|
3
|
+
require_relative "tldr/argv_parser"
|
4
|
+
require_relative "tldr/assertions"
|
2
5
|
require_relative "tldr/backtrace_filter"
|
3
6
|
require_relative "tldr/error"
|
4
|
-
require_relative "tldr/
|
5
|
-
require_relative "tldr/reporters"
|
6
|
-
require_relative "tldr/argv_parser"
|
7
|
+
require_relative "tldr/parallelizer"
|
7
8
|
require_relative "tldr/planner"
|
9
|
+
require_relative "tldr/reporters"
|
10
|
+
require_relative "tldr/run_these_together"
|
8
11
|
require_relative "tldr/runner"
|
9
|
-
require_relative "tldr/assertions"
|
10
12
|
require_relative "tldr/skippable"
|
11
13
|
require_relative "tldr/sorbet_compatibility"
|
14
|
+
require_relative "tldr/strategizer"
|
15
|
+
require_relative "tldr/value"
|
16
|
+
require_relative "tldr/version"
|
12
17
|
|
13
18
|
class TLDR
|
14
19
|
include Assertions
|
15
20
|
include Skippable
|
16
21
|
|
22
|
+
def setup
|
23
|
+
end
|
24
|
+
|
25
|
+
def teardown
|
26
|
+
end
|
27
|
+
|
17
28
|
module Run
|
18
29
|
def self.cli argv
|
19
30
|
config = ArgvParser.new.parse argv
|
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.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Searls
|
@@ -60,20 +60,24 @@ 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/parallelizer.rb
|
63
64
|
- lib/tldr/planner.rb
|
64
65
|
- lib/tldr/rake.rb
|
65
66
|
- lib/tldr/reporters.rb
|
66
67
|
- lib/tldr/reporters/base.rb
|
67
68
|
- lib/tldr/reporters/default.rb
|
68
69
|
- lib/tldr/reporters/icon_provider.rb
|
70
|
+
- lib/tldr/run_these_together.rb
|
69
71
|
- lib/tldr/runner.rb
|
70
72
|
- lib/tldr/skippable.rb
|
71
73
|
- lib/tldr/sorbet_compatibility.rb
|
74
|
+
- lib/tldr/strategizer.rb
|
72
75
|
- lib/tldr/value.rb
|
73
76
|
- lib/tldr/value/config.rb
|
74
77
|
- lib/tldr/value/location.rb
|
75
78
|
- lib/tldr/value/plan.rb
|
76
79
|
- lib/tldr/value/test.rb
|
80
|
+
- lib/tldr/value/test_group.rb
|
77
81
|
- lib/tldr/value/test_result.rb
|
78
82
|
- lib/tldr/value/wip_test.rb
|
79
83
|
- lib/tldr/version.rb
|
@@ -101,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
101
105
|
- !ruby/object:Gem::Version
|
102
106
|
version: '0'
|
103
107
|
requirements: []
|
104
|
-
rubygems_version: 3.4.
|
108
|
+
rubygems_version: 3.4.6
|
105
109
|
signing_key:
|
106
110
|
specification_version: 4
|
107
111
|
summary: TLDR will run your tests, but only for 1.8 seconds.
|