tldr 0.7.0 → 0.9.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: 3db38676fc9f10ee01f3332c6dfcafb5dc7c0b5405d9c687d13ba3c5b5be008e
4
- data.tar.gz: 874cbd810ed0ebff0e2bc92bba4d703903c10beb51220817314a9e6528308a01
3
+ metadata.gz: c9ae325a1d7b5c16b255bce28975b4e9c08bc23291932a02d91d02d46c247655
4
+ data.tar.gz: 0e92807c78187999ea26dba8f3ff48aa8ec6442b8884def3b7fc6b58db4127f0
5
5
  SHA512:
6
- metadata.gz: c79d10782d1d15ba8bd782358ad1b45ecbbc7096542aba5d487fd97822a61163049247a6cff2e050de95226e112653418b5cc656278a0fdf55cd7c5b09ce5ccb
7
- data.tar.gz: 429100174c63d3e9d917ef7a5b4f5ff14f105453c53a854a21369595272507b47e4629bd50edd067a6aaf46986f583f8327b9e4f66555e390440c86cd584c04b
6
+ metadata.gz: 79a8a3c45bc03731c80e5d7b00103d69efe4a1185c758fdecde1451864677af433126634a717dd912d7032556562cbe3894b1e7db75ab811412c1cc9097a457e
7
+ data.tar.gz: f27b99c4502b3a803d9c0507a1a9888715e8d203e1fc97bcd3284baa34afed4d9f55f2c5c06424c49645526c92534a54b52946867606a89b738dcb9c481a4c5d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## [0.9.0]
2
+
3
+ * Add a `--watch` option that will spawn fswatch | xargs and clear the screen
4
+ between runs (requires fswatch to gbe installed)
5
+ * Add "lib" as a default load path along with "test"
6
+
7
+ ## [0.8.0]
8
+
9
+ * Add a `--yes-i-know` flag that will suppress the large warning when your test
10
+ suite runs over the 1.8s limit
11
+
1
12
  ## [0.7.0]
2
13
 
3
14
  * Add a `tldt` alias for folks who have another executable named `tldr` on their
data/README.md CHANGED
@@ -116,14 +116,17 @@ Usage: tldr [options] some_tests/**/*.rb some/path.rb:13 ...
116
116
  --no-helper Don't require any test helpers
117
117
  --prepend PATH Prepend one or more paths to run before the rest (Default: most recently modified test)
118
118
  --no-prepend Don't prepend any tests before the rest of the suite
119
- -l, --load-path PATH Add one or more paths to the $LOAD_PATH (Default: ["test"])
119
+ -l, --load-path PATH Add one or more paths to the $LOAD_PATH (Default: ["lib", "test"])
120
120
  -r, --reporter REPORTER Set a custom reporter class (Default: "TLDR::Reporters::Default")
121
121
  --base-path PATH Change the working directory for all relative paths (Default: current working directory)
122
122
  --no-dotfile Disable loading .tldr.yml dotfile
123
123
  --no-emoji Disable emoji in the output
124
124
  -v, --verbose Print stack traces for errors
125
125
  --[no-]warnings Print Ruby warnings (Default: true)
126
- --comment COMMENT No-op comment, used internally for multi-line execution instructions
126
+ --watch Run your tests continuously on file save (requires 'fswatch' to be installed)
127
+ --yes-i-know Suppress TLDR report when suite runs over 1.8s
128
+ --i-am-being-watched [INTERNAL] Signals to tldr it is being invoked under --watch mode
129
+ --comment COMMENT [INTERNAL] No-op; used for multi-line execution instructions
127
130
  ```
128
131
 
129
132
  After being parsed, all the CLI options are converted into a
@@ -160,6 +163,17 @@ with these caveats:
160
163
  TLDR::Assertions::MinitestCompatibility` into the `TLDR` base class or
161
164
  individual test classesJust set it
162
165
 
