tldr 0.10.1 → 1.0.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.
@@ -5,21 +5,41 @@ class TLDR
5
5
  PATTERN_FRIENDLY_SPLITTER = /,(?=(?:[^\/]*\/[^\/]*\/)*[^\/]*$)/
6
6
 
7
7
  def parse args, options = {cli_defaults: true}
8
+ og_args = args.dup
8
9
  OptionParser.new do |opts|
9
10
  opts.banner = "Usage: tldr [options] some_tests/**/*.rb some/path.rb:13 ..."
10
11
 
11
- opts.on CONFLAGS[:fail_fast], "Stop running tests as soon as one fails" do |fail_fast|
12
- options[:fail_fast] = fail_fast
12
+ opts.on "-t", "--[no-]timeout [TIMEOUT]", "Timeout (in seconds) before timer aborts the run (Default: #{Config::DEFAULT_TIMEOUT})" do |timeout|
13
+ options[:timeout] = if timeout == false
14
+ # --no-timeout
15
+ -1
16
+ elsif timeout.nil?
17
+ # --timeout
18
+ Config::DEFAULT_TIMEOUT
19
+ else
20
+ # --timeout 42.3
21
+ handle_unparsable_optional_value(og_args, "timeout", timeout) do
22
+ Float(timeout)
23
+ end
24
+ end
13
25
  end
14
26
 
15
- opts.on "-s", "#{CONFLAGS[:seed]} SEED", Integer, "Seed for randomization" do |seed|
16
- options[:seed] = seed
27
+ opts.on CONFLAGS[:watch], "Run your tests continuously on file save (requires 'fswatch' to be installed)" do
28
+ options[:watch] = true
29
+ end
30
+
31
+ opts.on CONFLAGS[:fail_fast], "Stop running tests as soon as one fails" do |fail_fast|
32
+ options[:fail_fast] = fail_fast
17
33
  end
18
34
 
19
35
  opts.on CONFLAGS[:parallel], "Parallelize tests (Default: true)" do |parallel|
20
36
  options[:parallel] = parallel
21
37
  end
22
38
 
39
+ opts.on "-s", "#{CONFLAGS[:seed]} SEED", Integer, "Random seed for test order (setting --seed disables parallelization by default)" do |seed|
40
+ options[:seed] = seed
41
+ end
42
+
23
43
  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
44
  options[:names] ||= []
25
45
  options[:names] += name.split(PATTERN_FRIENDLY_SPLITTER)
@@ -58,54 +78,82 @@ class TLDR
58
78
  options[:load_paths] += load_path
59
79
  end
60
80
 
61
- opts.on "-r", "#{CONFLAGS[:reporter]} REPORTER", String, "Set a custom reporter class (Default: \"TLDR::Reporters::Default\")" do |reporter|
62
- options[:reporter] = Kernel.const_get(reporter)
63
- end
64
-
65
81
  opts.on "#{CONFLAGS[:base_path]} PATH", String, "Change the working directory for all relative paths (Default: current working directory)" do |path|
66
82
  options[:base_path] = path
67
83
  end
68
84
 
69
- opts.on CONFLAGS[:no_dotfile], "Disable loading .tldr.yml dotfile" do
70
- options[:no_dotfile] = true
71
- end
72
-
73
- opts.on CONFLAGS[:no_emoji], "Disable emoji in the output" do
74
- options[:no_emoji] = true
85
+ opts.on "-c", "#{CONFLAGS[:config_path]} PATH", String, "The YAML configuration file to load (Default: '.tldr.yml')" do |config_path|
86
+ options[:config_path] = config_path
75
87
  end
76
88
 
77
- opts.on "-v", CONFLAGS[:verbose], "Print stack traces for errors" do |verbose|
78
- options[:verbose] = verbose
89
+ opts.on "-r", "#{CONFLAGS[:reporter]} REPORTER", String, "Set a custom reporter class (Default: \"TLDR::Reporters::Default\")" do |reporter|
90
+ options[:reporter] = reporter
79
91
  end
80
92
 
81
- opts.on CONFLAGS[:print_interrupted_test_backtraces], "Print stack traces for interrupted tests" do |print_interrupted_test_backtraces|
82
- options[:print_interrupted_test_backtraces] = print_interrupted_test_backtraces
93
+ opts.on CONFLAGS[:emoji], "Enable emoji output for the default reporter (Default: false)" do |emoji|
94
+ options[:emoji] = emoji
83
95
  end
84
96
 
85
97
  opts.on CONFLAGS[:warnings], "Print Ruby warnings (Default: true)" do |warnings|
