tldr 0.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.
- 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
|