tldr 0.2.0 → 0.3.0
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.
- 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.
|