86
98
  options[:warnings] = warnings
87
99
  end
88
100
 
89
- opts.on CONFLAGS[:watch], "Run your tests continuously on file save (requires 'fswatch' to be installed)" do
90
- options[:watch] = true
101
+ opts.on "-v", CONFLAGS[:verbose], "Print stack traces for errors" do |verbose|
102
+ options[:verbose] = verbose
91
103
  end
92
104
 
93
- opts.on CONFLAGS[:yes_i_know], "Suppress TLDR report when suite runs over 1.8s" do
105
+ opts.on CONFLAGS[:yes_i_know], "Suppress TLDR report when suite runs beyond any configured --timeout" do
94
106
  options[:yes_i_know] = true
95
107
  end
96
108
 
97
- opts.on CONFLAGS[:i_am_being_watched], "[INTERNAL] Signals to tldr it is being invoked under --watch mode" do
98
- options[:i_am_being_watched] = true
109
+ opts.on CONFLAGS[:print_interrupted_test_backtraces], "Print stack traces of tests interrupted after a timeout" do |print_interrupted_test_backtraces|
110
+ options[:print_interrupted_test_backtraces] = print_interrupted_test_backtraces
99
111
  end
100
112
 
101
- opts.on "--comment COMMENT", String, "[INTERNAL] No-op; used for multi-line execution instructions" do
102
- # See "--comment" in lib/tldr/reporters/default.rb for an example of how this is used internally
113
+ unless ARGV.include?("--help") || ARGV.include?("--h")
114
+ opts.on CONFLAGS[:i_am_being_watched], "[INTERNAL] Signals to tldr it is being invoked under --watch mode" do
115
+ options[:i_am_being_watched] = true
116
+ end
117
+
118
+ opts.on "--comment COMMENT", String, "[INTERNAL] No-op; used for multi-line execution instructions" do
119
+ # See "--comment" in lib/tldr/reporters/default.rb for an example of how this is used internally
120
+ end
103
121
  end
104
122
  end.parse!(args)
105
123
 
106
124
  options[:paths] = args if args.any?
125
+ options[:config_path] = case options[:config_path]
126
+ when nil then Config::DEFAULT_YAML_PATH
127
+ when false then nil
128
+ else options[:config_path]
129
+ end
107
130
 
108
131
  Config.new(**options)
109
132
  end
133
+
134
+ private
135
+
136
+ def handle_unparsable_optional_value(og_args, option_name, value, &blk)
137
+ yield
138
+ rescue ArgumentError
139
+ args = og_args.dup
140
+ if (option_index = args.index("--#{option_name}"))
141
+ args.insert(option_index + 1, "--")
142
+ warn <<~MSG
143
+ TLDR exited in error!
144
+
145
+ We couldn't parse #{value.inspect} as a valid #{option_name} value
146
+
147
+ Did you mean to set --#{option_name} as the last option and without an explicit value?
148
+
149
+ If so, you need to append ' -- ' before any paths, like:
150
+
151
+ tldr #{args.join(" ")}
152
+ MSG
153
+ exit 1
154
+ else
155
+ raise
156
+ end
157
+ end
110
158
  end
111
159
  end
