tldr 0.8.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: 81e3d6734a718c5abbf80cfb088369d5a6bb89b81fc261020d97e34cbd6903bb
4
- data.tar.gz: 1c4d0b5a53a948d868556b027f6fd6cdb4b9a96e67b459f0b2bf73e05a251d2f
3
+ metadata.gz: c9ae325a1d7b5c16b255bce28975b4e9c08bc23291932a02d91d02d46c247655
4
+ data.tar.gz: 0e92807c78187999ea26dba8f3ff48aa8ec6442b8884def3b7fc6b58db4127f0
5
5
  SHA512:
6
- metadata.gz: df9de67f0609adc2c5e0d2fc44a3bec44dd8a25a4ed0d128b8f1beaad6f45343dc9e5644e8d3b94b5bbe4db919f3becdf2e9a468a6eda0978669018135993d1f
7
- data.tar.gz: 00e813b4947ef3d9e267e3ea4248def3254829b4b910719ad4cdd258cf809d751d7c5a539d527a708f9ff3471fe45fe24fc342d132f15948a56d7d5a56b38c7e
6
+ metadata.gz: 79a8a3c45bc03731c80e5d7b00103d69efe4a1185c758fdecde1451864677af433126634a717dd912d7032556562cbe3894b1e7db75ab811412c1cc9097a457e
7
+ data.tar.gz: f27b99c4502b3a803d9c0507a1a9888715e8d203e1fc97bcd3284baa34afed4d9f55f2c5c06424c49645526c92534a54b52946867606a89b738dcb9c481a4c5d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
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
+
1
7
  ## [0.8.0]
2
8
 
3
9
  * Add a `--yes-i-know` flag that will suppress the large warning when your test
data/README.md CHANGED
@@ -116,15 +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
+ --watch Run your tests continuously on file save (requires 'fswatch' to be installed)
126
127
  --yes-i-know Suppress TLDR report when suite runs over 1.8s
127
- --comment COMMENT No-op; used for multi-line execution instructions
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
128
130
  ```
129
131
 
130
132
  After being parsed, all the CLI options are converted into a
@@ -161,6 +163,17 @@ with these caveats:
161
163
  TLDR::Assertions::MinitestCompatibility` into the `TLDR` base class or
162
164
  individual test classesJust set it
163
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
+
164
177
  ### Running TLDR with Rake
165
178
 
166
179
  TLDR ships with a [very](lib/tldr/rake.rb) minimal rake task that simply shells
@@ -240,8 +253,9 @@ encountered multiple times, only the first hook will be registered. If the
240
253
 
241
254
  #### Setting up the load path
242
255
 
243
- When running TLDR from a Ruby script, one thing the framework can't help you with
244
- 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.
245
259
 
246
260
  If you want to require code in `test/` or `lib/` without using
247
261
  `require_relative`, you'll need to add those directories to the load path. You
@@ -350,7 +364,7 @@ TLDR is laser-focused on running tests, so it doesn't provide a built-in mocking
350
364
  facility. Might we interest you in a refreshing
