tldr 0.7.0 → 0.9.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 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