@@ -0,0 +1,107 @@
1
+ class TLDR
2
+ class ArgvReconstructor
3
+ def reconstruct config, exclude:, ensure_args:, exclude_dotfile_matches:
4
+ argv = to_cli_argv(
5
+ config,
6
+ CONFLAGS.keys - exclude - [
7
+ (:seed unless config.seed_set_intentionally),
8
+ :watch,
9
+ :i_am_being_watched
10
+ ],
11
+ exclude_dotfile_matches:
12
+ )
13
+
14
+ ensure_args.each do |arg|
15
+ argv << arg unless argv.include?(arg)
16
+ end
17
+
18
+ argv.join(" ")
19
+ end
20
+
21
+ def reconstruct_single_path_args config, path, exclude_dotfile_matches:
22
+ argv = to_cli_argv(config, CONFLAGS.keys - [
23
+ :seed, :parallel, :names, :fail_fast, :paths, :prepend_paths,
24
+ :no_prepend, :exclude_paths, :watch, :i_am_being_watched
25
+ ], exclude_dotfile_matches:)
26
+
27
+ (argv + [stringify(:paths, path)]).join(" ")
28
+ end
29
+
30
+ private
31
+
32
+ def to_cli_argv config, options = CONFLAGS.keys, exclude_dotfile_matches:
33
+ defaults = Config.build_defaults(cli_defaults: true)
34
+ defaults = defaults.merge(config.dotfile_args(config.config_path)) if exclude_dotfile_matches
35
+ options.map { |key|
36
+ flag = CONFLAGS[key]
37
+
38
+ # Special cases
39
+ if key == :prepend_paths
40
+ if config.prepend_paths.map { |s| stringify(key, s) }.sort == config.paths.map { |s| stringify(:paths, s) }.sort
41
+ # Don't print prepended tests if they're the same as the test paths
42
+ next
43
+ elsif config.no_prepend
44
+ # Don't print prepended tests if they're disabled
45
+ next
46
+ end
47
+ elsif key == :helper_paths && config.no_helper
48
+ # Don't print the helper if it's disabled
49
+ next
50
+ elsif key == :parallel
51
+ val = if !config.seed_set_intentionally && !config.parallel
52
+ "--no-parallel"
53
+ elsif !config.seed.nil? && config.seed_set_intentionally && config.parallel
54
+ "--parallel"
55
+ end
56
+ next val
57
+ elsif key == :timeout
58
+ if config[:timeout] < 0
59
+ next
60
+ elsif config[:timeout] == Config::DEFAULT_TIMEOUT
61
+ next "--timeout"
62
+ elsif config[:timeout] != Config::DEFAULT_TIMEOUT
63
+ next "--timeout #{config[:timeout]}"
64
+ else
65
+ next
66
+ end
67
+ elsif key == :config_path
68
+ case config[:config_path]
69
+ when nil then next "--no-config"
70
+ when Config::DEFAULT_YAML_PATH then next
71
+ else next "--config #{config[:config_path]}"
72
+ end
73
+ end
74
+
75
+ if defaults[key] == config[key] && (key != :seed || !config.seed_set_intentionally)
76
+ next
77
+ elsif CONFLAGS[key]&.start_with?("--[no-]")
78
+ case config[key]
79
+ when false then CONFLAGS[key].gsub(/[\[\]]/, "")
80
+ when nil || true then CONFLAGS[key].gsub("[no-]", "")
81
+ else "#{CONFLAGS[key].gsub("[no-]", "")} #{stringify(key, config[key])}"
82
+ end
83
+ elsif config[key].is_a?(Array)
84
+ config[key].map { |value| [flag, stringify(key, value)] }
85
+ elsif config[key].is_a?(TrueClass) || config[key].is_a?(FalseClass)
86
+ flag if config[key]
87
+ elsif config[key].is_a?(Class)
88
+ [flag, config[key].name]
89
+ elsif !config[key].nil?
90
+ [flag, stringify(key, config[key])]
91
+ end
92
+ }.flatten.compact
93
+ end
94
+
95
+ def stringify key, val
96
+ if PATH_FLAGS.include?(key) && val.start_with?(Dir.pwd)
97
+ val = val[Dir.pwd.length + 1..]
98
+ end
99
+
100
+ if val.nil? || val.is_a?(Integer)
101
+ val
102
+ else
103
+ "\"#{val}\""
104
+ end
105
+ end
106
+ end
107
+ end
@@ -9,7 +9,6 @@
9
9
 
10
10
  require "pp"
11
11
  require "super_diff"
12
- require_relative "assertions/minitest_compatibility"
13
12
 
14
13
  class TLDR
15
14
  module Assertions
@@ -109,7 +108,7 @@ class TLDR
109
108
  refute_in_delta expected, actual, expected * epsilon, msg
110
109
  end
111
110
 
112
- def assert_include? expected, actual, message = nil
111
+ def assert_includes actual, expected, message = nil
113
112
  message = Assertions.msg(message) {
114
113
  "Expected #{Assertions.h(actual)} to include #{Assertions.h(expected)}"
115
114
  }
@@ -117,7 +116,7 @@ class TLDR
117
116
  assert actual.include?(expected), message
118
117
  end
119
118
 
120
- def refute_include? expected, actual, message = nil
119
+ def refute_includes actual, expected, message = nil
121
120
  message = Assertions.msg(message) {
122
121
  "Expected #{Assertions.h(actual)} to not include #{Assertions.h(expected)}"
123
122
  }
