reviewer 0.1.5 → 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.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +3 -0
- data/.github/workflows/main.yml +79 -11
- data/.github/workflows/release.yml +98 -0
- data/.gitignore +1 -1
- data/.inch.yml +3 -1
- data/.reek.yml +175 -0
- data/.reviewer.example.yml +7 -2
- data/.reviewer.yml +166 -40
- data/.rubocop.yml +34 -2
- data/CHANGELOG.md +42 -2
- data/Gemfile +39 -1
- data/Gemfile.lock +291 -70
- data/LICENSE.txt +20 -4
- data/README.md +310 -21
- data/RELEASING.md +190 -0
- data/Rakefile +117 -0
- data/dependency_decisions.yml +61 -0
- data/exe/fmt +1 -1
- data/exe/rvw +1 -1
- data/lib/reviewer/arguments/files.rb +47 -20
- data/lib/reviewer/arguments/keywords.rb +34 -41
- data/lib/reviewer/arguments/tags.rb +11 -11
- data/lib/reviewer/arguments.rb +100 -29
- data/lib/reviewer/batch/formatter.rb +87 -0
- data/lib/reviewer/batch.rb +32 -48
- data/lib/reviewer/capabilities.rb +81 -0
- data/lib/reviewer/command/string/env.rb +12 -6
- data/lib/reviewer/command/string/flags.rb +2 -4
- data/lib/reviewer/command/string.rb +47 -12
- data/lib/reviewer/command.rb +65 -10
- data/lib/reviewer/configuration/loader.rb +70 -0
- data/lib/reviewer/configuration.rb +6 -3
- data/lib/reviewer/context.rb +15 -0
- data/lib/reviewer/doctor/config_check.rb +46 -0
- data/lib/reviewer/doctor/environment_check.rb +58 -0
- data/lib/reviewer/doctor/formatter.rb +75 -0
- data/lib/reviewer/doctor/keyword_check.rb +85 -0
- data/lib/reviewer/doctor/opportunity_check.rb +88 -0
- data/lib/reviewer/doctor/report.rb +63 -0
- data/lib/reviewer/doctor/tool_inventory.rb +41 -0
- data/lib/reviewer/doctor.rb +28 -0
- data/lib/reviewer/history.rb +10 -17
- data/lib/reviewer/output/formatting.rb +40 -0
- data/lib/reviewer/output/printer.rb +70 -9
- data/lib/reviewer/output.rb +37 -78
- data/lib/reviewer/prompt.rb +38 -0
- data/lib/reviewer/report/formatter.rb +124 -0
- data/lib/reviewer/report.rb +100 -0
- data/lib/reviewer/runner/failed_files.rb +66 -0
- data/lib/reviewer/runner/formatter.rb +103 -0
- data/lib/reviewer/runner/guidance.rb +79 -0
- data/lib/reviewer/runner/result.rb +150 -0
- data/lib/reviewer/runner/strategies/captured.rb +98 -23
- data/lib/reviewer/runner/strategies/passthrough.rb +2 -11
- data/lib/reviewer/runner.rb +126 -40
- data/lib/reviewer/session/formatter.rb +87 -0
- data/lib/reviewer/session.rb +208 -0
- data/lib/reviewer/setup/catalog.rb +233 -0
- data/lib/reviewer/setup/detector.rb +61 -0
- data/lib/reviewer/setup/formatter.rb +94 -0
- data/lib/reviewer/setup/gemfile_lock.rb +55 -0
- data/lib/reviewer/setup/generator.rb +54 -0
- data/lib/reviewer/setup/tool_block.rb +112 -0
- data/lib/reviewer/setup.rb +41 -0
- data/lib/reviewer/shell/result.rb +14 -15
- data/lib/reviewer/shell/timer.rb +40 -35
- data/lib/reviewer/shell.rb +41 -12
- data/lib/reviewer/tool/conversions.rb +20 -0
- data/lib/reviewer/tool/file_resolver.rb +54 -0
- data/lib/reviewer/tool/settings.rb +88 -44
- data/lib/reviewer/tool/test_file_mapper.rb +73 -0
- data/lib/reviewer/tool/timing.rb +78 -0
- data/lib/reviewer/tool.rb +88 -69
- data/lib/reviewer/tools.rb +47 -33
- data/lib/reviewer/version.rb +1 -1
- data/lib/reviewer.rb +109 -50
- data/reviewer.gemspec +16 -19
- metadata +101 -142
- data/lib/reviewer/conversions.rb +0 -16
- data/lib/reviewer/guidance.rb +0 -77
- data/lib/reviewer/keywords/git/staged.rb +0 -64
- data/lib/reviewer/keywords/git.rb +0 -14
- data/lib/reviewer/keywords.rb +0 -9
- data/lib/reviewer/loader.rb +0 -59
- data/lib/reviewer/output/scrubber.rb +0 -48
- data/lib/reviewer/output/token.rb +0 -85
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
class Runner
|
|
5
|
+
# Immutable value object representing the result of running a single tool
|
|
6
|
+
#
|
|
7
|
+
# @!attribute [r] tool_key
|
|
8
|
+
# @return [Symbol] the unique identifier for the tool
|
|
9
|
+
# @!attribute [r] tool_name
|
|
10
|
+
# @return [String] the human-readable name of the tool
|
|
11
|
+
# @!attribute [r] command_type
|
|
12
|
+
# @return [Symbol] the type of command run (:review, :format, etc.)
|
|
13
|
+
# @!attribute [r] command_string
|
|
14
|
+
# @return [String] the full command string that was executed
|
|
15
|
+
# @!attribute [r] success
|
|
16
|
+
# @return [Boolean] whether the command completed successfully
|
|
17
|
+
# @!attribute [r] exit_status
|
|
18
|
+
# @return [Integer] the exit status code from the command
|
|
19
|
+
# @!attribute [r] duration
|
|
20
|
+
# @return [Float] the execution time in seconds
|
|
21
|
+
# @!attribute [r] stdout
|
|
22
|
+
# @return [String, nil] the standard output from the command
|
|
23
|
+
# @!attribute [r] stderr
|
|
24
|
+
# @return [String, nil] the standard error from the command
|
|
25
|
+
# @!attribute [r] skipped
|
|
26
|
+
# @return [Boolean] whether the tool was skipped
|
|
27
|
+
# @!attribute [r] missing
|
|
28
|
+
# @return [Boolean] whether the tool's executable was not found
|
|
29
|
+
Result = Struct.new(
|
|
30
|
+
:tool_key,
|
|
31
|
+
:tool_name,
|
|
32
|
+
:command_type,
|
|
33
|
+
:command_string,
|
|
34
|
+
:success,
|
|
35
|
+
:exit_status,
|
|
36
|
+
:duration,
|
|
37
|
+
:stdout,
|
|
38
|
+
:stderr,
|
|
39
|
+
:skipped,
|
|
40
|
+
:missing,
|
|
41
|
+
:summary_pattern,
|
|
42
|
+
:summary_label,
|
|
43
|
+
keyword_init: true
|
|
44
|
+
) do
|
|
45
|
+
# Freeze on initialization to maintain immutability like Data.define
|
|
46
|
+
def initialize(...)
|
|
47
|
+
super
|
|
48
|
+
freeze
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Builds an immutable Result from a runner's current state.
|
|
52
|
+
# @param runner [Runner] the runner after command execution
|
|
53
|
+
#
|
|
54
|
+
# @return [Result] an immutable result for reporting
|
|
55
|
+
def self.from_runner(runner)
|
|
56
|
+
if runner.skipped?
|
|
57
|
+
build_skipped(runner)
|
|
58
|
+
elsif runner.missing?
|
|
59
|
+
build_missing(runner)
|
|
60
|
+
else
|
|
61
|
+
build_executed(runner)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.base_attributes(runner)
|
|
66
|
+
tool = runner.tool
|
|
67
|
+
{
|
|
68
|
+
tool_key: tool.key,
|
|
69
|
+
tool_name: tool.name,
|
|
70
|
+
command_type: runner.command.type,
|
|
71
|
+
command_string: runner.command.string
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.build_skipped(runner)
|
|
76
|
+
new(
|
|
77
|
+
**base_attributes(runner),
|
|
78
|
+
command_string: nil,
|
|
79
|
+
success: true, exit_status: 0, duration: 0,
|
|
80
|
+
stdout: nil, stderr: nil, skipped: true
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.build_missing(runner)
|
|
85
|
+
new(
|
|
86
|
+
**base_attributes(runner),
|
|
87
|
+
success: false, exit_status: runner.shell.result.exit_status, duration: 0,
|
|
88
|
+
stdout: nil, stderr: nil, skipped: nil, missing: true
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.build_executed(runner)
|
|
93
|
+
shell = runner.shell
|
|
94
|
+
shell_result = shell.result
|
|
95
|
+
settings = runner.tool.settings
|
|
96
|
+
new(
|
|
97
|
+
**base_attributes(runner),
|
|
98
|
+
success: runner.success?, exit_status: shell_result.exit_status,
|
|
99
|
+
duration: shell.timer.total_seconds,
|
|
100
|
+
stdout: shell_result.stdout, stderr: shell_result.stderr, skipped: nil,
|
|
101
|
+
summary_pattern: settings.summary_pattern,
|
|
102
|
+
summary_label: settings.summary_label
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private_class_method :base_attributes, :build_skipped, :build_missing, :build_executed
|
|
107
|
+
|
|
108
|
+
alias_method :success?, :success
|
|
109
|
+
alias_method :skipped?, :skipped
|
|
110
|
+
alias_method :missing?, :missing
|
|
111
|
+
|
|
112
|
+
# Whether this result represents a tool that actually ran (not skipped or missing)
|
|
113
|
+
#
|
|
114
|
+
# @return [Boolean] true if the tool was executed
|
|
115
|
+
def executed? = !skipped? && !missing?
|
|
116
|
+
|
|
117
|
+
# Extracts a short summary detail from stdout for display purposes.
|
|
118
|
+
# Each tool type may have its own summary format (test count, offense count, etc.)
|
|
119
|
+
#
|
|
120
|
+
# @return [String, nil] a brief summary or nil if no detail can be extracted
|
|
121
|
+
def detail_summary
|
|
122
|
+
return nil unless summary_pattern
|
|
123
|
+
|
|
124
|
+
match = stdout&.match(/#{summary_pattern}/i)
|
|
125
|
+
return nil unless match
|
|
126
|
+
|
|
127
|
+
summary_label.gsub(/\\(\d+)/) { match[Regexp.last_match(1).to_i] }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Converts the result to a hash suitable for serialization
|
|
131
|
+
#
|
|
132
|
+
# @return [Hash] hash representation with nil values removed
|
|
133
|
+
def to_h
|
|
134
|
+
{
|
|
135
|
+
tool: tool_key,
|
|
136
|
+
name: tool_name,
|
|
137
|
+
command_type: command_type,
|
|
138
|
+
command: command_string,
|
|
139
|
+
success: success,
|
|
140
|
+
exit_status: exit_status,
|
|
141
|
+
duration: duration,
|
|
142
|
+
stdout: stdout,
|
|
143
|
+
stderr: stderr,
|
|
144
|
+
skipped: skipped,
|
|
145
|
+
missing: missing
|
|
146
|
+
}.compact # Excludes summary_pattern/summary_label (config, not results)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'ruby-progressbar'
|
|
4
|
+
|
|
3
5
|
module Reviewer
|
|
4
6
|
class Runner
|
|
5
7
|
module Strategies
|
|
@@ -8,6 +10,12 @@ module Reviewer
|
|
|
8
10
|
# @attr_reader runner [Runner] the instance of the runner that will be executed with this strategy
|
|
9
11
|
# @attr_reader start_time [Time] the start time for the strategy_for timing purposes
|
|
10
12
|
class Captured
|
|
13
|
+
# 256-color ANSI codes for progress bar styling (not in AnsiStyles' 16-color palette)
|
|
14
|
+
PROGRESS_GRAY = "\e[38;5;245m"
|
|
15
|
+
PROGRESS_DARK_GRAY = "\e[38;5;240m"
|
|
16
|
+
ANSI_RESET = "\e[0m"
|
|
17
|
+
ERASE_LINE = "\r\e[2K"
|
|
18
|
+
|
|
11
19
|
attr_reader :runner, :start_time
|
|
12
20
|
|
|
13
21
|
# Create an instance of the captured strategy for a command runner so that any output is
|
|
@@ -28,6 +36,9 @@ module Reviewer
|
|
|
28
36
|
|
|
29
37
|
display_progress(command) { runner.shell.capture_prep(command) }
|
|
30
38
|
|
|
39
|
+
# Erase the prep progress bar — the run step will show its own
|
|
40
|
+
stream.print(ERASE_LINE) if style_enabled? && runner.streaming?
|
|
41
|
+
|
|
31
42
|
# Running the prepare command, so make sure the timestamp is updated
|
|
32
43
|
runner.update_last_prepared_at
|
|
33
44
|
end
|
|
@@ -40,6 +51,12 @@ module Reviewer
|
|
|
40
51
|
|
|
41
52
|
display_progress(command) { runner.shell.capture_main(command) }
|
|
42
53
|
|
|
54
|
+
# Skip output for non-streaming modes - results are formatted at the end
|
|
55
|
+
return unless runner.streaming?
|
|
56
|
+
|
|
57
|
+
# Missing tools are handled by the Runner (shows "Skipped (not installed)")
|
|
58
|
+
return if runner.shell.result.executable_not_found?
|
|
59
|
+
|
|
43
60
|
# If it's successful, show that it was a success and how long it took to run, otherwise,
|
|
44
61
|
# it wasn't successful and we got some explaining to do...
|
|
45
62
|
runner.success? ? show_timing_result : show_command_output
|
|
@@ -51,36 +68,92 @@ module Reviewer
|
|
|
51
68
|
# Helps people know that the sub-command is running within expectations.
|
|
52
69
|
# @param command [String] the precise command string generated by Reviewer. Serves as the
|
|
53
70
|
# key for looking up the command's history.
|
|
54
|
-
# @param
|
|
71
|
+
# @param block [Block] the runner for the command that's being timed and having its
|
|
55
72
|
# progress updated and printed
|
|
56
73
|
#
|
|
57
74
|
# @return [void]
|
|
58
|
-
def display_progress(command
|
|
59
|
-
start_time = Time.now
|
|
75
|
+
def display_progress(command)
|
|
60
76
|
average_time = runner.tool.average_time(command)
|
|
77
|
+
start_time = Time.now
|
|
78
|
+
thread = Thread.new { yield }
|
|
61
79
|
|
|
62
|
-
|
|
80
|
+
# Skip progress output for non-streaming modes
|
|
81
|
+
return thread.join unless runner.streaming?
|
|
82
|
+
|
|
83
|
+
print_progress(thread, start_time, average_time)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def print_progress(thread, start_time, average_time)
|
|
87
|
+
bar = create_progress_bar(average_time)
|
|
63
88
|
|
|
64
89
|
while thread.alive?
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
update_progress(bar, start_time, average_time)
|
|
91
|
+
sleep 0.1
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
thread.join
|
|
95
|
+
|
|
96
|
+
# Erase the progress bar line if the executable wasn't found
|
|
97
|
+
if runner.shell.result.executable_not_found?
|
|
98
|
+
stream.print(ERASE_LINE) if style_enabled?
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
finish_progress_bar(bar, average_time.positive?)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def finish_progress_bar(bar, timed)
|
|
106
|
+
if timed
|
|
107
|
+
bar.format = style_enabled? ? "#{PROGRESS_GRAY}%b#{PROGRESS_DARK_GRAY}%i %p%%#{ANSI_RESET}" : '%b%i %p%%'
|
|
108
|
+
bar.finish
|
|
109
|
+
else
|
|
110
|
+
bar.stop
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def create_progress_bar(average_time)
|
|
115
|
+
shared = {
|
|
116
|
+
output: stream,
|
|
117
|
+
title: '',
|
|
118
|
+
progress_mark: "\u2501",
|
|
119
|
+
remainder_mark: "\u2500"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if average_time.positive?
|
|
123
|
+
eta = average_time >= 3 ? ' %e' : ''
|
|
124
|
+
format = style_enabled? ? "#{PROGRESS_GRAY}%b#{PROGRESS_DARK_GRAY}%i %p%%#{eta}#{ANSI_RESET}" : "%b%i %p%%#{eta}"
|
|
125
|
+
ProgressBar.create(**shared, total: 100, format: format)
|
|
126
|
+
else
|
|
127
|
+
format = style_enabled? ? "#{PROGRESS_GRAY}%B#{ANSI_RESET}" : '%B'
|
|
128
|
+
ProgressBar.create(**shared, total: nil, format: format)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def update_progress(bar, start_time, average_time)
|
|
133
|
+
if average_time.positive?
|
|
134
|
+
elapsed = Time.now - start_time
|
|
135
|
+
percent = [(elapsed / average_time * 100).round, 99].min
|
|
136
|
+
bar.progress = percent if percent > bar.progress
|
|
137
|
+
else
|
|
138
|
+
bar.increment
|
|
74
139
|
end
|
|
75
140
|
end
|
|
76
141
|
|
|
142
|
+
# The output stream from the runner's printer
|
|
143
|
+
# @return [IO]
|
|
144
|
+
def stream = runner.output.printer.stream
|
|
145
|
+
|
|
146
|
+
# Whether ANSI styling is enabled on the output stream
|
|
147
|
+
# @return [Boolean]
|
|
148
|
+
def style_enabled? = runner.output.printer.style_enabled?
|
|
149
|
+
|
|
77
150
|
# Determines if stdout or stderr captured any useful output that can be displayed in order
|
|
78
151
|
# to more rapidly display output when a command fails. As long as both aren't nil or
|
|
79
152
|
# otherwise 'blank' strings, then that's enough.
|
|
80
153
|
#
|
|
81
154
|
# @return [Boolean] true if either stdout or stderr contain printable content
|
|
82
155
|
def usable_output_captured?
|
|
83
|
-
[runner.stdout, runner.stderr].reject { |value| value.
|
|
156
|
+
[runner.stdout, runner.stderr].reject { |value| value.to_s.strip.empty? }.any?
|
|
84
157
|
end
|
|
85
158
|
|
|
86
159
|
# Prints "Success" and the resulting timing details before moving on to the next tool
|
|
@@ -88,7 +161,7 @@ module Reviewer
|
|
|
88
161
|
# @return [void]
|
|
89
162
|
def show_timing_result
|
|
90
163
|
runner.record_timing
|
|
91
|
-
runner.
|
|
164
|
+
runner.formatter.success(runner.timer)
|
|
92
165
|
end
|
|
93
166
|
|
|
94
167
|
# Prints "Failure" and the resulting exit status. Shows the precise command that led to the
|
|
@@ -101,7 +174,7 @@ module Reviewer
|
|
|
101
174
|
runner.output.clear
|
|
102
175
|
|
|
103
176
|
# Show the exit status and failed command
|
|
104
|
-
runner.
|
|
177
|
+
runner.formatter.failure("Exit Status #{runner.exit_status}", command: runner.command)
|
|
105
178
|
|
|
106
179
|
# If it can't be rerun, then don't try
|
|
107
180
|
usable_output_captured? ? show_captured_output : rerun_via_passthrough
|
|
@@ -118,25 +191,27 @@ module Reviewer
|
|
|
118
191
|
# If there's a useful stdout value, display it with a divider to visually separate it.
|
|
119
192
|
#
|
|
120
193
|
# @return [void]
|
|
121
|
-
def show_captured_stdout
|
|
122
|
-
|
|
194
|
+
def show_captured_stdout
|
|
195
|
+
stdout = runner.stdout.to_s
|
|
196
|
+
return if stdout.empty?
|
|
123
197
|
|
|
124
198
|
runner.output.divider
|
|
125
199
|
runner.output.newline
|
|
126
|
-
runner.output.unfiltered(
|
|
200
|
+
runner.output.unfiltered(stdout)
|
|
127
201
|
end
|
|
128
202
|
|
|
129
203
|
# If there's a useful stderr value, display it with a divider to visually separate it.
|
|
130
204
|
#
|
|
131
205
|
# @return [void]
|
|
132
|
-
def show_captured_stderr
|
|
133
|
-
|
|
206
|
+
def show_captured_stderr
|
|
207
|
+
stderr = runner.stderr.to_s
|
|
208
|
+
return if stderr.empty?
|
|
134
209
|
|
|
135
|
-
scrubbed_stderr =
|
|
210
|
+
scrubbed_stderr = Output.scrub(stderr)
|
|
136
211
|
|
|
137
212
|
runner.output.divider
|
|
138
213
|
runner.output.newline
|
|
139
|
-
runner.
|
|
214
|
+
runner.formatter.guidance('Runtime Errors:', scrubbed_stderr)
|
|
140
215
|
end
|
|
141
216
|
|
|
142
217
|
# If for some reason, the command didn't send anything to stdout/stderr, the only option to
|
|
@@ -30,10 +30,7 @@ module Reviewer
|
|
|
30
30
|
# Display the exact command syntax that's being run. This can come in handy if there's an
|
|
31
31
|
# issue and the command can be copied/pasted or if the generated command somehow has some
|
|
32
32
|
# incorrect syntax or options that need to be corrected.
|
|
33
|
-
runner.
|
|
34
|
-
|
|
35
|
-
# Add a divider to visually delineate the results
|
|
36
|
-
runner.output.divider
|
|
33
|
+
runner.formatter.current_command(runner.prepare_command)
|
|
37
34
|
|
|
38
35
|
# Run the command through the shell directly so no output is suppressed
|
|
39
36
|
runner.shell.direct(runner.prepare_command)
|
|
@@ -46,16 +43,10 @@ module Reviewer
|
|
|
46
43
|
# Display the exact command syntax that's being run. This can come in handy if there's an
|
|
47
44
|
# issue and the command can be copied/pasted or if the generated command somehow has some
|
|
48
45
|
# incorrect syntax or options that need to be corrected.
|
|
49
|
-
runner.
|
|
50
|
-
|
|
51
|
-
# Add a divider to visually delineate the results
|
|
52
|
-
runner.output.divider
|
|
46
|
+
runner.formatter.current_command(runner.command)
|
|
53
47
|
|
|
54
48
|
# Run the command through the shell directly so no output is suppressed
|
|
55
49
|
runner.shell.direct(runner.command)
|
|
56
|
-
|
|
57
|
-
# Add a final divider to visually delineate the results
|
|
58
|
-
runner.output.divider
|
|
59
50
|
end
|
|
60
51
|
end
|
|
61
52
|
end
|
data/lib/reviewer/runner.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'runner/failed_files'
|
|
4
|
+
require_relative 'runner/formatter'
|
|
5
|
+
require_relative 'runner/guidance'
|
|
6
|
+
require_relative 'runner/result'
|
|
3
7
|
require_relative 'runner/strategies/captured'
|
|
4
8
|
require_relative 'runner/strategies/passthrough'
|
|
5
9
|
|
|
@@ -8,60 +12,106 @@ module Reviewer
|
|
|
8
12
|
class Runner
|
|
9
13
|
extend Forwardable
|
|
10
14
|
|
|
15
|
+
# @!attribute strategy
|
|
16
|
+
# @return [Class] the strategy class for running the command (Captured or Passthrough)
|
|
11
17
|
attr_accessor :strategy
|
|
12
18
|
|
|
13
|
-
attr_reader :command, :shell
|
|
19
|
+
attr_reader :command, :shell
|
|
14
20
|
|
|
15
21
|
def_delegators :@command, :tool
|
|
16
22
|
def_delegators :@shell, :result, :timer
|
|
17
23
|
def_delegators :result, :exit_status, :stdout, :stderr, :rerunnable?
|
|
18
24
|
|
|
19
|
-
# Creates a wrapper for running
|
|
25
|
+
# Creates a wrapper for running commands through Reviewer in order to provide a more accessible
|
|
20
26
|
# API for recording execution time and interpreting the results of a command in a more
|
|
21
|
-
# generous way so that non-zero exit statuses can still
|
|
27
|
+
# generous way so that non-zero exit statuses can still potentially be passing.
|
|
22
28
|
# @param tool [Symbol] the key for the desired tool to run
|
|
23
29
|
# @param command_type [Symbol] the key for the type of command to run
|
|
24
|
-
# @param strategy
|
|
25
|
-
#
|
|
26
|
-
# @param output: Reviewer.output [Review::Output] the output formatter for the results
|
|
30
|
+
# @param strategy [Runner::Strategies] how to execute and handle the results of the command
|
|
31
|
+
# @param context [Context] the shared runtime dependencies (arguments, output, history)
|
|
27
32
|
#
|
|
28
33
|
# @return [self]
|
|
29
|
-
def initialize(tool, command_type, strategy = Strategies::Captured,
|
|
30
|
-
@command = Command.new(tool, command_type)
|
|
34
|
+
def initialize(tool, command_type, strategy = Strategies::Captured, context:)
|
|
35
|
+
@command = Command.new(tool, command_type, context: context)
|
|
31
36
|
@strategy = strategy
|
|
32
|
-
@shell = Shell.new
|
|
33
|
-
@
|
|
37
|
+
@shell = Shell.new(stream: context.output.printer.stream)
|
|
38
|
+
@context = context
|
|
39
|
+
@skipped = false
|
|
40
|
+
@missing = false
|
|
34
41
|
end
|
|
35
42
|
|
|
43
|
+
# The output channel for displaying content, delegated from context.
|
|
44
|
+
#
|
|
45
|
+
# @return [Output]
|
|
46
|
+
def output = @context.output
|
|
47
|
+
|
|
48
|
+
# Display formatter for runner-specific output (tool summary, success, failure, etc.)
|
|
49
|
+
# Computed rather than stored to avoid exceeding instance variable threshold.
|
|
50
|
+
#
|
|
51
|
+
# @return [Runner::Formatter]
|
|
52
|
+
def formatter = @formatter ||= Runner::Formatter.new(output)
|
|
53
|
+
|
|
54
|
+
# Whether this runner is operating in streaming mode
|
|
55
|
+
#
|
|
56
|
+
# @return [Boolean] true if output should be streamed
|
|
57
|
+
def streaming? = @context.arguments.streaming?
|
|
58
|
+
|
|
59
|
+
# Executes the command and returns the exit status
|
|
60
|
+
#
|
|
61
|
+
# @return [Integer] the exit status from the command
|
|
36
62
|
def run
|
|
63
|
+
# Skip if files were requested but none match this tool's pattern
|
|
64
|
+
if command.skip?
|
|
65
|
+
@skipped = true
|
|
66
|
+
show_skipped
|
|
67
|
+
return 0
|
|
68
|
+
end
|
|
69
|
+
|
|
37
70
|
# Show which tool is running
|
|
38
71
|
identify_tool
|
|
39
72
|
|
|
40
73
|
# Use the provided strategy to run the command
|
|
41
74
|
execute_strategy
|
|
42
75
|
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# Return the exit status generated by the tool as interpreted by the Result
|
|
47
|
-
exit_status
|
|
76
|
+
# Handle the result based on whether the tool was found
|
|
77
|
+
result.executable_not_found? ? handle_missing : handle_result
|
|
48
78
|
end
|
|
49
79
|
|
|
80
|
+
# Whether this runner was skipped due to no matching files
|
|
81
|
+
#
|
|
82
|
+
# @return [Boolean] true if the tool was skipped
|
|
83
|
+
def skipped? = @skipped == true
|
|
84
|
+
|
|
85
|
+
# Whether this runner's executable was not found (exit status 127)
|
|
86
|
+
#
|
|
87
|
+
# @return [Boolean] true if the tool was missing
|
|
88
|
+
def missing? = @missing == true
|
|
89
|
+
|
|
90
|
+
# Some review tools return a range of non-zero exit statuses and almost never return 0.
|
|
91
|
+
# (`yarn audit` is a good example.) Those tools can be configured to accept a non-zero exit
|
|
92
|
+
# status so they aren't constantly considered to be failing over minor issues.
|
|
93
|
+
#
|
|
94
|
+
# But when other command types (prepare, install, format) are run, they either succeed or they
|
|
95
|
+
# fail. With no shades of gray in those cases, anything other than a 0 is a failure.
|
|
96
|
+
#
|
|
97
|
+
# Skipped tools are always considered successful.
|
|
50
98
|
def success?
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
99
|
+
return true if skipped?
|
|
100
|
+
|
|
101
|
+
command.type == :review ? exit_status <= tool.max_exit_status : exit_status.zero?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def failure? = !success?
|
|
105
|
+
|
|
106
|
+
# Extracts file paths from stdout/stderr for failed-file tracking
|
|
107
|
+
#
|
|
108
|
+
# @return [Array<String>] file paths found in the command output
|
|
109
|
+
def failed_files
|
|
110
|
+
FailedFiles.new(stdout, stderr).to_a
|
|
62
111
|
end
|
|
63
112
|
|
|
64
|
-
# Prints the tool name and description to the console as a frame of reference
|
|
113
|
+
# Prints the tool name and description to the console as a frame of reference.
|
|
114
|
+
# Only displays in streaming mode; non-streaming strategies handle their own output.
|
|
65
115
|
#
|
|
66
116
|
# @return [void]
|
|
67
117
|
def identify_tool
|
|
@@ -69,7 +119,18 @@ module Reviewer
|
|
|
69
119
|
# be redundant.
|
|
70
120
|
return if result.exists?
|
|
71
121
|
|
|
72
|
-
|
|
122
|
+
stream_output { formatter.tool_summary(tool) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Shows that a tool was skipped due to no matching files.
|
|
126
|
+
# Only displays in streaming mode; non-streaming modes report skips in the final summary.
|
|
127
|
+
#
|
|
128
|
+
# @return [void]
|
|
129
|
+
def show_skipped
|
|
130
|
+
stream_output do
|
|
131
|
+
formatter.tool_summary(tool)
|
|
132
|
+
formatter.skipped
|
|
133
|
+
end
|
|
73
134
|
end
|
|
74
135
|
|
|
75
136
|
# Runs the relevant strategy to either capture or pass through command output.
|
|
@@ -88,25 +149,18 @@ module Reviewer
|
|
|
88
149
|
# be a superfluous run of the preparation.
|
|
89
150
|
#
|
|
90
151
|
# @return [Boolean] true the primary command is not prepare and the tool needs to be prepare
|
|
91
|
-
def run_prepare_step?
|
|
92
|
-
command.type != :prepare && tool.prepare?
|
|
93
|
-
end
|
|
152
|
+
def run_prepare_step? = command.type != :prepare && tool.prepare?
|
|
94
153
|
|
|
95
154
|
# Creates_an instance of the prepare command for a tool
|
|
96
155
|
#
|
|
97
156
|
# @return [Comman] the current tool's prepare command
|
|
98
|
-
def prepare_command
|
|
99
|
-
@prepare_command ||= Command.new(tool, :prepare)
|
|
100
|
-
end
|
|
157
|
+
def prepare_command = @prepare_command ||= Command.new(tool, :prepare, context: @context)
|
|
101
158
|
|
|
102
159
|
# Updates the 'last prepared at' timestamp that Reviewer uses to know if a tool's preparation
|
|
103
160
|
# step is stale and needs to be run again.
|
|
104
161
|
#
|
|
105
162
|
# @return [Time] the timestamp `last_prepared_at` is updated to
|
|
106
|
-
def update_last_prepared_at
|
|
107
|
-
# Touch the `last_prepared_at` timestamp for the tool so it waits before running again.
|
|
108
|
-
tool.last_prepared_at = Time.now
|
|
109
|
-
end
|
|
163
|
+
def update_last_prepared_at = tool.last_prepared_at = Time.now
|
|
110
164
|
|
|
111
165
|
# Saves the last 5 elapsed times for the commands used this run by using the raw command as a
|
|
112
166
|
# unique key. This enables the ability to compare times across runs while taking into
|
|
@@ -124,8 +178,40 @@ module Reviewer
|
|
|
124
178
|
# get back on track in the event of an unsuccessful run.
|
|
125
179
|
#
|
|
126
180
|
# @return [Guidance] the relevant guidance based on the result of the runner
|
|
127
|
-
def guidance
|
|
128
|
-
|
|
181
|
+
def guidance = @guidance ||= Guidance.new(command: command, result: result, context: @context)
|
|
182
|
+
|
|
183
|
+
# Builds an immutable Result object from the current runner state
|
|
184
|
+
#
|
|
185
|
+
# @return [Runner::Result] the result of running this tool
|
|
186
|
+
def to_result
|
|
187
|
+
Result.from_runner(self)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
# Yields the block only when in streaming mode.
|
|
193
|
+
# Centralizes the streaming guard so display methods don't each check independently.
|
|
194
|
+
#
|
|
195
|
+
# @return [void]
|
|
196
|
+
def stream_output
|
|
197
|
+
yield if streaming?
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Marks the tool as missing and shows a skip message
|
|
201
|
+
#
|
|
202
|
+
# @return [Integer] the exit status from the command
|
|
203
|
+
def handle_missing
|
|
204
|
+
@missing = true
|
|
205
|
+
stream_output { formatter.skipped('not installed') }
|
|
206
|
+
exit_status
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Shows failure guidance if needed and returns the exit status
|
|
210
|
+
#
|
|
211
|
+
# @return [Integer] the exit status from the command
|
|
212
|
+
def handle_result
|
|
213
|
+
stream_output { guidance.show } if failure?
|
|
214
|
+
exit_status
|
|
129
215
|
end
|
|
130
216
|
end
|
|
131
217
|
end
|