351
365
  [mocktail](https://github.com/testdouble/mocktail), instead?
352
366
 
353
- ### Contributing to TLDR
367
+ ## Contributing to TLDR
354
368
 
355
369
  If you want to submit PRs on this repo, please know that the code style is
356
370
  [Kirkland-style Ruby](https://mastodon.social/@searls/111137666157318482), where
@@ -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,11 +82,19 @@ class TLDR
82
82
  options[:warnings] = warnings
83
83
  end
84
84
 
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
+
85
89
  opts.on CONFLAGS[:yes_i_know], "Suppress TLDR report when suite runs over 1.8s" do
86
90
  options[:yes_i_know] = true
87
91
  end
88
92
 
89
- opts.on "--comment COMMENT", String, "No-op; used for multi-line execution instructions" do
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
90
98
  # See "--comment" in lib/tldr/reporters/default.rb for an example of how this is used internally
91
99
  end
92
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}
@@ -32,7 +33,7 @@ class TLDR
32
33
  end
33
34
 
34
35
  def after_tldr planned_tests, wip_tests, test_results
35
- stop_time = Process.clock_gettime Process::CLOCK_MONOTONIC, :microsecond
36
+ stop_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
36
37
 
37
38
  @out.print @icons.tldr
38
39
  @err.print "\n\n"
@@ -45,7 +46,7 @@ class TLDR
45
46
  "too long; didn't run!",
46
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.",
47
48
  (<<~WIP.chomp if wip_tests.any?),
48
- #{@icons.wip} #{plural wip_tests.size, "test was", "tests were"} cancelled in progress:
49
+ #{@icons.wip} #{plural(wip_tests.size, "test was", "tests were")} cancelled in progress:
49
50
  #{wip_tests.map { |wip_test| " #{time_diff(wip_test.start_time, stop_time)}ms - #{describe(wip_test.test)}" }.join("\n")}
50
51
  WIP
51
52
  (<<~SLOW.chomp if test_results.any?),
@@ -58,7 +59,7 @@ class TLDR
58
59
  end
59
60
  end
60
61
 
61
- after_suite test_results
62
+ after_suite(test_results)
62
63
  end
63
64
 
64
65
  def after_fail_fast planned_tests, wip_tests, test_results, last_result
@@ -68,17 +69,17 @@ class TLDR
68
69
  wrap_in_horizontal_rule do
69
70
  @err.print [
70
71
  "Failing fast after #{describe(last_result.test, last_result.relevant_location)} #{last_result.error? ? "errored" : "failed"}.",
71
- ("#{@icons.wip} #{plural wip_tests.size, "test was", "tests were"} cancelled in progress." if wip_tests.any?),
72
- ("#{@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?),
73
74
  describe_tests_that_didnt_finish(planned_tests, test_results)
74
75
  ].compact.join("\n\n")
75
76
  end
76
77
 
77
- after_suite test_results
78
+ after_suite(test_results)
78
79
  end
79
80
 
80
81
  def after_suite test_results
81
- duration = time_diff @suite_start_time
82
+ duration = time_diff(@suite_start_time)
82
83
  test_results = test_results.sort_by { |result| [result.test.location.file, result.test.location.line] }
83
84
 
84
85
  @err.print summarize_failures(test_results).join("\n\n")
@@ -112,7 +113,7 @@ class TLDR
112
113
  failures = results.select { |result| result.failing? }
113
114
  return failures if failures.empty?
114
115
 
115
- ["\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) }
116
117
  end
117
118
 
118
119
  def summarize_result result, index
@@ -151,15 +152,15 @@ class TLDR
151
152
  unrun = planned_tests - test_results.map(&:test)
152
153
  return if unrun.empty?
153
154
 
154
- unrun_locators = consolidate unrun
155
+ unrun_locators = consolidate(unrun)
155
156
  failed = test_results.select(&:failing?).map(&:test)
156
- failed_locators = consolidate failed, exclude: unrun_locators
157
+ failed_locators = consolidate(failed, exclude: unrun_locators)
157
158
  suggested_locators = unrun_locators + [
158
- ("--comment \"Also include #{plural failed.size, "test"} that failed:\"" if failed_locators.any?)
159
+ ("--comment \"Also include #{plural(failed.size, "test")} that failed:\"" if failed_locators.any?)
159
160
  ].compact + failed_locators
160
161
  <<~MSG
161
- #{@icons.rock_on} Run the #{plural unrun.size, "test"} that didn't finish:
162
- #{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 ")}
163
164
  MSG
164
165
  end
165
166
 
@@ -172,6 +173,12 @@ class TLDR
172
173
  def tldr_command
173
174
  "#{"bundle exec " if defined?(Bundler)}tldr"
174
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
175
182
  end
176
183
  end
177
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,7 +17,9 @@ class TLDR
17
17
  base_path: "--base-path",
18
18
  no_dotfile: "--no-dotfile",
19
19
  warnings: "--[no-]warnings",
20
+ watch: "--watch",
20
21
  yes_i_know: "--yes-i-know",
22
+ i_am_being_watched: "--i-am-being-watched",
21
23
  paths: nil
22
24
  }.freeze
23
25
 
@@ -27,7 +29,7 @@ class TLDR
27
29
  :paths, :seed, :no_helper, :verbose, :reporter,
28
30
  :helper_paths, :load_paths, :parallel, :names, :fail_fast, :no_emoji,
29
31
  :prepend_paths, :no_prepend, :exclude_paths, :exclude_names, :base_path,
30
- :no_dotfile, :warnings, :yes_i_know,
32
+ :no_dotfile, :warnings, :watch, :yes_i_know, :i_am_being_watched,
31
33
  # Internal properties
32
34
  :config_intended_for_merge_only, :seed_set_intentionally, :cli_defaults
33
35
  ].freeze
@@ -66,14 +68,16 @@ class TLDR
66
68
  exclude_names: [],
67
69
  base_path: nil,
68
70
  warnings: true,
69
- yes_i_know: false
71
+ watch: false,
72
+ yes_i_know: false,
73
+ i_am_being_watched: false
70
74
  }