166
+ ### Running tests continuously with --watch
167
+
168
+ The `tldr` CLI includes a `--watch` option which will watch for changes in any
169
+ of the configured load paths (`["test", "lib"]` by default) and then execute
170
+ your tests each time a file is changed. To keep the output up-to-date and easy
171
+ to scan, it will also clear your console before each run.
172
+
173
+ Here's what that might look like:
174
+
175
+ ![tldr-watch](https://github.com/tendersearls/tldr/assets/79303/364f0e52-5596-49ce-a470-5eaeddd11f03)
176
+
163
177
  ### Running TLDR with Rake
164
178
 
165
179
  TLDR ships with a [very](lib/tldr/rake.rb) minimal rake task that simply shells
@@ -239,8 +253,9 @@ encountered multiple times, only the first hook will be registered. If the
239
253
 
240
254
  #### Setting up the load path
241
255
 
242
- When running TLDR from a Ruby script, one thing the framework can't help you with
243
- is setting up load paths for you.
256
+ By default, the `tldr` CLI adds `test` and `lib` directories to the load path
257
+ for you, but when running TLDR from a Ruby script, it doesn't set those up for
258
+ you.
244
259
 
245
260
  If you want to require code in `test/` or `lib/` without using
246
261
  `require_relative`, you'll need to add those directories to the load path. You
@@ -336,12 +351,26 @@ with the `--reporter` command line option. It can be set to any fully-qualified
336
351
  class name that extends from
337
352
  [TLDR::Reporters::Base](/lib/tldr/reporters/base.rb).
338
353
 
354
+ ### I know my tests are over 1.8s, how do I suppress the huge output?
355
+
356
+ Plenty of test suites are over 1.8s and having TLDR repeatedly print out the
357
+ huge summary at the end of each test run can be distracting and make it harder
358
+ to spot test failures. If you know your test suite is too slow, you can simply
359
+ add the `--yes-i-know` flag
360
+
339
361
  ### What about mocking?
340
362
 
341
363
  TLDR is laser-focused on running tests, so it doesn't provide a built-in mocking
342
364
  facility. Might we interest you in a refreshing
343
365
  [mocktail](https://github.com/testdouble/mocktail), instead?
344
366
 
367
+ ## Contributing to TLDR
368
+
369
+ If you want to submit PRs on this repo, please know that the code style is
370
+ [Kirkland-style Ruby](https://mastodon.social/@searls/111137666157318482), where
371
+ method definitions have parentheses omitted but parentheses are generally
372
+ expected for method invocations.
373
+
345
374
  ## Acknowledgements
346
375
 
347
376
  Thanks to [George Sheppard](https://github.com/fuzzmonkey) for freeing up the
@@ -4,7 +4,7 @@ class TLDR
4
4
  class ArgvParser
5
5
  PATTERN_FRIENDLY_SPLITTER = /,(?=(?:[^\/]*\/[^\/]*\/)*[^\/]*$)/
6
6
 
7
- def parse(args, options = {cli_defaults: true})
7
+ def parse args, options = {cli_defaults: true}
8
8
  OptionParser.new do |opts|
9
9
  opts.banner = "Usage: tldr [options] some_tests/**/*.rb some/path.rb:13 ..."
10
10
 
@@ -22,12 +22,12 @@ class TLDR
22
22
 
23
23
  opts.on "-n", "#{CONFLAGS[:names]} PATTERN", "One or more names or /patterns/ of tests to run (like: foo_test, /test_foo.*/, Foo#foo_test)" do |name|
24
24
  options[:names] ||= []
25
- options[:names] += name.split PATTERN_FRIENDLY_SPLITTER
25
+ options[:names] += name.split(PATTERN_FRIENDLY_SPLITTER)
26
26
  end
27
27
 
28
28
  opts.on "#{CONFLAGS[:exclude_names]} PATTERN", "One or more names or /patterns/ NOT to run" do |exclude_name|
29
29
  options[:exclude_names] ||= []
30
- options[:exclude_names] += exclude_name.split PATTERN_FRIENDLY_SPLITTER
30
+ options[:exclude_names] += exclude_name.split(PATTERN_FRIENDLY_SPLITTER)
31
31
  end
32
32
 
33
33
  opts.on "#{CONFLAGS[:exclude_paths]} PATH", Array, "One or more paths NOT to run (like: foo.rb, \"test/bar/**\", baz.rb:3)" do |path|
@@ -53,7 +53,7 @@ class TLDR
53
53
  options[:no_prepend] = true
54
54
  end
55
55
 
56
- opts.on "-l", "#{CONFLAGS[:load_paths]} PATH", Array, "Add one or more paths to the $LOAD_PATH (Default: [\"test\"])" do |load_path|
56
+ opts.on "-l", "#{CONFLAGS[:load_paths]} PATH", Array, "Add one or more paths to the $LOAD_PATH (Default: [\"lib\", \"test\"])" do |load_path|
57
57
  options[:load_paths] ||= []
58
58
  options[:load_paths] += load_path
59
59
  end
@@ -82,7 +82,19 @@ class TLDR
82
82
  options[:warnings] = warnings
83
83
  end
84
84
 
85
- opts.on "--comment COMMENT", String, "No-op comment, used internally for multi-line execution instructions" do
85
+ opts.on CONFLAGS[:watch], "Run your tests continuously on file save (requires 'fswatch' to be installed)" do
86
+ options[:watch] = true
87
+ end
88
+
89
+ opts.on CONFLAGS[:yes_i_know], "Suppress TLDR report when suite runs over 1.8s" do
90
+ options[:yes_i_know] = true
91
+ end
92
+
93
+ opts.on CONFLAGS[:i_am_being_watched], "[INTERNAL] Signals to tldr it is being invoked under --watch mode" do
94
+ options[:i_am_being_watched] = true
95
+ end
96
+
97
+ opts.on "--comment COMMENT", String, "[INTERNAL] No-op; used for multi-line execution instructions" do
86
98
  # See "--comment" in lib/tldr/reporters/default.rb for an example of how this is used internally
87
99
  end
88
100
  end.parse!(args)
@@ -34,7 +34,7 @@ class TLDR
34
34
  assert receiver.__send__(method, *args), message
35
35
  end
36
36
 
37
- def capture_io(&blk)
37
+ def capture_io &blk
38
38
  Assertions.capture_io(&blk)
39
39
  end
40
40
 
@@ -74,7 +74,7 @@ class TLDR
74
74
  end
75
75
 
76
76
  def assert_equal expected, actual, message = nil
77
- message = Assertions.msg(message) { Assertions.diff expected, actual }
77
+ message = Assertions.msg(message) { Assertions.diff(expected, actual) }
78
78
  assert expected == actual, message
79
79
  end
80
80
 
@@ -158,7 +158,7 @@ class TLDR
158
158
  "Expected #{Assertions.h(actual)} to match #{Assertions.h(matcher)}"
159
159
  }
160
160
  assert_respond_to matcher, :=~
161
- matcher = Regexp.new Regexp.escape matcher if String === matcher
161
+ matcher = Regexp.new(Regexp.escape(matcher)) if String === matcher
162
162
  assert matcher =~ actual, message
163
163
  Regexp.last_match
164
164
  end
@@ -294,7 +294,7 @@ class TLDR
294
294
  "---Backtrace---",
295
295
  TLDR.filter_backtrace(e.backtrace).join("\n"),
296
296
  "---------------"
297
- ].compact.join "\n"
297
+ ].compact.join("\n")
298
298
  }
