tldr 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +213 -0
- data/Rakefile +14 -0
- data/exe/tldr +5 -0
- data/lib/tldr/argv_parser.rb +94 -0
- data/lib/tldr/assertions/minitest_compatibility.rb +38 -0
- data/lib/tldr/assertions.rb +365 -0
- data/lib/tldr/backtrace_filter.rb +44 -0
- data/lib/tldr/error.rb +7 -0
- data/lib/tldr/planner.rb +170 -0
- data/lib/tldr/reporters/base.rb +36 -0
- data/lib/tldr/reporters/default.rb +167 -0
- data/lib/tldr/reporters/icon_provider.rb +93 -0
- data/lib/tldr/reporters.rb +4 -0
- data/lib/tldr/runner.rb +113 -0
- data/lib/tldr/skippable.rb +7 -0
- data/lib/tldr/sorbet_compatibility.rb +9 -0
- data/lib/tldr/value/config.rb +217 -0
- data/lib/tldr/value/location.rb +15 -0
- data/lib/tldr/value/plan.rb +3 -0
- data/lib/tldr/value/test.rb +23 -0
- data/lib/tldr/value/test_result.rb +62 -0
- data/lib/tldr/value/wip_test.rb +3 -0
- data/lib/tldr/value.rb +6 -0
- data/lib/tldr/version.rb +3 -0
- data/lib/tldr.rb +41 -0
- data/script/parse +6 -0
- data/script/run +6 -0
- data/script/test +25 -0
- metadata +108 -0
@@ -0,0 +1,167 @@
|
|
1
|
+
class TLDR
|
2
|
+
module Reporters
|
3
|
+
class Default < Base
|
4
|
+
def initialize(config, out = $stdout, err = $stderr)
|
5
|
+
super
|
6
|
+
@icons = @config.no_emoji ? IconProvider::Base.new : IconProvider::Emoji.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def before_suite tests
|
10
|
+
@suite_start_time = Process.clock_gettime Process::CLOCK_MONOTONIC, :microsecond
|
11
|
+
@out.print <<~MSG
|
12
|
+
Command: #{tldr_command} #{@config.to_full_args}
|
13
|
+
|
14
|
+
#{@icons.run} Running:
|
15
|
+
|
16
|
+
MSG
|
17
|
+
end
|
18
|
+
|
19
|
+
def after_test result
|
20
|
+
@out.print case result.type
|
21
|
+
when :success then @icons.success
|
22
|
+
when :skip then @icons.skip
|
23
|
+
when :failure then @icons.failure
|
24
|
+
when :error then @icons.error
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def time_diff start, stop = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
29
|
+
((stop - start) / 1000.0).round
|
30
|
+
end
|
31
|
+
|
32
|
+
def after_tldr planned_tests, wip_tests, test_results
|
33
|
+
stop_time = Process.clock_gettime Process::CLOCK_MONOTONIC, :microsecond
|
34
|
+
|
35
|
+
@out.print @icons.tldr
|
36
|
+
@err.print "\n\n"
|
37
|
+
wrap_in_horizontal_rule do
|
38
|
+
@err.print [
|
39
|
+
"too long; didn't run!",
|
40
|
+
"#{@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.",
|
41
|
+
(<<~WIP.chomp if wip_tests.any?),
|
42
|
+
#{@icons.wip} #{plural wip_tests.size, "test was", "tests were"} cancelled in progress:
|
43
|
+
#{wip_tests.map { |wip_test| " #{time_diff(wip_test.start_time, stop_time)}ms - #{describe(wip_test.test)}" }.join("\n")}
|
44
|
+
WIP
|
45
|
+
(<<~SLOW.chomp if test_results.any?),
|
46
|
+
#{@icons.slow} Your #{[10, test_results.size].min} slowest completed tests:
|
47
|
+
#{test_results.sort_by(&:runtime).last(10).reverse.map { |result| " #{result.runtime}ms - #{describe(result.test)}" }.join("\n")}
|
48
|
+
SLOW
|
49
|
+
describe_tests_that_didnt_finish(planned_tests, test_results)
|
50
|
+
].compact.join("\n\n")
|
51
|
+
end
|
52
|
+
|
53
|
+
after_suite test_results
|
54
|
+
end
|
55
|
+
|
56
|
+
def after_fail_fast planned_tests, wip_tests, test_results, last_result
|
57
|
+
unrun_tests = planned_tests - test_results.map(&:test) - wip_tests.map(&:test)
|
58
|
+
|
59
|
+
@err.print "\n\n"
|
60
|
+
wrap_in_horizontal_rule do
|
61
|
+
@err.print [
|
62
|
+
"Failing fast after #{describe(last_result.test, last_result.relevant_location)} #{last_result.error? ? "errored" : "failed"}.",
|
63
|
+
("#{@icons.wip} #{plural wip_tests.size, "test was", "tests were"} cancelled in progress." if wip_tests.any?),
|
64
|
+
("#{@icons.not_run} #{plural unrun_tests.size, "test was", "tests were"} not run at all." if unrun_tests.any?),
|
65
|
+
describe_tests_that_didnt_finish(planned_tests, test_results)
|
66
|
+
].compact.join("\n\n")
|
67
|
+
end
|
68
|
+
|
69
|
+
after_suite test_results
|
70
|
+
end
|
71
|
+
|
72
|
+
def after_suite test_results
|
73
|
+
duration = time_diff @suite_start_time
|
74
|
+
test_results = test_results.sort_by { |result| [result.test.location.file, result.test.location.line] }
|
75
|
+
|
76
|
+
@out.print "\n\n"
|
77
|
+
@err.print summarize_failures(test_results).join("\n\n")
|
78
|
+
|
79
|
+
@out.print "\n\n"
|
80
|
+
@out.print summarize_skips(test_results).join("\n")
|
81
|
+
|
82
|
+
@out.print "\n\n"
|
83
|
+
@out.print "Finished in #{duration}ms."
|
84
|
+
|
85
|
+
@out.print "\n\n"
|
86
|
+
class_count = test_results.uniq { |result| result.test.class }.size
|
87
|
+
test_count = test_results.size
|
88
|
+
@out.print [
|
89
|
+
plural(class_count, "test class", "test classes"),
|
90
|
+
plural(test_count, "test method"),
|
91
|
+
plural(test_results.count(&:failure?), "failure"),
|
92
|
+
plural(test_results.count(&:error?), "error"),
|
93
|
+
plural(test_results.count(&:skip?), "skip")
|
94
|
+
].join(", ")
|
95
|
+
|
96
|
+
@out.print "\n"
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def summarize_failures results
|
102
|
+
failures = results.select { |result| result.failing? }
|
103
|
+
return failures if failures.empty?
|
104
|
+
|
105
|
+
["Failing tests:"] + failures.map.with_index { |result, i| summarize_result result, i }
|
106
|
+
end
|
107
|
+
|
108
|
+
def summarize_result result, index
|
109
|
+
[
|
110
|
+
"#{index + 1}) #{describe(result.test, result.relevant_location)} #{result.failure? ? "failed" : "errored"}:",
|
111
|
+
result.error.message.chomp,
|
112
|
+
"\n Re-run this test:",
|
113
|
+
" #{tldr_command} #{@config.to_single_path_args(result.test.location.locator)}",
|
114
|
+
(TLDR.filter_backtrace(result.error.backtrace).join("\n") if @config.verbose)
|
115
|
+
].compact.reject(&:empty?).join("\n").strip
|
116
|
+
end
|
117
|
+
|
118
|
+
def summarize_skips results
|
119
|
+
skips = results.select { |result| result.skip? }
|
120
|
+
return skips if skips.empty?
|
121
|
+
|
122
|
+
["Skipped tests:\n"] + skips.map { |result| " - #{describe(result.test)}" }
|
123
|
+
end
|
124
|
+
|
125
|
+
def describe test, location = test.location
|
126
|
+
"#{test.klass}##{test.method} [#{location.locator}]"
|
127
|
+
end
|
128
|
+
|
129
|
+
def plural count, singular, plural = "#{singular}s"
|
130
|
+
"#{count} #{(count == 1) ? singular : plural}"
|
131
|
+
end
|
132
|
+
|
133
|
+
def wrap_in_horizontal_rule
|
134
|
+
rule = @icons.alarm + "=" * 20 + " ABORTED RUN " + "=" * 20 + @icons.alarm
|
135
|
+
@err.print "#{rule}\n\n"
|
136
|
+
yield
|
137
|
+
@err.print "\n\n#{rule}\n\n"
|
138
|
+
end
|
139
|
+
|
140
|
+
def describe_tests_that_didnt_finish planned_tests, test_results
|
141
|
+
unrun = planned_tests - test_results.map(&:test)
|
142
|
+
return if unrun.empty?
|
143
|
+
|
144
|
+
unrun_locators = consolidate unrun
|
145
|
+
failed = test_results.select(&:failing?).map(&:test)
|
146
|
+
failed_locators = consolidate failed, exclude: unrun_locators
|
147
|
+
suggested_locators = unrun_locators + [
|
148
|
+
("--comment \"Also include #{plural failed.size, "test"} that failed:\"" if failed_locators.any?)
|
149
|
+
] + failed_locators
|
150
|
+
<<~MSG
|
151
|
+
#{@icons.rock_on} Run the #{plural unrun.size, "test"} that didn't finish:
|
152
|
+
#{tldr_command} #{@config.to_full_args exclude: [:paths]} #{suggested_locators.join(" \\\n ")}
|
153
|
+
MSG
|
154
|
+
end
|
155
|
+
|
156
|
+
def consolidate tests, exclude: []
|
157
|
+
tests.group_by(&:file).map { |_, tests|
|
158
|
+
"\"#{tests.first.location.relative}:#{tests.map(&:line).uniq.sort.join(":")}\""
|
159
|
+
}.uniq - exclude
|
160
|
+
end
|
161
|
+
|
162
|
+
def tldr_command
|
163
|
+
"#{"bundle exec " if defined?(Bundler)}tldr"
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module IconProvider
|
2
|
+
class Base
|
3
|
+
def success
|
4
|
+
"."
|
5
|
+
end
|
6
|
+
|
7
|
+
def failure
|
8
|
+
"F"
|
9
|
+
end
|
10
|
+
|
11
|
+
def error
|
12
|
+
"E"
|
13
|
+
end
|
14
|
+
|
15
|
+
def skip
|
16
|
+
"S"
|
17
|
+
end
|
18
|
+
|
19
|
+
def tldr
|
20
|
+
"!"
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
""
|
25
|
+
end
|
26
|
+
|
27
|
+
def wip
|
28
|
+
""
|
29
|
+
end
|
30
|
+
|
31
|
+
def slow
|
32
|
+
""
|
33
|
+
end
|
34
|
+
|
35
|
+
def not_run
|
36
|
+
""
|
37
|
+
end
|
38
|
+
|
39
|
+
def alarm
|
40
|
+
""
|
41
|
+
end
|
42
|
+
|
43
|
+
def rock_on
|
44
|
+
""
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Emoji < Base
|
49
|
+
def success
|
50
|
+
"😁"
|
51
|
+
end
|
52
|
+
|
53
|
+
def failure
|
54
|
+
"😡"
|
55
|
+
end
|
56
|
+
|
57
|
+
def error
|
58
|
+
"🤬"
|
59
|
+
end
|
60
|
+
|
61
|
+
def skip
|
62
|
+
"🫥"
|
63
|
+
end
|
64
|
+
|
65
|
+
def tldr
|
66
|
+
"🥵"
|
67
|
+
end
|
68
|
+
|
69
|
+
def run
|
70
|
+
"🏃"
|
71
|
+
end
|
72
|
+
|
73
|
+
def wip
|
74
|
+
"🙅"
|
75
|
+
end
|
76
|
+
|
77
|
+
def slow
|
78
|
+
"🐢"
|
79
|
+
end
|
80
|
+
|
81
|
+
def not_run
|
82
|
+
"🙈"
|
83
|
+
end
|
84
|
+
|
85
|
+
def alarm
|
86
|
+
"🚨"
|
87
|
+
end
|
88
|
+
|
89
|
+
def rock_on
|
90
|
+
"🤘"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/tldr/runner.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
require "irb"
|
2
|
+
require "concurrent"
|
3
|
+
|
4
|
+
class TLDR
|
5
|
+
class Runner
|
6
|
+
def initialize
|
7
|
+
@wip = Concurrent::Array.new
|
8
|
+
@results = Concurrent::Array.new
|
9
|
+
@run_aborted = Concurrent::AtomicBoolean.new false
|
10
|
+
end
|
11
|
+
|
12
|
+
def run config, plan
|
13
|
+
@wip.clear
|
14
|
+
@results.clear
|
15
|
+
reporter = config.reporter.new config
|
16
|
+
reporter.before_suite plan.tests
|
17
|
+
|
18
|
+
time_bomb = Thread.new {
|
19
|
+
explode = proc do
|
20
|
+
next if ENV["CI"] && !$stderr.tty?
|
21
|
+
next if @run_aborted.true?
|
22
|
+
@run_aborted.make_true
|
23
|
+
reporter.after_tldr plan.tests, @wip.dup, @results.dup
|
24
|
+
exit! 3
|
25
|
+
end
|
26
|
+
|
27
|
+
sleep 1.8
|
28
|
+
# Don't hard-kill the runner if user is debugging, it'll
|
29
|
+
# screw up their terminal slash be a bad time
|
30
|
+
if IRB.CurrentContext
|
31
|
+
IRB.conf[:AT_EXIT] << explode
|
32
|
+
else
|
33
|
+
explode.call
|
34
|
+
end
|
35
|
+
}
|
36
|
+
|
37
|
+
results = parallelize(plan.tests, config.parallel) { |test|
|
38
|
+
e = nil
|
39
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
40
|
+
wip_test = WIPTest.new test, start_time
|
41
|
+
@wip << wip_test
|
42
|
+
runtime = time_it(start_time) do
|
43
|
+
instance = test.klass.new
|
44
|
+
instance.setup if instance.respond_to? :setup
|
45
|
+
instance.send(test.method)
|
46
|
+
instance.teardown if instance.respond_to? :teardown
|
47
|
+
rescue Skip, Failure, StandardError => e
|
48
|
+
end
|
49
|
+
TestResult.new(test, e, runtime).tap do |result|
|
50
|
+
next if @run_aborted.true?
|
51
|
+
@results << result
|
52
|
+
@wip.delete wip_test
|
53
|
+
reporter.after_test result
|
54
|
+
fail_fast reporter, plan, result if result.failing? && config.fail_fast
|
55
|
+
end
|
56
|
+
}.tap do
|
57
|
+
time_bomb.kill
|
58
|
+
end
|
59
|
+
|
60
|
+
unless @run_aborted.true?
|
61
|
+
reporter.after_suite results
|
62
|
+
exit exit_code results
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def parallelize tests, parallel, &blk
|
69
|
+
return tests.map(&blk) if tests.size < 2 || !parallel
|
70
|
+
tldr_pool = Concurrent::ThreadPoolExecutor.new(
|
71
|
+
name: "tldr",
|
72
|
+
auto_terminate: true
|
73
|
+
)
|
74
|
+
|
75
|
+
tests.map { |test|
|
76
|
+
Concurrent::Promises.future_on(tldr_pool) {
|
77
|
+
blk.call test
|
78
|
+
}
|
79
|
+
}.flat_map(&:value)
|
80
|
+
end
|
81
|
+
|
82
|
+
def fail_fast reporter, plan, fast_failed_result
|
83
|
+
unless @run_aborted.true?
|
84
|
+
@run_aborted.make_true
|
85
|
+
abort = proc do
|
86
|
+
reporter.after_fail_fast plan.tests, @wip.dup, @results.dup, fast_failed_result
|
87
|
+
exit! exit_code([fast_failed_result])
|
88
|
+
end
|
89
|
+
|
90
|
+
if IRB.CurrentContext
|
91
|
+
IRB.conf[:AT_EXIT] << abort
|
92
|
+
else
|
93
|
+
abort.call
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def time_it(start)
|
99
|
+
yield
|
100
|
+
((Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - start) / 1000.0).round
|
101
|
+
end
|
102
|
+
|
103
|
+
def exit_code results
|
104
|
+
if results.any? { |result| result.error? }
|
105
|
+
2
|
106
|
+
elsif results.any? { |result| result.failure? }
|
107
|
+
1
|
108
|
+
else
|
109
|
+
0
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
require "concurrent"
|
2
|
+
|
3
|
+
class TLDR
|
4
|
+
CONFLAGS = {
|
5
|
+
seed: "--seed",
|
6
|
+
no_helper: "--no-helper",
|
7
|
+
verbose: "--verbose",
|
8
|
+
reporter: "--reporter",
|
9
|
+
helper: "--helper",
|
10
|
+
load_paths: "--load-path",
|
11
|
+
parallel: "--[no-]parallel",
|
12
|
+
names: "--name",
|
13
|
+
fail_fast: "--fail-fast",
|
14
|
+
no_emoji: "--no-emoji",
|
15
|
+
prepend_tests: "--prepend",
|
16
|
+
no_prepend: "--no-prepend",
|
17
|
+
exclude_paths: "--exclude-path",
|
18
|
+
exclude_names: "--exclude-name",
|
19
|
+
base_path: "--base-path",
|
20
|
+
no_dotfile: "--no-dotfile",
|
21
|
+
paths: nil
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
PATH_FLAGS = [:paths, :helper, :load_paths, :prepend_tests, :exclude_paths].freeze
|
25
|
+
MOST_RECENTLY_MODIFIED_TAG = "MOST_RECENTLY_MODIFIED".freeze
|
26
|
+
|
27
|
+
Config = Struct.new :paths, :seed, :no_helper, :verbose, :reporter,
|
28
|
+
:helper, :load_paths, :parallel, :names, :fail_fast, :no_emoji,
|
29
|
+
:prepend_tests, :no_prepend, :exclude_paths, :exclude_names, :base_path,
|
30
|
+
:no_dotfile,
|
31
|
+
:seed_set_intentionally, :cli_mode, keyword_init: true do
|
32
|
+
def initialize(**args)
|
33
|
+
change_working_directory_because_i_am_bad_and_i_should_feel_bad!(args[:base_path])
|
34
|
+
args = merge_dotfile_args(args) unless args[:no_dotfile]
|
35
|
+
|
36
|
+
super(**merge_defaults(args))
|
37
|
+
end
|
38
|
+
|
39
|
+
# Must be set when the Config is first initialized
|
40
|
+
undef_method :cli_mode=, :no_dotfile=, :base_path=
|
41
|
+
|
42
|
+
def self.build_defaults(cli_mode = false)
|
43
|
+
common = {
|
44
|
+
seed: rand(10_000),
|
45
|
+
no_helper: false,
|
46
|
+
verbose: false,
|
47
|
+
reporter: Reporters::Default,
|
48
|
+
parallel: true,
|
49
|
+
names: [],
|
50
|
+
fail_fast: false,
|
51
|
+
no_emoji: false,
|
52
|
+
no_prepend: false,
|
53
|
+
exclude_paths: [],
|
54
|
+
exclude_names: [],
|
55
|
+
base_path: nil
|
56
|
+
}
|
57
|
+
|
58
|
+
if cli_mode
|
59
|
+
common.merge(
|
60
|
+
paths: Dir["test/**/*_test.rb", "test/**/test_*.rb"],
|
61
|
+
helper: "test/helper.rb",
|
62
|
+
load_paths: ["test"],
|
63
|
+
prepend_tests: [MOST_RECENTLY_MODIFIED_TAG]
|
64
|
+
)
|
65
|
+
else
|
66
|
+
common.merge(
|
67
|
+
paths: [],
|
68
|
+
helper: nil,
|
69
|
+
load_paths: [],
|
70
|
+
prepend_tests: []
|
71
|
+
)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def merge_defaults(user_args)
|
76
|
+
merged_args = user_args.dup
|
77
|
+
defaults = Config.build_defaults(merged_args[:cli_mode])
|
78
|
+
merged_args[:seed_set_intentionally] = !merged_args[:seed].nil?
|
79
|
+
|
80
|
+
# Special cases
|
81
|
+
if merged_args[:parallel].nil?
|
82
|
+
# Disable parallelization if seed is set
|
83
|
+
merged_args[:parallel] = merged_args[:seed].nil?
|
84
|
+
end
|
85
|
+
|
86
|
+
# Arrays
|
87
|
+
[:paths, :load_paths, :names, :prepend_tests, :exclude_paths, :exclude_names].each do |key|
|
88
|
+
merged_args[key] = defaults[key] if merged_args[key].nil? || merged_args[key].empty?
|
89
|
+
end
|
90
|
+
|
91
|
+
# Booleans
|
92
|
+
[:no_helper, :verbose, :fail_fast, :no_emoji, :no_prepend].each do |key|
|
93
|
+
merged_args[key] = defaults[key] if merged_args[key].nil?
|
94
|
+
end
|
95
|
+
|
96
|
+
# Values
|
97
|
+
[:seed, :reporter, :helper].each do |key|
|
98
|
+
merged_args[key] ||= defaults[key]
|
99
|
+
end
|
100
|
+
|
101
|
+
merged_args
|
102
|
+
end
|
103
|
+
|
104
|
+
# We needed this hook (to be called by the planner), because we can't know
|
105
|
+
# the default prepend location until we have all the resolved test paths,
|
106
|
+
# so we have to mutate it after the fact.
|
107
|
+
def update_after_gathering_tests! tests
|
108
|
+
return unless prepend_tests.include?(MOST_RECENTLY_MODIFIED_TAG)
|
109
|
+
|
110
|
+
self.prepend_tests = prepend_tests.map { |path|
|
111
|
+
if path == MOST_RECENTLY_MODIFIED_TAG
|
112
|
+
most_recently_modified_test_file tests
|
113
|
+
else
|
114
|
+
path
|
115
|
+
end
|
116
|
+
}.compact
|
117
|
+
end
|
118
|
+
|
119
|
+
def to_full_args(exclude: [])
|
120
|
+
to_cli_argv(CONFLAGS.keys - exclude).join(" ")
|
121
|
+
end
|
122
|
+
|
123
|
+
def to_single_path_args(path)
|
124
|
+
argv = to_cli_argv(CONFLAGS.keys - [
|
125
|
+
:seed, :parallel, :names, :fail_fast, :paths, :prepend_tests,
|
126
|
+
:no_prepend, :exclude_paths
|
127
|
+
])
|
128
|
+
|
129
|
+
(argv + [stringify(:paths, path)]).join(" ")
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def to_cli_argv(options = CONFLAGS.keys)
|
135
|
+
defaults = Config.build_defaults(cli_mode)
|
136
|
+
options.map { |key|
|
137
|
+
flag = CONFLAGS[key]
|
138
|
+
|
139
|
+
# Special cases
|
140
|
+
if key == :prepend_tests
|
141
|
+
if prepend_tests.map { |s| stringify(key, s) }.sort == paths.map { |s| stringify(:paths, s) }.sort
|
142
|
+
# Don't print prepended tests if they're the same as the test paths
|
143
|
+
next
|
144
|
+
elsif no_prepend
|
145
|
+
# Don't print prepended tests if they're disabled
|
146
|
+
next
|
147
|
+
end
|
148
|
+
elsif key == :helper && no_helper
|
149
|
+
# Don't print the helper if it's disabled
|
150
|
+
next
|
151
|
+
elsif key == :parallel
|
152
|
+
val = if !seed_set_intentionally && !parallel
|
153
|
+
"--no-parallel"
|
154
|
+
elsif !seed.nil? && seed_set_intentionally && parallel
|
155
|
+
"--parallel"
|
156
|
+
end
|
157
|
+
next val
|
158
|
+
end
|
159
|
+
|
160
|
+
if defaults[key] == self[key]
|
161
|
+
next
|
162
|
+
elsif self[key].is_a?(Array)
|
163
|
+
self[key].map { |value| [flag, stringify(key, value)] }
|
164
|
+
elsif self[key].is_a?(TrueClass) || self[key].is_a?(FalseClass)
|
165
|
+
flag if self[key]
|
166
|
+
elsif self[key].is_a?(Class)
|
167
|
+
[flag, self[key].name]
|
168
|
+
elsif !self[key].nil?
|
169
|
+
[flag, stringify(key, self[key])]
|
170
|
+
end
|
171
|
+
}.flatten.compact
|
172
|
+
end
|
173
|
+
|
174
|
+
def stringify key, val
|
175
|
+
if PATH_FLAGS.include?(key) && val.start_with?(Dir.pwd)
|
176
|
+
val = val[Dir.pwd.length + 1..]
|
177
|
+
end
|
178
|
+
|
179
|
+
if val.nil? || val.is_a?(Integer)
|
180
|
+
val
|
181
|
+
else
|
182
|
+
"\"#{val}\""
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def most_recently_modified_test_file(tests)
|
187
|
+
return if tests.empty?
|
188
|
+
|
189
|
+
tests.max_by { |test| File.mtime(test.file) }.file
|
190
|
+
end
|
191
|
+
|
192
|
+
# If the user sets a custom base path, we need to change the working directory
|
193
|
+
# ASAP, even before globbing to find default paths of tests. If there is
|
194
|
+
# a way to change all of our Dir.glob calls to be relative to base_path
|
195
|
+
# without a loss in accuracy, would love to not have to use Dir.chdir!
|
196
|
+
def change_working_directory_because_i_am_bad_and_i_should_feel_bad!(base_path)
|
197
|
+
Dir.chdir(base_path) unless base_path.nil?
|
198
|
+
end
|
199
|
+
|
200
|
+
def merge_dotfile_args(args)
|
201
|
+
return args if args[:no_dotfile] || !File.exist?(".tldr.yml")
|
202
|
+
require "yaml"
|
203
|
+
|
204
|
+
dotfile_args = YAML.load_file(".tldr.yml").transform_keys { |k| k.to_sym }
|
205
|
+
# Since we don't have shell expansion, we have to glob any paths ourselves
|
206
|
+
if dotfile_args.key?(:paths)
|
207
|
+
dotfile_args[:paths] = dotfile_args[:paths].flat_map { |path| Dir[path] }
|
208
|
+
end
|
209
|
+
# The argv parser normally does this:
|
210
|
+
if dotfile_args.key?(:reporter)
|
211
|
+
dotfile_args[:reporter] = Kernel.const_get(dotfile_args[:reporter])
|
212
|
+
end
|
213
|
+
|
214
|
+
dotfile_args.merge(args)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class TLDR
|
2
|
+
Test = Struct.new :klass, :method, :file, :line do
|
3
|
+
attr_reader :location
|
4
|
+
|
5
|
+
def initialize(*args)
|
6
|
+
super
|
7
|
+
@location = Location.new(file, line)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Memoizing at call time, because re-parsing isn't free and isn't usually necessary
|
11
|
+
def end_line
|
12
|
+
@end_line ||= begin
|
13
|
+
test_method = SorbetCompatibility.unwrap_method klass.instance_method(method)
|
14
|
+
RubyVM::AbstractSyntaxTree.of(test_method).last_lineno
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Test exact match starting line condition first to save us a potential re-parsing to look up end_line
|
19
|
+
def covers_line? l
|
20
|
+
line == l || (l >= line && l <= end_line)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|