71
75
 
72
76
  if cli_defaults
73
77
  common.merge(
74
78
  paths: Dir["test/**/*_test.rb", "test/**/test_*.rb"],
75
79
  helper_paths: ["test/helper.rb"],
76
- load_paths: ["test"],
80
+ load_paths: ["lib", "test"],
77
81
  prepend_paths: [MOST_RECENTLY_MODIFIED_TAG]
78
82
  )
79
83
  else
@@ -103,7 +107,7 @@ class TLDR
103
107
  end
104
108
 
105
109
  # Booleans
106
- [:no_helper, :verbose, :fail_fast, :no_emoji, :no_prepend, :warnings, :yes_i_know].each do |key|
110
+ [:no_helper, :verbose, :fail_fast, :no_emoji, :no_prepend, :warnings, :yes_i_know, :i_am_being_watched].each do |key|
107
111
  merged_args[key] = defaults[key] if merged_args[key].nil?
108
112
  end
109
113
 
@@ -131,26 +135,34 @@ class TLDR
131
135
 
132
136
  self.prepend_paths = prepend_paths.map { |path|
133
137
  if path == MOST_RECENTLY_MODIFIED_TAG
134
- most_recently_modified_test_file tests
138
+ most_recently_modified_test_file(tests)
135
139
  else
136
140
  path
137
141
  end
138
142
  }.compact
139
143
  end
140
144
 
141
- def to_full_args(exclude: [])
142
- to_cli_argv(
145
+ def to_full_args exclude: [], ensure_args: []
146
+ argv = to_cli_argv(
143
147
  CONFLAGS.keys -
144
148
  exclude - [
145
- (:seed unless seed_set_intentionally)
149
+ (:seed unless seed_set_intentionally),
150
+ :watch,
151
+ :i_am_being_watched
146
152
  ]
147
- ).join(" ")
153
+ )
154
+
155
+ ensure_args.each do |arg|
156
+ argv << arg unless argv.include?(arg)
157
+ end
158
+
159
+ argv.join(" ")
148
160
  end
149
161
 
150
- def to_single_path_args(path)
162
+ def to_single_path_args path
151
163
  argv = to_cli_argv(CONFLAGS.keys - [
152
164
  :seed, :parallel, :names, :fail_fast, :paths, :prepend_paths,
153
- :no_prepend, :exclude_paths
165
+ :no_prepend, :exclude_paths, :watch, :i_am_being_watched
154
166
  ])
155
167
 
156
168
  (argv + [stringify(:paths, path)]).join(" ")
@@ -212,7 +224,7 @@ class TLDR
212
224
  end
213
225
  end
214
226
 
215
- def most_recently_modified_test_file(tests)
227
+ def most_recently_modified_test_file tests
216
228
  return if tests.empty?
217
229
 
218
230
  tests.max_by { |test| File.mtime(test.file) }.file
@@ -222,11 +234,11 @@ class TLDR
222
234
  # ASAP, even before globbing to find default paths of tests. If there is
223
235
  # a way to change all of our Dir.glob calls to be relative to base_path
224
236
  # without a loss in accuracy, would love to not have to use Dir.chdir!
225
- 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
226
238
  Dir.chdir(base_path) unless base_path.nil?
227
239
  end
228
240
 
229
- def merge_dotfile_args(args)
241
+ def merge_dotfile_args args
230
242
  return args if args[:no_dotfile] || !File.exist?(".tldr.yml")
231
243
  require "yaml"
232
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.8.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.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Searls
@@ -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