299
299
  end
300
300
 
@@ -14,19 +14,19 @@ class TLDR
14
14
  private
15
15
 
16
16
  def trim_leading_frames backtrace
17
- if (trimmed = backtrace.take_while { |frame| meaningful? frame }).any?
17
+ if (trimmed = backtrace.take_while { |frame| meaningful?(frame) }).any?
18
18
  trimmed
19
19
  end
20
20
  end
21
21
 
22
22
  def trim_internal_frames backtrace
23
- if (trimmed = backtrace.select { |frame| meaningful? frame }).any?
23
+ if (trimmed = backtrace.select { |frame| meaningful?(frame) }).any?
24
24
  trimmed
25
25
  end
26
26
  end
27
27
 
28
28
  def meaningful? frame
29
- !internal? frame
29
+ !internal?(frame)
30
30
  end
31
31
 
32
32
  def internal? frame
@@ -35,6 +35,6 @@ class TLDR
35
35
  end
36
36
 
37
37
  def self.filter_backtrace backtrace
38
- BacktraceFilter.new.filter backtrace
38
+ BacktraceFilter.new.filter(backtrace)
39
39
  end
40
40
  end
@@ -2,13 +2,13 @@ class TLDR
2
2
  module ClassUtil
3
3
  def self.gather_descendants root_klass
4
4
  root_klass.subclasses + root_klass.subclasses.flat_map { |subklass|
5
- gather_descendants subklass
5
+ gather_descendants(subklass)
6
6
  }
7
7
  end
8
8
 
9
9
  def self.gather_tests klass
10
10
  klass.instance_methods.grep(/^test_/).sort.map { |method|
11
- Test.new klass, method
11
+ Test.new(klass, method)
12
12
  }
13
13
  end
14
14
  end
@@ -1,7 +1,7 @@
1
1
  class TLDR
2
2
  module PathUtil
3
3
  def self.expand_paths path_strings, globs: true
4
- path_strings = expand_globs path_strings if globs
4
+ path_strings = expand_globs(path_strings) if globs
5
5
 