@@ -0,0 +1,2 @@
1
+ require "tldr"
2
+ TLDR::Run.at_exit!(TLDR::ArgvParser.new.parse(ARGV))
@@ -0,0 +1,35 @@
1
+ # These methods are provided to support drop-in compatibility with Minitest. You
2
+ # can use them by mixing them into your test or into the `TLDR` base class
3
+ # itself:
4
+ #
5
+ # class YourTest < TLDR
6
+ # include TLDR::MinitestCompatibility
7
+ #
8
+ # def test_something
9
+ # # …
10
+ # end
11
+ # end
12
+ #
13
+ # The implementation of these methods is extremely similar or identical to those
14
+ # found in minitest itself, which is Copyright © Ryan Davis, seattle.rb and
15
+ # distributed under the MIT License
16
+ class TLDR
17
+ module MinitestCompatibility
18
+ def capture_io &blk
19
+ Assertions.capture_io(&blk)
20
+ end
21
+
22
+ def mu_pp obj
23
+ s = obj.inspect.encode(Encoding.default_external)
24
+
25
+ if String === obj && (obj.encoding != Encoding.default_external ||
26
+ !obj.valid_encoding?)
27
+ enc = "# encoding: #{obj.encoding}"
28
+ val = "# valid: #{obj.valid_encoding?}"
29
+ "#{enc}\n#{val}\n#{s}"
30
+ else
31
+ s
32
+ end
33
+ end
34
+ end
35
+ end
@@ -3,7 +3,7 @@ class TLDR
3
3
  class Default < Base
4
4
  def initialize config, out = $stdout, err = $stderr
5
5
  super
6
- @icons = @config.no_emoji ? IconProvider::Base.new : IconProvider::Emoji.new
6
+ @icons = @config.emoji ? IconProvider::Emoji.new : IconProvider::Base.new
7
7
  end
8
8
 
9
9
  def before_suite tests
@@ -11,9 +11,9 @@ class TLDR
11
11
  @suite_start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
12
12
  @out.print <<~MSG
13
13
  Command: #{tldr_command} #{@config.to_full_args}
14
- #{@icons.seed} #{CONFLAGS[:seed]} #{@config.seed}
14
+ #{@icons.rpad(:seed)}#{CONFLAGS[:seed]} #{@config.seed}
15
15
 
16
- #{@icons.run} Running:
16
+ #{@icons.rpad(:run)}Running:
17
17
 
18
18
  MSG
19
19
  end
@@ -39,22 +39,22 @@ class TLDR
39
39
  @err.print "\n\n"
40
40
 
41
41
  if @config.yes_i_know
42
- @err.print "🚨 TLDR after completing #{test_results.size} of #{planned_tests.size} tests! Print full summary by omitting --yes-i-know"
42
+ @err.print "#{@icons.rpad(:alarm)}TLDR after completing #{test_results.size} of #{planned_tests.size} tests! Print full summary by omitting --yes-i-know"
43
43
  else
44
44
  wrap_in_horizontal_rule do
45
45
  @err.print [
46
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.",
47
+ "#{@icons.rpad(: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
48
  (<<~WIP.chomp if wip_tests.any?),
49
- #{@icons.wip} #{plural(wip_tests.size, "test was", "tests were")} cancelled in progress:
49
+ #{@icons.rpad(:wip)}#{plural(wip_tests.size, "test was", "tests were")} cancelled in progress:
50
50
  #{wip_tests.map { |wip_test| " #{time_diff(wip_test.start_time, stop_time)}ms - #{describe(wip_test.test)}#{print_wip_backtrace(wip_test, indent: " ") if @config.print_interrupted_test_backtraces}" }.join("\n")}
51
51
  WIP
52
52
  (<<~SLOW.chomp if test_results.any?),
53
- #{@icons.slow} Your #{[10, test_results.size].min} slowest completed tests:
53
+ #{@icons.rpad(:slow)}Your #{[10, test_results.size].min} slowest completed tests:
54
54
  #{test_results.sort_by(&:runtime).last(10).reverse.map { |result| " #{result.runtime}ms - #{describe(result.test)}" }.join("\n")}
55
55
  SLOW
56
56
  describe_tests_that_didnt_finish(planned_tests, test_results),
57
- "🙈 Suppress this summary with --yes-i-know"
57
+ "#{@icons.rpad(:not_run)}Suppress this summary with --yes-i-know"
58
58
  ].compact.join("\n\n")
59
59
  end
60
60
  end
@@ -69,8 +69,8 @@ class TLDR
69
69
  wrap_in_horizontal_rule do
70
70
  @err.print [
71
71
  "Failing fast after #{describe(last_result.test, last_result.relevant_location)} #{last_result.error? ? "errored" : "failed"}.",
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?),
72
+ ("#{@icons.rpad(:wip)}#{plural(wip_tests.size, "test was", "tests were")} cancelled in progress." if wip_tests.any?),
73
+ ("#{@icons.rpad(:not_run)} #{plural(unrun_tests.size, "test was", "tests were")} not run at all." if unrun_tests.any?),
74
74
  describe_tests_that_didnt_finish(planned_tests, test_results)
