tldr 0.10.1 → 1.1.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,90 @@ 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
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
71
87
  end
72
88
 
73
- opts.on CONFLAGS[:no_emoji], "Disable emoji in the output" do
74
- options[:no_emoji] = true
75
- end
76
-
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[:exit_0_on_timeout], "Exit with status code 0 when suite times out instead of 3" do
110
+ options[:exit_0_on_timeout] = true
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
+ opts.on CONFLAGS[:exit_2_on_failure], "Exit with status code 2 (normally for errors) for both failures and errors" do
114
+ options[:exit_2_on_failure] = true
115
+ end
116
+
117
+ opts.on CONFLAGS[:print_interrupted_test_backtraces], "Print stack traces of tests interrupted after a timeout" do |print_interrupted_test_backtraces|
118
+ options[:print_interrupted_test_backtraces] = print_interrupted_test_backtraces
119
+ end
120
+
121
+ unless ARGV.include?("--help") || ARGV.include?("--h")
122
+ opts.on CONFLAGS[:i_am_being_watched], "[INTERNAL] Signals to tldr it is being invoked under --watch mode" do
123
+ options[:i_am_being_watched] = true
124
+ end
125
+
126
+ opts.on "--comment COMMENT", String, "[INTERNAL] No-op; used for multi-line execution instructions" do
127
+ # See "--comment" in lib/tldr/reporters/default.rb for an example of how this is used internally
128
+ end
103
129
  end
104
130
  end.parse!(args)
105
131
 
106
132
  options[:paths] = args if args.any?
133
+ options[:config_path] = case options[:config_path]
134
+ when nil then Config::DEFAULT_YAML_PATH
135
+ when false then nil
136
+ else options[:config_path]
137
+ end
107
138
 
108
139
  Config.new(**options)
109
140
  end
141
+
142
+ private
143
+
144
+ def handle_unparsable_optional_value(og_args, option_name, value, &blk)
145
+ yield
146
+ rescue ArgumentError
147
+ args = og_args.dup
148
+ if (option_index = args.index("--#{option_name}"))
149
+ args.insert(option_index + 1, "--")
150
+ warn <<~MSG
151
+ TLDR exited in error!
152
+
153
+ We couldn't parse #{value.inspect} as a valid #{option_name} value
154
+
155
+ Did you mean to set --#{option_name} as the last option and without an explicit value?
156
+
157
+ If so, you need to append ' -- ' before any paths, like:
158
+
159
+ tldr #{args.join(" ")}
160
+ MSG
161
+ exit 1
162
+ else
163
+ raise
164
+ end
165
+ end
110
166
  end
111
167
  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,48 @@ 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)
25
- exit!(3)
42
+ reporter.after_tldr(plan.tests, @wip.dup, @results.dup) if reporter.respond_to?(:after_tldr)
43
+
44
+ # If there are failures/errors, use their exit code regardless of exit_0_on_timeout
45
+ if @results.any? { |result| result.error? || result.failure? }
46
+ exit!(exit_code(@results, config))
47
+ else
48
+ exit!(config.exit_0_on_timeout ? 0 : 3)
49
+ end
26
50
  end
27
51
 
28
- sleep(1.8)
52
+ sleep(config.timeout)
53
+
29
54
  # Don't hard-kill the runner if user is debugging, it'll
30
55
  # screw up their terminal slash be a bad time
31
56
  if IRB.CurrentContext
@@ -42,8 +67,8 @@ class TLDR
42
67
  end
43
68
 
44
69
  unless @run_aborted.true?
45
- reporter.after_suite(results)
46
- exit(exit_code(results))
70
+ reporter.after_suite(results) if reporter.respond_to?(:after_suite)
71
+ exit(exit_code(results, config))
47
72
  end
48
73
  end
49
74
 
@@ -74,7 +99,7 @@ class TLDR
74
99
  next if @run_aborted.true?
75
100
  @results << result
76
101
  @wip.delete(wip_test)
77
- reporter.after_test(result)
102
+ reporter.after_test(result) if reporter.respond_to?(:after_test)
78
103
  fail_fast(reporter, plan, result) if result.failing? && config.fail_fast
79
104
  end
80
105
  end
@@ -83,7 +108,7 @@ class TLDR
83
108
  unless @run_aborted.true?
84
109
  @run_aborted.make_true
85
110
  abort = proc do
86
- reporter.after_fail_fast(plan.tests, @wip.dup, @results.dup, fast_failed_result)
111
+ reporter.after_fail_fast(plan.tests, @wip.dup, @results.dup, fast_failed_result) if reporter.respond_to?(:after_fail_fast)
87
112
  exit!(exit_code([fast_failed_result]))
88
113
  end
89
114
 
@@ -100,11 +125,11 @@ class TLDR
100
125
  ((Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - start) / 1000.0).round
101
126
  end
102
127
 
103
- def exit_code results
128
+ def exit_code results, config
104
129
  if results.any? { |result| result.error? }
105
130
  2
106
131
  elsif results.any? { |result| result.failure? }
107
- 1
132
+ config.exit_2_on_failure ? 2 : 1
108
133
  else
109
134
  0
110
135
  end