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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dff20d2ad060e8ac1b1219978e871b9dc06014186429371dd2de8e929c092b78
4
- data.tar.gz: 5922e6fb5053898d5ff70162d9d542f759332db59f29cfbd89ab4aadfb7d5623
3
+ metadata.gz: 71122b11466d7a2682ace7dbba2258d2ead4a4aec723882b0c543bcbc3c12f39
4
+ data.tar.gz: efe76c0ab2ee09ba20921d56922630557ffb11704e3839a8a7322329b8fb5889
5
5
  SHA512:
6
- metadata.gz: 46165b260ba40d54acbc986258c9a7f94377960f05acaec34bba27b6dc16cde89a19884393b008d751f64c413d54c445a39202c493293f106fd7a2cbd32f06fa
7
- data.tar.gz: 60785c5e4bb01cc804636c770b299e1c08b1f97165a1459a17e8de59a178df2267e04e9ffb9a98e95aa9fc698aeb26139094b79b20c035d72a6ffa7780d89aa7
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
- file, line = SorbetCompatibility.unwrap_method(subklass.instance_method(method)).source_location
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
@@ -1,5 +1,3 @@
1
- require "concurrent"
2
-
3
1
  class TLDR
4
2
  CONFLAGS = {
5
3
  seed: "--seed",
@@ -1,10 +1,11 @@
1
1
  class TLDR
2
- Test = Struct.new :klass, :method, :file, :line do
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
- @location = Location.new(file, line)
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
@@ -4,3 +4,4 @@ require "tldr/value/test"
4
4
  require "tldr/value/wip_test"
5
5
  require "tldr/value/test_result"
6
6
  require "tldr/value/location"
7
+ require "tldr/value/test_group"
data/lib/tldr/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class TLDR
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/tldr.rb CHANGED
@@ -1,19 +1,30 @@
1
- require_relative "tldr/version"
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/value"
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.2.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.17
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.