reviewer 0.1.4 → 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/.alexignore +1 -0
- data/.github/FUNDING.yml +3 -0
- data/.github/workflows/main.yml +81 -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 +27 -12
- data/.reviewer.future.yml +221 -0
- data/.reviewer.yml +191 -28
- data/.reviewer_stdout +0 -0
- data/.rubocop.yml +34 -1
- data/CHANGELOG.md +42 -2
- data/Gemfile +39 -1
- data/Gemfile.lock +294 -72
- data/README.md +315 -7
- 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 +60 -27
- data/lib/reviewer/arguments/keywords.rb +39 -43
- data/lib/reviewer/arguments/tags.rb +21 -14
- data/lib/reviewer/arguments.rb +107 -29
- data/lib/reviewer/batch/formatter.rb +87 -0
- data/lib/reviewer/batch.rb +46 -35
- data/lib/reviewer/capabilities.rb +81 -0
- data/lib/reviewer/command/string/env.rb +16 -6
- data/lib/reviewer/command/string/flags.rb +14 -5
- data/lib/reviewer/command/string.rb +53 -24
- data/lib/reviewer/command.rb +69 -39
- data/lib/reviewer/configuration/loader.rb +70 -0
- data/lib/reviewer/configuration.rb +14 -4
- 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 +36 -12
- data/lib/reviewer/output/formatting.rb +40 -0
- data/lib/reviewer/output/printer.rb +105 -0
- data/lib/reviewer/output.rb +54 -65
- 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 +232 -0
- data/lib/reviewer/runner/strategies/{verbose.rb → passthrough.rb} +15 -24
- data/lib/reviewer/runner.rb +179 -35
- 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 +25 -11
- data/lib/reviewer/shell/timer.rb +47 -27
- data/lib/reviewer/shell.rb +46 -21
- data/lib/reviewer/tool/conversions.rb +20 -0
- data/lib/reviewer/tool/file_resolver.rb +54 -0
- data/lib/reviewer/tool/settings.rb +107 -56
- data/lib/reviewer/tool/test_file_mapper.rb +73 -0
- data/lib/reviewer/tool/timing.rb +78 -0
- data/lib/reviewer/tool.rb +88 -47
- data/lib/reviewer/tools.rb +47 -33
- data/lib/reviewer/version.rb +1 -1
- data/lib/reviewer.rb +114 -54
- data/reviewer.gemspec +21 -20
- data/structure.svg +1 -0
- metadata +113 -148
- data/.ruby-version +0 -1
- data/lib/reviewer/command/string/verbosity.rb +0 -51
- data/lib/reviewer/command/verbosity.rb +0 -65
- data/lib/reviewer/conversions.rb +0 -27
- data/lib/reviewer/guidance.rb +0 -73
- data/lib/reviewer/keywords/git/staged.rb +0 -48
- 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/printer.rb +0 -25
- data/lib/reviewer/runner/strategies/quiet.rb +0 -90
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
class Runner
|
|
5
|
+
# Handles the logic around what to display after a command has been run
|
|
6
|
+
class Guidance
|
|
7
|
+
attr_reader :command, :result, :formatter, :context
|
|
8
|
+
private :context
|
|
9
|
+
|
|
10
|
+
# Create an instance of guidance for suggesting recovery steps after errors
|
|
11
|
+
# @param command [Command] the command that was run and needs recovery guidance
|
|
12
|
+
# @param result [Result] the result of the command
|
|
13
|
+
# @param context [Context] the shared runtime dependencies
|
|
14
|
+
#
|
|
15
|
+
# @return [Guidance] the guidance class to suggest relevant recovery steps
|
|
16
|
+
def initialize(command:, result:, context:)
|
|
17
|
+
@command = command
|
|
18
|
+
@result = result
|
|
19
|
+
@context = context
|
|
20
|
+
@formatter = Runner::Formatter.new(context.output)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Prints the relevant guidance based on the command and result context
|
|
24
|
+
#
|
|
25
|
+
# @return [void] prints the relevant guidance to the stream
|
|
26
|
+
def show
|
|
27
|
+
case result
|
|
28
|
+
when executable_not_found? then show_missing_executable_guidance
|
|
29
|
+
when cannot_execute? then show_unrecoverable_guidance
|
|
30
|
+
else show_syntax_guidance
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Conditional check for when the command result was that the executable couldn't be found
|
|
37
|
+
#
|
|
38
|
+
# @return [Boolean] true if the result indicates the command couldn't be found
|
|
39
|
+
def executable_not_found?
|
|
40
|
+
lambda(&:executable_not_found?)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Conditional check for when the command result was that it was unable to be executed
|
|
44
|
+
#
|
|
45
|
+
# @return [Boolean] true if the result indicates the command couldn't be executed
|
|
46
|
+
def cannot_execute?
|
|
47
|
+
lambda(&:cannot_execute?)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Shows the recovery guidance for when a command is missing
|
|
51
|
+
#
|
|
52
|
+
# @return [void] prints missing executable guidance
|
|
53
|
+
def show_missing_executable_guidance
|
|
54
|
+
tool = command.tool
|
|
55
|
+
installation_command = Command.new(tool, :install, context: context).string if tool.installable?
|
|
56
|
+
install_link = tool.install_link
|
|
57
|
+
|
|
58
|
+
formatter.failure("Missing executable for '#{tool}'", command: command)
|
|
59
|
+
formatter.guidance('Try installing the tool:', installation_command)
|
|
60
|
+
formatter.guidance('Read the installation guidance:', install_link)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Shows the recovery guidance for when a command generates an unrecoverable error
|
|
64
|
+
#
|
|
65
|
+
# @return [void] prints unrecoverable error guidance
|
|
66
|
+
def show_unrecoverable_guidance
|
|
67
|
+
formatter.unrecoverable(result.stderr)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Shows suggestions for ignoring or disable rules when a command fails after reviewing code
|
|
71
|
+
#
|
|
72
|
+
# @return [void] prints syntax guidance
|
|
73
|
+
def show_syntax_guidance
|
|
74
|
+
formatter.guidance('Selectively Ignore a Rule:', command.tool.links[:ignore_syntax])
|
|
75
|
+
formatter.guidance('Fully Disable a Rule:', command.tool.links[:disable_syntax])
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -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
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby-progressbar'
|
|
4
|
+
|
|
5
|
+
module Reviewer
|
|
6
|
+
class Runner
|
|
7
|
+
module Strategies
|
|
8
|
+
# Execution strategy for a runner to run a command quietly by capturing the output and only
|
|
9
|
+
# displaying it if there's a failure that justifies it
|
|
10
|
+
# @attr_reader runner [Runner] the instance of the runner that will be executed with this strategy
|
|
11
|
+
# @attr_reader start_time [Time] the start time for the strategy_for timing purposes
|
|
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
|
+
|
|
19
|
+
attr_reader :runner, :start_time
|
|
20
|
+
|
|
21
|
+
# Create an instance of the captured strategy for a command runner so that any output is
|
|
22
|
+
# fully suppressed so as to not create too much noise when running multiple commands.
|
|
23
|
+
# @param runner [Runner] the instance of the runner to apply the strategy to
|
|
24
|
+
#
|
|
25
|
+
# @return [self]
|
|
26
|
+
def initialize(runner)
|
|
27
|
+
@runner = runner
|
|
28
|
+
@start_time = Time.now
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The prepare command strategy when running a command and capturing the results
|
|
32
|
+
#
|
|
33
|
+
# @return [void]
|
|
34
|
+
def prepare
|
|
35
|
+
command = runner.prepare_command
|
|
36
|
+
|
|
37
|
+
display_progress(command) { runner.shell.capture_prep(command) }
|
|
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
|
+
|
|
42
|
+
# Running the prepare command, so make sure the timestamp is updated
|
|
43
|
+
runner.update_last_prepared_at
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The run command strategy when running a command and capturing the results
|
|
47
|
+
#
|
|
48
|
+
# @return [void]
|
|
49
|
+
def run
|
|
50
|
+
command = runner.command
|
|
51
|
+
|
|
52
|
+
display_progress(command) { runner.shell.capture_main(command) }
|
|
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
|
+
|
|
60
|
+
# If it's successful, show that it was a success and how long it took to run, otherwise,
|
|
61
|
+
# it wasn't successful and we got some explaining to do...
|
|
62
|
+
runner.success? ? show_timing_result : show_command_output
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Displays the progress of the current command since the output is captured/suppressed.
|
|
68
|
+
# Helps people know that the sub-command is running within expectations.
|
|
69
|
+
# @param command [String] the precise command string generated by Reviewer. Serves as the
|
|
70
|
+
# key for looking up the command's history.
|
|
71
|
+
# @param block [Block] the runner for the command that's being timed and having its
|
|
72
|
+
# progress updated and printed
|
|
73
|
+
#
|
|
74
|
+
# @return [void]
|
|
75
|
+
def display_progress(command)
|
|
76
|
+
average_time = runner.tool.average_time(command)
|
|
77
|
+
start_time = Time.now
|
|
78
|
+
thread = Thread.new { yield }
|
|
79
|
+
|
|
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)
|
|
88
|
+
|
|
89
|
+
while thread.alive?
|
|
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
|
|
139
|
+
end
|
|
140
|
+
end
|
|
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
|
+
|
|
150
|
+
# Determines if stdout or stderr captured any useful output that can be displayed in order
|
|
151
|
+
# to more rapidly display output when a command fails. As long as both aren't nil or
|
|
152
|
+
# otherwise 'blank' strings, then that's enough.
|
|
153
|
+
#
|
|
154
|
+
# @return [Boolean] true if either stdout or stderr contain printable content
|
|
155
|
+
def usable_output_captured?
|
|
156
|
+
[runner.stdout, runner.stderr].reject { |value| value.to_s.strip.empty? }.any?
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Prints "Success" and the resulting timing details before moving on to the next tool
|
|
160
|
+
#
|
|
161
|
+
# @return [void]
|
|
162
|
+
def show_timing_result
|
|
163
|
+
runner.record_timing
|
|
164
|
+
runner.formatter.success(runner.timer)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Prints "Failure" and the resulting exit status. Shows the precise command that led to the
|
|
168
|
+
# failure for easier copy and paste or making it easier to see any incorrect syntax or
|
|
169
|
+
# options that could be corrected.
|
|
170
|
+
#
|
|
171
|
+
# @return [void]
|
|
172
|
+
def show_command_output
|
|
173
|
+
# If there's a failure, clear the successful command output to focus on the issues
|
|
174
|
+
runner.output.clear
|
|
175
|
+
|
|
176
|
+
# Show the exit status and failed command
|
|
177
|
+
runner.formatter.failure("Exit Status #{runner.exit_status}", command: runner.command)
|
|
178
|
+
|
|
179
|
+
# If it can't be rerun, then don't try
|
|
180
|
+
usable_output_captured? ? show_captured_output : rerun_via_passthrough
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# If the command sent output to stdout/stderr as most will, simply display what was captured
|
|
184
|
+
#
|
|
185
|
+
# @return [void]
|
|
186
|
+
def show_captured_output
|
|
187
|
+
show_captured_stdout
|
|
188
|
+
show_captured_stderr
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# If there's a useful stdout value, display it with a divider to visually separate it.
|
|
192
|
+
#
|
|
193
|
+
# @return [void]
|
|
194
|
+
def show_captured_stdout
|
|
195
|
+
stdout = runner.stdout.to_s
|
|
196
|
+
return if stdout.empty?
|
|
197
|
+
|
|
198
|
+
runner.output.divider
|
|
199
|
+
runner.output.newline
|
|
200
|
+
runner.output.unfiltered(stdout)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# If there's a useful stderr value, display it with a divider to visually separate it.
|
|
204
|
+
#
|
|
205
|
+
# @return [void]
|
|
206
|
+
def show_captured_stderr
|
|
207
|
+
stderr = runner.stderr.to_s
|
|
208
|
+
return if stderr.empty?
|
|
209
|
+
|
|
210
|
+
scrubbed_stderr = Output.scrub(stderr)
|
|
211
|
+
|
|
212
|
+
runner.output.divider
|
|
213
|
+
runner.output.newline
|
|
214
|
+
runner.formatter.guidance('Runtime Errors:', scrubbed_stderr)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# If for some reason, the command didn't send anything to stdout/stderr, the only option to
|
|
218
|
+
# show results is to rerun it via the passthrough strategy
|
|
219
|
+
#
|
|
220
|
+
# @return [void]
|
|
221
|
+
def rerun_via_passthrough
|
|
222
|
+
return unless runner.rerunnable?
|
|
223
|
+
|
|
224
|
+
runner.strategy = Strategies::Passthrough
|
|
225
|
+
|
|
226
|
+
runner.output.divider
|
|
227
|
+
runner.run
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
@@ -3,22 +3,24 @@
|
|
|
3
3
|
module Reviewer
|
|
4
4
|
class Runner
|
|
5
5
|
module Strategies
|
|
6
|
-
# Execution strategy to run a command
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
# Execution strategy for a runner to run a command transparently-displaying the command's
|
|
7
|
+
# output in realtime as it runs (as opposed to capturing and suppressing the output unless
|
|
8
|
+
# the command fails)
|
|
9
|
+
# @attr_reader runner [Runner] the instance of the runner that will be executed with this strategy
|
|
10
|
+
class Passthrough
|
|
11
|
+
attr_reader :runner
|
|
12
|
+
|
|
13
|
+
# Create an instance of the passthrough strategy for a command runner. This strategy ensures
|
|
14
|
+
# that when a command is run, the output isn't suppressed. Essentially, it's a transparent
|
|
15
|
+
# wrapper for running a command and displaying the results realtime.
|
|
13
16
|
# @param runner [Runner] the instance of the runner to apply the strategy to
|
|
14
17
|
#
|
|
15
|
-
# @return [
|
|
18
|
+
# @return [self]
|
|
16
19
|
def initialize(runner)
|
|
17
20
|
@runner = runner
|
|
18
|
-
@runner.command.verbosity = Reviewer::Command::Verbosity::NO_SILENCE
|
|
19
21
|
end
|
|
20
22
|
|
|
21
|
-
# The prepare command strategy when running a command
|
|
23
|
+
# The prepare command strategy when running a command transparently
|
|
22
24
|
#
|
|
23
25
|
# @return [void]
|
|
24
26
|
def prepare
|
|
@@ -28,34 +30,23 @@ module Reviewer
|
|
|
28
30
|
# Display the exact command syntax that's being run. This can come in handy if there's an
|
|
29
31
|
# issue and the command can be copied/pasted or if the generated command somehow has some
|
|
30
32
|
# incorrect syntax or options that need to be corrected.
|
|
31
|
-
runner.
|
|
32
|
-
|
|
33
|
-
# Add a divider to visually delineate the results
|
|
34
|
-
runner.output.divider
|
|
33
|
+
runner.formatter.current_command(runner.prepare_command)
|
|
35
34
|
|
|
36
35
|
# Run the command through the shell directly so no output is suppressed
|
|
37
36
|
runner.shell.direct(runner.prepare_command)
|
|
38
37
|
end
|
|
39
38
|
|
|
40
|
-
# The run command strategy when running a command
|
|
39
|
+
# The run command strategy when running a command transparently
|
|
41
40
|
#
|
|
42
41
|
# @return [void]
|
|
43
42
|
def run
|
|
44
|
-
# Display the exact command that's being run
|
|
45
|
-
|
|
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
|