75
75
  ].compact.join("\n\n")
76
76
  end
@@ -165,7 +165,7 @@ class TLDR
165
165
  ("--comment \"Also include #{plural(failed.size, "test")} that failed:\"" if failed_locators.any?)
166
166
  ].compact + failed_locators
167
167
  <<~MSG
168
- #{@icons.rock_on} Run the #{plural(unrun.size, "test")} that didn't finish:
168
+ #{@icons.rpad(:rock_on)}Run the #{plural(unrun.size, "test")} that didn't finish:
169
169
  #{tldr_command} #{@config.to_full_args(exclude: [:paths], exclude_dotfile_matches: true)} #{suggested_locators.join(" \\\n ")}
170
170
  MSG
171
171
  end
@@ -47,6 +47,15 @@ module IconProvider
47
47
  def seed
48
48
  ""
49
49
  end
50
+
51
+ def rpad(icon_name)
52
+ icon = send(icon_name)
53
+ if icon.nil? || icon.size == 0
54
+ icon
55
+ else
56
+ "#{icon} "
57
+ end
58
+ end
50
59
  end
51
60
 
52
61
  class Emoji < Base
data/lib/tldr/runner.rb CHANGED
@@ -9,23 +9,42 @@ class TLDR
9
9
  @run_aborted = Concurrent::AtomicBoolean.new(false)
10
10
  end
11
11
 
12
+ def instantiate_reporter config
13
+ begin
14
+ reporter_class = Kernel.const_get(config.reporter)
15
+ rescue NameError
16
+ raise Error, "Unknown reporter '#{config.reporter}' (are you sure it was loaded by your test or helper?)"
17
+ end
18
+ if reporter_class.is_a?(Class)
19
+ if reporter_class.instance_method(:initialize).parameters.any? { |type, _| [:req, :opt, :rest].include?(type) }
20
+ reporter_class.new(config)
21
+ else
22
+ reporter_class.new
23
+ end
24
+ else
25
+ raise Error, "Reporter '#{config.reporter}' expected to be a class, but was a #{reporter_class.class}"
26
+ end
27
+ end
28
+
12
29
  def run config, plan
13
30
  @wip.clear
14
31
  @results.clear
15
- reporter = config.reporter.new(config)
16
- reporter.before_suite(plan.tests)
32
+ reporter = instantiate_reporter(config)
33
+ reporter.before_suite(plan.tests) if reporter.respond_to?(:before_suite)
17
34
 
18
35
  time_bomb = Thread.new {
36
+ next if config.timeout < 0
37
+
19
38
  explode = proc do
20
- next if ENV["CI"] && !$stderr.tty?
21
39
  next if @run_aborted.true?
22
40
  @run_aborted.make_true
23
41
  @wip.each(&:capture_backtrace_at_exit)
24
- reporter.after_tldr(plan.tests, @wip.dup, @results.dup)
42
+ reporter.after_tldr(plan.tests, @wip.dup, @results.dup) if reporter.respond_to?(:after_tldr)
25
43
  exit!(3)
26
44
  end
27
45
 
28
- sleep(1.8)
46
+ sleep(config.timeout)
47
+
29
48
  # Don't hard-kill the runner if user is debugging, it'll
30
49
  # screw up their terminal slash be a bad time
31
50
  if IRB.CurrentContext
@@ -42,7 +61,7 @@ class TLDR
42
61
  end
43
62
 
44
63
  unless @run_aborted.true?
45
- reporter.after_suite(results)
64
+ reporter.after_suite(results) if reporter.respond_to?(:after_suite)
46
65
  exit(exit_code(results))
47
66
  end
48
67
  end
@@ -74,7 +93,7 @@ class TLDR
74
93
  next if @run_aborted.true?
75
94
  @results << result
76
95
  @wip.delete(wip_test)
77
- reporter.after_test(result)
96
+ reporter.after_test(result) if reporter.respond_to?(:after_test)
78
97
  fail_fast(reporter, plan, result) if result.failing? && config.fail_fast
79
98
  end
80
99
  end
@@ -83,7 +102,7 @@ class TLDR
83
102
  unless @run_aborted.true?
84
103
  @run_aborted.make_true
85
104
  abort = proc do
86
- reporter.after_fail_fast(plan.tests, @wip.dup, @results.dup, fast_failed_result)
105
+ reporter.after_fail_fast(plan.tests, @wip.dup, @results.dup, fast_failed_result) if reporter.respond_to?(:after_fail_fast)
87
106
  exit!(exit_code([fast_failed_result]))
88
107
  end
89
108