6
6
  path_strings.flat_map { |path_string|
7
7
  File.directory?(path_string) ? Dir["#{path_string}/**/*.rb"] : path_string
@@ -10,7 +10,7 @@ class TLDR
10
10
  line_numbers = path_string.scan(/:(\d+)/).flatten.map(&:to_i)
11
11
 
12
12
  if line_numbers.any?
13
- line_numbers.map { |line_number| Location.new absolute_path, line_number }
13
+ line_numbers.map { |line_number| Location.new(absolute_path, line_number) }
14
14
  else
15
15
  [Location.new(absolute_path, nil)]
16
16
  end
data/lib/tldr/planner.rb CHANGED
@@ -8,14 +8,14 @@ class TLDR
8
8
 
9
9
  def plan config
10
10
  $VERBOSE = config.warnings
11
- search_locations = PathUtil.expand_paths config.paths, globs: false
11
+ search_locations = PathUtil.expand_paths(config.paths, globs: false)
12
12
 
13
- prepend_load_paths config
14
- require_test_helper config
15
- require_tests search_locations
13
+ prepend_load_paths(config)
14
+ require_test_helper(config)
15
+ require_tests(search_locations)
16
16
 
17
17
  tests = gather_tests
18
- config.update_after_gathering_tests! tests
18
+ config.update_after_gathering_tests!(tests)
19
19
  tests_to_run = prepend(
20
20
  shuffle(
21
21
  exclude_by_path(
@@ -40,7 +40,7 @@ class TLDR
40
40
  config
41
41
  )
42
42
 
43
- Plan.new tests_to_run, strategy
43
+ Plan.new(tests_to_run, strategy)
44
44
  end
45
45
 
46
46
  private
@@ -53,9 +53,9 @@ class TLDR
53
53
 
54
54
  def prepend tests, config
55
55
  return tests if config.no_prepend
56
- prepended_locations = PathUtil.expand_paths config.prepend_paths
56
+ prepended_locations = PathUtil.expand_paths(config.prepend_paths)
57
57
  prepended, rest = tests.partition { |test|
58
- PathUtil.locations_include_test? prepended_locations, test
58
+ PathUtil.locations_include_test?(prepended_locations, test)
59
59
  }
60
60
  prepended + rest
61
61
  end
@@ -65,18 +65,18 @@ class TLDR
65
65
  end
66
66
 
67
67
  def exclude_by_path tests, exclude_paths
68
- excluded_locations = PathUtil.expand_paths exclude_paths
68
+ excluded_locations = PathUtil.expand_paths(exclude_paths)
69
69
  return tests if excluded_locations.empty?
70
70
 
71
71
  tests.reject { |test|
72
- PathUtil.locations_include_test? excluded_locations, test
72
+ PathUtil.locations_include_test?(excluded_locations, test)
73
73
  }
74
74
  end
75
75
 
76
76
  def exclude_by_name tests, exclude_names
77
77
  return tests if exclude_names.empty?
78
78
 
79
- name_excludes = expand_names_with_patterns exclude_names
79
+ name_excludes = expand_names_with_patterns(exclude_names)
80
80
 
81
81
  tests.reject { |test|
82
82
  name_excludes.any? { |filter|
@@ -90,14 +90,14 @@ class TLDR
90
90
  return tests if line_specific_locations.empty?
91
91
 
92
92
  tests.select { |test|
93
- PathUtil.locations_include_test? line_specific_locations, test
93
+ PathUtil.locations_include_test?(line_specific_locations, test)
94
94
  }
95
95
  end
96
96
 
97
97
  def filter_by_name tests, names
98
98
  return tests if names.empty?
99
99
 
100
- name_filters = expand_names_with_patterns names
100
+ name_filters = expand_names_with_patterns(names)
101
101
 
102
102
  tests.select { |test|
103
103
  name_filters.any? { |filter|
@@ -108,7 +108,7 @@ class TLDR
108
108
 
109
109
  def prepend_load_paths config
110
110
  config.load_paths.each do |load_path|
111
- $LOAD_PATH.unshift File.expand_path(load_path, Dir.pwd)
111
+ $LOAD_PATH.unshift(File.expand_path(load_path, Dir.pwd))
112
112
  end
113
113
  end
114
114
 
@@ -130,7 +130,7 @@ class TLDR
130
130
  def expand_names_with_patterns names
131
131
  names.map { |name|
132
132
  if name.is_a?(String) && name =~ /^\/(.*)\/$/
133
- Regexp.new $1
133
+ Regexp.new($1)
134
134
  else
135
135
  name
136
136
  end
data/lib/tldr/rake.rb CHANGED
@@ -7,7 +7,7 @@ class TLDR
7
7
  class Task
8
8
  include Rake::DSL
9
9
 
10
- def initialize(name: "tldr", config: Config.new)
10
+ def initialize name: "tldr", config: Config.new
11
11
  define name, config
12
12
  end
13
13
 
@@ -1,7 +1,7 @@
1
1
  class TLDR
2
2
  module Reporters
3
3
  class Base
4
- def initialize(config, out = $stdout, err = $stderr)
4
+ def initialize config, out = $stdout, err = $stderr
5
5
  out.sync = true
6
6
  err.sync = true
7
7
 
@@ -1,13 +1,14 @@
1
1
  class TLDR
2
2
  module Reporters
3
3
  class Default < Base
4
- def initialize(config, out = $stdout, err = $stderr)
4
+ def initialize config, out = $stdout, err = $stderr
5
5
  super
6
6
  @icons = @config.no_emoji ? IconProvider::Base.new : IconProvider::Emoji.new
7
7
  end
8
8
 
9
9
  def before_suite tests
10
- @suite_start_time = Process.clock_gettime Process::CLOCK_MONOTONIC, :microsecond
10
+ clear_screen_if_being_watched!
11
+ @suite_start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
11
12
  @out.print <<~MSG
12
13
  Command: #{tldr_command} #{@config.to_full_args}
13
14
  #{@icons.seed} #{CONFLAGS[:seed]} #{@config.seed}
@@ -31,32 +32,34 @@ class TLDR
31
32
  end
32
33
  end
33
34
 
34
- def time_diff start, stop = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
35
- ((stop - start) / 1000.0).round
36
- end
37
-
38
35
  def after_tldr planned_tests, wip_tests, test_results
39
- stop_time = Process.clock_gettime Process::CLOCK_MONOTONIC, :microsecond
36
+ stop_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
40
37
 
41
38
  @out.print @icons.tldr
42
39
  @err.print "\n\n"
43
- wrap_in_horizontal_rule do
44
- @err.print [
45
- "too long; didn't run!",
46
- "#{@icons.run} Completed #{test_results.size} of #{planned_tests.size} tests (#{((test_results.size.to_f / planned_tests.size) * 100).round}%) before running out of time.",
47
- (<<~WIP.chomp if wip_tests.any?),
48
- #{@icons.wip} #{plural wip_tests.size, "test was", "tests were"} cancelled in progress:
49
- #{wip_tests.map { |wip_test| " #{time_diff(wip_test.start_time, stop_time)}ms - #{describe(wip_test.test)}" }.join("\n")}
50
- WIP
51
- (<<~SLOW.chomp if test_results.any?),
52
- #{@icons.slow} Your #{[10, test_results.size].min} slowest completed tests:
53
- #{test_results.sort_by(&:runtime).last(10).reverse.map { |result| " #{result.runtime}ms - #{describe(result.test)}" }.join("\n")}
54
- SLOW
55
- describe_tests_that_didnt_finish(planned_tests, test_results)
56
- ].compact.join("\n\n")
40
+
41
+ if @config.yes_i_know
42
+ @err.print "🚨 TLDR! Display summary by omitting --yes-i-know"
43
+ else
44
+ wrap_in_horizontal_rule do
45
+ @err.print [
46
+ "too long; didn't run!",
47
+ "#{@icons.run} Completed #{test_results.size} of #{planned_tests.size} tests (#{((test_results.size.to_f / planned_tests.size) * 100).round}%) before running out of time.",
48
+ (<<~WIP.chomp if wip_tests.any?),
49
+ #{@icons.wip} #{plural(wip_tests.size, "test was", "tests were")} cancelled in progress:
50
+ #{wip_tests.map { |wip_test| " #{time_diff(wip_test.start_time, stop_time)}ms - #{describe(wip_test.test)}" }.join("\n")}
51
+ WIP
52
+ (<<~SLOW.chomp if test_results.any?),
53
+ #{@icons.slow} Your #{[10, test_results.size].min} slowest completed tests:
54
+ #{test_results.sort_by(&:runtime).last(10).reverse.map { |result| " #{result.runtime}ms - #{describe(result.test)}" }.join("\n")}
55
+ SLOW
56
+ describe_tests_that_didnt_finish(planned_tests, test_results),
57
+ "🙈 Suppress this summary with --yes-i-know"
58
+ ].compact.join("\n\n")
59
+ end
57
60
  end
58
61
 
59
- after_suite test_results
62
+ after_suite(test_results)
60
63
  end
61
64
 
62
65
  def after_fail_fast planned_tests, wip_tests, test_results, last_result
@@ -66,17 +69,17 @@ class TLDR
66
69
  wrap_in_horizontal_rule do
67
70
  @err.print [
68
71
  "Failing fast after #{describe(last_result.test, last_result.relevant_location)} #{last_result.error? ? "errored" : "failed"}.",
69
- ("#{@icons.wip} #{plural wip_tests.size, "test was", "tests were"} cancelled in progress." if wip_tests.any?),
70
- ("#{@icons.not_run} #{plural unrun_tests.size, "test was", "tests were"} not run at all." if unrun_tests.any?),
72
+ ("#{@icons.wip} #{plural(wip_tests.size, "test was", "tests were")} cancelled in progress." if wip_tests.any?),
73
+ ("#{@icons.not_run} #{plural(unrun_tests.size, "test was", "tests were")} not run at all." if unrun_tests.any?),
71
74
  describe_tests_that_didnt_finish(planned_tests, test_results)
72
75
  ].compact.join("\n\n")
73
76
  end
74
77
 
75
- after_suite test_results
78
+ after_suite(test_results)
76
79
  end
77
80
 
78
81
  def after_suite test_results
79
- duration = time_diff @suite_start_time
82
+ duration = time_diff(@suite_start_time)
80
83
  test_results = test_results.sort_by { |result| [result.test.location.file, result.test.location.line] }
81
84
 
82
85
  @err.print summarize_failures(test_results).join("\n\n")
@@ -102,11 +105,15 @@ class TLDR
102
105
 
103
106
  private
104
107
 
108
+ def time_diff start, stop = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
109
+ ((stop - start) / 1000.0).round
110
+ end
111
+
105
112
  def summarize_failures results
106
113
  failures = results.select { |result| result.failing? }
107
114
  return failures if failures.empty?
108
115
 
109
- ["\n\nFailing tests:"] + failures.map.with_index { |result, i| summarize_result result, i }
116
+ ["\n\nFailing tests:"] + failures.map.with_index { |result, i| summarize_result(result, i) }
110
117
  end
111
118
 
112
119
  def summarize_result result, index
@@ -138,22 +145,22 @@ class TLDR
138
145
  rule = @icons.alarm + "=" * 20 + " ABORTED RUN " + "=" * 20 + @icons.alarm
139
146
  @err.print "#{rule}\n\n"
140
147
  yield
141
- @err.print "\n\n#{rule}\n\n"
148
+ @err.print "\n\n#{rule}"
142
149
  end
143
150
 
144
151
  def describe_tests_that_didnt_finish planned_tests, test_results
145
152
  unrun = planned_tests - test_results.map(&:test)
146
153
  return if unrun.empty?
147
154
 
148
- unrun_locators = consolidate unrun
155
+ unrun_locators = consolidate(unrun)
149
156
  failed = test_results.select(&:failing?).map(&:test)
150
- failed_locators = consolidate failed, exclude: unrun_locators
157
+ failed_locators = consolidate(failed, exclude: unrun_locators)
151
158
  suggested_locators = unrun_locators + [
152
- ("--comment \"Also include #{plural failed.size, "test"} that failed:\"" if failed_locators.any?)
153
- ] + failed_locators
159
+ ("--comment \"Also include #{plural(failed.size, "test")} that failed:\"" if failed_locators.any?)
160
+ ].compact + failed_locators
154
161
  <<~MSG
155
- #{@icons.rock_on} Run the #{plural unrun.size, "test"} that didn't finish:
156
- #{tldr_command} #{@config.to_full_args exclude: [:paths]} #{suggested_locators.join(" \\\n ")}
162
+ #{@icons.rock_on} Run the #{plural(unrun.size, "test")} that didn't finish:
163
+ #{tldr_command} #{@config.to_full_args(exclude: [:paths])} #{suggested_locators.join(" \\\n ")}
157
164
  MSG
158
165
  end
159
166
 
@@ -166,6 +173,12 @@ class TLDR
166
173
  def tldr_command
167
174
  "#{"bundle exec " if defined?(Bundler)}tldr"
168
175
  end
176
+
177
+ def clear_screen_if_being_watched!
178
+ if @config.i_am_being_watched
179
+ @out.print "\e[2J\e[f"
180
+ end
181
+ end
169
182
  end
170
183
  end
171
184
  end
data/lib/tldr/runner.rb CHANGED
@@ -6,7 +6,7 @@ class TLDR
6
6
  @executor = Executor.new
7
7
  @wip = Concurrent::Array.new
8
8
  @results = Concurrent::Array.new
9
- @run_aborted = Concurrent::AtomicBoolean.new false
9
+ @run_aborted = Concurrent::AtomicBoolean.new(false)
10
10
  end
11
11
 
12
12
  def run config, plan
@@ -20,11 +20,11 @@ class TLDR
20
20
  next if ENV["CI"] && !$stderr.tty?
21
21
  next if @run_aborted.true?
22
22
  @run_aborted.make_true
23
- reporter.after_tldr plan.tests, @wip.dup, @results.dup
24
- exit! 3
23
+ reporter.after_tldr(plan.tests, @wip.dup, @results.dup)
24
+ exit!(3)
25
25
  end
26
26
 
27
- sleep 1.8
27
+ sleep(1.8)
28
28
  # Don't hard-kill the runner if user is debugging, it'll
29
29
  # screw up their terminal slash be a bad time
30
30
  if IRB.CurrentContext
@@ -41,8 +41,8 @@ class TLDR
41
41
  end
42
42
 
43
43
  unless @run_aborted.true?
44
- reporter.after_suite results
45
- exit exit_code results
44
+ reporter.after_suite(results)
45
+ exit(exit_code(results))
46
46
  end
47
47
  end
48
48
 
@@ -51,12 +51,12 @@ class TLDR
51
51
  def run_test test, config, plan, reporter
52
52
  e = nil
53
53
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
54
- wip_test = WIPTest.new test, start_time
54
+ wip_test = WIPTest.new(test, start_time)
55
55
  @wip << wip_test
56
56
  runtime = time_it(start_time) do
57
57
  instance = test.test_class.new
58
- instance.setup if instance.respond_to? :setup
59
- if instance.respond_to? :around
58
+ instance.setup if instance.respond_to?(:setup)
59
+ if instance.respond_to?(:around)
60
60
  did_run = false
61
61
  instance.around {
62
62
  did_run = true
@@ -66,15 +66,15 @@ class TLDR
66
66
  else
67
67
  instance.send(test.method_name)
68
68
  end
69
- instance.teardown if instance.respond_to? :teardown
69
+ instance.teardown if instance.respond_to?(:teardown)
70
70
  rescue Skip, Failure, StandardError => e
71
71
  end
72
72
  TestResult.new(test, e, runtime).tap do |result|
73
73
  next if @run_aborted.true?
74
74
  @results << result
75
- @wip.delete wip_test
76
- reporter.after_test result
77
- fail_fast reporter, plan, result if result.failing? && config.fail_fast
75
+ @wip.delete(wip_test)
76
+ reporter.after_test(result)
77
+ fail_fast(reporter, plan, result) if result.failing? && config.fail_fast
78
78
  end
79
79
  end
80
80
 
@@ -82,8 +82,8 @@ class TLDR
82
82
  unless @run_aborted.true?
83
83
  @run_aborted.make_true
84
84
  abort = proc do
85
- reporter.after_fail_fast plan.tests, @wip.dup, @results.dup, fast_failed_result
86
- exit! exit_code([fast_failed_result])
85
+ reporter.after_fail_fast(plan.tests, @wip.dup, @results.dup, fast_failed_result)
86
+ exit!(exit_code([fast_failed_result]))
87
87
  end
88
88
 
89
89
  if IRB.CurrentContext
@@ -94,7 +94,7 @@ class TLDR
94
94
  end
95
95
  end
96
96
 
97
- def time_it(start)
97
+ def time_it start
98
98
  yield
99
99
  ((Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - start) / 1000.0).round
100
100
  end
@@ -1,8 +1,8 @@
1
1
  class TLDR
2
2
  class Strategizer
3
- Strategy = Struct.new :parallel?, :prepend_sequential_tests,
3
+ Strategy = Struct.new(:parallel?, :prepend_sequential_tests,
4
4
  :parallel_tests_and_groups, :append_sequential_tests,
5
- keyword_init: true
5
+ keyword_init: true)
6
6
 
7
7
  # Combine all discovered test methods with any methods grouped by run_these_together!
8
8
  #
@@ -16,11 +16,11 @@ class TLDR
16
16
  thread_unsafe_tests, thread_safe_tests = partition_unsafe(all_tests, thread_unsafe_test_groups)
17
17
  prepend_sequential_tests, append_sequential_tests = partition_prepend(thread_unsafe_tests, config)
18
18
 
19
- grouped_tests = prepare_run_together_groups run_these_together_groups, thread_safe_tests, append_sequential_tests
19
+ grouped_tests = prepare_run_together_groups(run_these_together_groups, thread_safe_tests, append_sequential_tests)
20
20
  already_included_groups = []
21
21
  parallel_tests_and_groups = thread_safe_tests.map { |test|
22
- if (group = grouped_tests.find { |group| group.tests.include? test })
23
- if already_included_groups.include? group
22
+ if (group = grouped_tests.find { |group| group.tests.include?(test) })
23
+ if already_included_groups.include?(group)
24
24
  next
25
25
  elsif (other = already_included_groups.find { |other| (group.tests & other.tests).any? })
26
26
  other.tests |= group.tests
@@ -49,7 +49,7 @@ class TLDR
49
49
 
50
50
  def partition_unsafe tests, thread_unsafe_test_groups
51
51
  tests.partition { |test|
52
- thread_unsafe_test_groups.any? { |group| group.tests.include? test }
52
+ thread_unsafe_test_groups.any? { |group| group.tests.include?(test) }
53
53
  }
54
54
  end
55
55
 
@@ -57,10 +57,10 @@ class TLDR
57
57
  # Suboptimal, but we do indeed need to do this work in two places ¯\_(ツ)_/¯
58
58
  def partition_prepend thread_unsafe_tests, config
59
59
  prepend_paths = config.no_prepend ? [] : config.prepend_paths
60
- locations = PathUtil.expand_paths prepend_paths
60
+ locations = PathUtil.expand_paths(prepend_paths)
61
61
 
62
62
  thread_unsafe_tests.partition { |test|
63
- PathUtil.locations_include_test? locations, test
63
+ PathUtil.locations_include_test?(locations, test)
64
64
  }
65
65
  end
66
66
 
@@ -17,6 +17,9 @@ class TLDR
17
17
  base_path: "--base-path",
18
18
  no_dotfile: "--no-dotfile",
19
19
  warnings: "--[no-]warnings",
20
+ watch: "--watch",
21
+ yes_i_know: "--yes-i-know",
22
+ i_am_being_watched: "--i-am-being-watched",
20
23
  paths: nil
21
24
  }.freeze
22
25
 
@@ -26,7 +29,7 @@ class TLDR
26
29
  :paths, :seed, :no_helper, :verbose, :reporter,
27
30
  :helper_paths, :load_paths, :parallel, :names, :fail_fast, :no_emoji,
28
31
  :prepend_paths, :no_prepend, :exclude_paths, :exclude_names, :base_path,
29
- :no_dotfile, :warnings,
32
+ :no_dotfile, :warnings, :watch, :yes_i_know, :i_am_being_watched,
30
33
  # Internal properties
31
34
  :config_intended_for_merge_only, :seed_set_intentionally, :cli_defaults
32
35
  ].freeze
@@ -64,14 +67,17 @@ class TLDR
64
67
  exclude_paths: [],
65
68
  exclude_names: [],
66
69
  base_path: nil,
67
- warnings: true
70
+ warnings: true,
71
+ watch: false,
72
+ yes_i_know: false,
73
+ i_am_being_watched: false
68
74
  }
69
75
 
70
76
  if cli_defaults
71
77
  common.merge(
72
78
  paths: Dir["test/**/*_test.rb", "test/**/test_*.rb"],
73
79
  helper_paths: ["test/helper.rb"],
74
- load_paths: ["test"],
80
+ load_paths: ["lib", "test"],
75
81
  prepend_paths: [MOST_RECENTLY_MODIFIED_TAG]
76
82
  )
77
83
  else
@@ -101,7 +107,7 @@ class TLDR
101
107
  end
102
108
 
103
109
  # Booleans
104
- [:no_helper, :verbose, :fail_fast, :no_emoji, :no_prepend, :warnings].each do |key|
110
+ [:no_helper, :verbose, :fail_fast, :no_emoji, :no_prepend, :warnings, :yes_i_know, :i_am_being_watched].each do |key|
105
111
  merged_args[key] = defaults[key] if merged_args[key].nil?
106
112
  end
107
113
 
@@ -129,26 +135,34 @@ class TLDR
129
135
 
130
136
  self.prepend_paths = prepend_paths.map { |path|
131
137
  if path == MOST_RECENTLY_MODIFIED_TAG
132
- most_recently_modified_test_file tests
138
+ most_recently_modified_test_file(tests)
133
139
  else
134
140
  path
135
141
  end
136
142
  }.compact
137
143
  end
138
144
 
139
- def to_full_args(exclude: [])
140
- to_cli_argv(
145
+ def to_full_args exclude: [], ensure_args: []
146
+ argv = to_cli_argv(
141
147
  CONFLAGS.keys -
142
148
  exclude - [
143
- (:seed unless seed_set_intentionally)
149
+ (:seed unless seed_set_intentionally),
150
+ :watch,
151
+ :i_am_being_watched
144
152
  ]
145
- ).join(" ")
153
+ )
154
+
155
+ ensure_args.each do |arg|
156
+ argv << arg unless argv.include?(arg)
157
+ end
158
+
159
+ argv.join(" ")
146
160
  end
147
161
 
148
- def to_single_path_args(path)
162
+ def to_single_path_args path
149
163
  argv = to_cli_argv(CONFLAGS.keys - [
150
164
  :seed, :parallel, :names, :fail_fast, :paths, :prepend_paths,
151
- :no_prepend, :exclude_paths
165
+ :no_prepend, :exclude_paths, :watch, :i_am_being_watched
152
166
  ])
153
167
 
154
168
  (argv + [stringify(:paths, path)]).join(" ")
@@ -210,7 +224,7 @@ class TLDR
210
224
  end
211
225
  end
212
226
 
213
- def most_recently_modified_test_file(tests)
227
+ def most_recently_modified_test_file tests
214
228
  return if tests.empty?
215
229
 
216
230
  tests.max_by { |test| File.mtime(test.file) }.file
@@ -220,11 +234,11 @@ class TLDR
220
234
  # ASAP, even before globbing to find default paths of tests. If there is
221
235
  # a way to change all of our Dir.glob calls to be relative to base_path
222
236
  # without a loss in accuracy, would love to not have to use Dir.chdir!
223
- def change_working_directory_because_i_am_bad_and_i_should_feel_bad!(base_path)
237
+ def change_working_directory_because_i_am_bad_and_i_should_feel_bad! base_path
224
238
  Dir.chdir(base_path) unless base_path.nil?
225
239
  end
226
240
 
227
- def merge_dotfile_args(args)
241
+ def merge_dotfile_args args
228
242
  return args if args[:no_dotfile] || !File.exist?(".tldr.yml")
229
243
  require "yaml"
230
244
 
@@ -1,5 +1,5 @@
1
1
  class TLDR
2
- Location = Struct.new :file, :line do
2
+ Location = Struct.new(:file, :line) do
3
3
  def relative
4
4
  if file.start_with?(Dir.pwd)
5
5
  file[Dir.pwd.length + 1..]
@@ -1,3 +1,3 @@
1
1
  class TLDR
2
- Plan = Struct.new :tests, :strategy
2
+ Plan = Struct.new(:tests, :strategy)
3
3
  end
@@ -1,17 +1,17 @@
1
1
  class TLDR
2
- Test = Struct.new :test_class, :method_name do
2
+ Test = Struct.new(:test_class, :method_name) do
3
3
  attr_reader :file, :line, :location
4
4
 
5
5
  def initialize(*args)
6
6
  super
7
7
  @file, @line = SorbetCompatibility.unwrap_method(test_class.instance_method(method_name)).source_location
8
- @location = Location.new file, line
8
+ @location = Location.new(file, line)
9
9
  end
10
10
 
11
11
  # Memoizing at call time, because re-parsing isn't free and isn't usually necessary
12
12
  def end_line
13
13
  @end_line ||= begin
14
- test_method = SorbetCompatibility.unwrap_method test_class.instance_method(method_name)
14
+ test_method = SorbetCompatibility.unwrap_method(test_class.instance_method(method_name))
15
15
  RubyVM::AbstractSyntaxTree.of(test_method).last_lineno
16
16
  end
17
17
  end
@@ -1,16 +1,16 @@
1
1
  class TLDR
2
- TestGroup = Struct.new :configuration do
2
+ TestGroup = Struct.new(:configuration) do
3
3
  attr_writer :tests
4
4
 
5
5
  def tests
6
6
  @tests ||= configuration.flat_map { |(klass, method)|
7
- klass = Kernel.const_get(klass) if klass.is_a? String
7
+ klass = Kernel.const_get(klass) if klass.is_a?(String)
8
8
  if method.nil?
9
9
  ([klass] + ClassUtil.gather_descendants(klass)).flat_map { |klass|
10
10
  ClassUtil.gather_tests(klass)
11
11
  }
12
12
  else
13
- Test.new klass, method
13
+ Test.new(klass, method)
14
14
  end
15
15
  }
16
16
  end
@@ -1,5 +1,5 @@
1
1
  class TLDR
2
- TestResult = Struct.new :test, :error, :runtime do
2
+ TestResult = Struct.new(:test, :error, :runtime) do
3
3
  attr_reader :type, :error_location
4
4
 
5
5
  def initialize(*args)
@@ -1,3 +1,3 @@
1
1
  class TLDR
2
- WIPTest = Struct.new :test, :start_time
2
+ WIPTest = Struct.new(:test, :start_time)
3
3
  end
data/lib/tldr/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class TLDR
2
- VERSION = "0.7.0"
2
+ VERSION = "0.9.0"
3
3
  end
@@ -0,0 +1,32 @@
1
+ class TLDR
2
+ class Watcher
3
+ def watch config
4
+ require_fs_watch!
5
+ command = "fswatch -o #{config.load_paths.reverse.join(" ")} | xargs -n1 -I{} #{tldr_command} #{config.to_full_args}"
6
+
7
+ puts <<~MSG
8
+ Watching #{config.load_paths.map(&:inspect).join(", ")} for changes...
9
+ MSG
10
+
11
+ exec command
12
+ end
13
+
14
+ private
15
+
16
+ def require_fs_watch!
17
+ `which fswatch`
18
+ return if $?.success?
19
+
20
+ warn <<~MSG
21
+ Error: fswatch must be installed and on your PATH to run TLDR in --watch mode
22
+
23
+ See: https://github.com/emcrisostomo/fswatch
24
+ MSG
25
+ exit 1
26
+ end
27
+
28
+ def tldr_command
29
+ "#{"bundle exec " if defined?(Bundler)}tldr"
30
+ end
31
+ end
32
+ end
data/lib/tldr.rb CHANGED
@@ -16,6 +16,7 @@ require_relative "tldr/sorbet_compatibility"
16
16
  require_relative "tldr/strategizer"
17
17
  require_relative "tldr/value"
18
18
  require_relative "tldr/version"
19
+ require_relative "tldr/watcher"
19
20
 
20
21
  class TLDR
21
22
  include Assertions
@@ -29,25 +30,29 @@ class TLDR
29
30
 
30
31
  module Run
31
32
  def self.cli argv
32
- config = ArgvParser.new.parse argv
33
- tests config
33
+ config = ArgvParser.new.parse(argv)
34
+ tests(config)
34
35
  end
35
36
 
36
37
  def self.tests config = Config.new
37
- Runner.new.run config, Planner.new.plan(config)
38
+ if config.watch
39
+ Watcher.new.watch(config)
40
+ else
41
+ Runner.new.run(config, Planner.new.plan(config))
42
+ end
38
43
  end
39
44
 
40
45
  @@at_exit_registered = false
41
46
  def self.at_exit! config = Config.new
42
47
  # Ignore at_exit when running tldr CLI, since that will run any tests
43
- return if $PROGRAM_NAME.end_with? "tldr"
48
+ return if $PROGRAM_NAME.end_with?("tldr")
44
49
  # Also ignore if we're running from within our rake task
45
- return if caller.any? { |line| line.include? "lib/tldr/rake.rb" }
50
+ return if caller.any? { |line| line.include?("lib/tldr/rake.rb") }
46
51
  # Ignore at_exit when we've already registered an at_exit hook
47
52
  return if @@at_exit_registered
48
53
 
49
54
  Kernel.at_exit do
50
- Run.tests config
55
+ Run.tests(config)
51
56
  end
52
57
 
53
58
  @@at_exit_registered = true
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.7.0
4
+ version: 0.9.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-10-03 00:00:00.000000000 Z
12
+ date: 2023-10-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: super_diff
@@ -85,6 +85,7 @@ files:
85
85
  - lib/tldr/value/test_result.rb
86
86
  - lib/tldr/value/wip_test.rb
87
87
  - lib/tldr/version.rb
88
+ - lib/tldr/watcher.rb
88
89
  - script/setup
89
90
  - script/test
90
91
  homepage: https://github.com/tenderlove/tldr