reviewer 0.1.1 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.alexignore +1 -0
- data/.flayignore +1 -0
- data/.github/workflows/main.yml +14 -4
- data/.gitignore +5 -0
- data/.inch.yml +4 -0
- data/.reviewer.example.yml +63 -0
- data/.reviewer.future.yml +221 -0
- data/.reviewer.yml +140 -0
- data/.reviewer_stdout +0 -0
- data/.rubocop.yml +20 -0
- data/CHANGELOG.md +38 -3
- data/Gemfile +2 -4
- data/Gemfile.lock +103 -20
- data/LICENSE.txt +4 -20
- data/README.md +23 -29
- data/Rakefile +5 -5
- data/bin/console +4 -4
- data/exe/fmt +7 -0
- data/exe/rvw +7 -0
- data/lib/reviewer/arguments/files.rb +94 -0
- data/lib/reviewer/arguments/keywords.rb +133 -0
- data/lib/reviewer/arguments/tags.rb +71 -0
- data/lib/reviewer/arguments.rb +54 -10
- data/lib/reviewer/batch.rb +91 -0
- data/lib/reviewer/command/string/env.rb +44 -0
- data/lib/reviewer/command/string/flags.rb +51 -0
- data/lib/reviewer/command/string.rb +66 -0
- data/lib/reviewer/command.rb +75 -0
- data/lib/reviewer/configuration.rb +32 -7
- data/lib/reviewer/conversions.rb +16 -0
- data/lib/reviewer/guidance.rb +77 -0
- data/lib/reviewer/history.rb +69 -0
- data/lib/reviewer/keywords/git/staged.rb +64 -0
- data/lib/reviewer/keywords/git.rb +14 -0
- data/lib/reviewer/keywords.rb +9 -0
- data/lib/reviewer/loader.rb +36 -9
- data/lib/reviewer/output/printer.rb +44 -0
- data/lib/reviewer/output/scrubber.rb +48 -0
- data/lib/reviewer/output/token.rb +85 -0
- data/lib/reviewer/output.rb +122 -0
- data/lib/reviewer/runner/strategies/captured.rb +157 -0
- data/lib/reviewer/runner/strategies/passthrough.rb +63 -0
- data/lib/reviewer/runner.rb +131 -0
- data/lib/reviewer/shell/result.rb +84 -0
- data/lib/reviewer/shell/timer.rb +72 -0
- data/lib/reviewer/shell.rb +54 -0
- data/lib/reviewer/tool/settings.rb +38 -19
- data/lib/reviewer/tool.rb +137 -23
- data/lib/reviewer/tools.rb +87 -8
- data/lib/reviewer/version.rb +1 -1
- data/lib/reviewer.rb +107 -28
- data/reviewer.gemspec +34 -19
- data/structure.svg +1 -0
- metadata +244 -12
- data/bin/review +0 -5
- data/lib/reviewer/tool/command.rb +0 -78
- data/lib/reviewer/tool/env.rb +0 -39
- data/lib/reviewer/tool/flags.rb +0 -39
- data/lib/reviewer/tool/verbosity.rb +0 -39
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reviewer
|
4
|
+
module Keywords
|
5
|
+
module Git
|
6
|
+
# Provides a convenient interface to get the list of staged files via Git
|
7
|
+
class Staged
|
8
|
+
OPTIONS = [
|
9
|
+
'diff',
|
10
|
+
'--staged',
|
11
|
+
'--name-only'
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
attr_reader :stdout, :stderr, :status, :exit_status
|
15
|
+
|
16
|
+
def to_a
|
17
|
+
stdout.strip.empty? ? [] : stdout.split("\n")
|
18
|
+
end
|
19
|
+
|
20
|
+
# Gets the list of staged files
|
21
|
+
#
|
22
|
+
# @example Get the list of files
|
23
|
+
# staged.list #=> ['/Code/example.rb', '/Code/run.rb']
|
24
|
+
#
|
25
|
+
# @return [Array<String>] the array of staged filenames as strings
|
26
|
+
def list
|
27
|
+
@stdout, @stderr, @status = Open3.capture3(command)
|
28
|
+
@exit_status = @status.exitstatus.to_i
|
29
|
+
|
30
|
+
@status.success? ? to_a : raise_command_line_error
|
31
|
+
end
|
32
|
+
|
33
|
+
# Convenience method for retrieving the list of staged files since there's no parameters
|
34
|
+
# for an initializer.
|
35
|
+
#
|
36
|
+
# @example Get the list of files
|
37
|
+
# Reviewer::Keywords::Git::Staged.list #=> ['/Code/example.rb', '/Code/run.rb']
|
38
|
+
#
|
39
|
+
# @return [Array<String>] the array of staged filenames as strings
|
40
|
+
def self.list
|
41
|
+
new.list
|
42
|
+
end
|
43
|
+
|
44
|
+
# Assembles the pieces of the command that gets the list of staged files
|
45
|
+
#
|
46
|
+
# @return [String] the full command to run to retrieve the list of staged files
|
47
|
+
def command
|
48
|
+
command_parts.join(' ')
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def raise_command_line_error
|
54
|
+
message = "Git Error: #{stderr} (#{command})"
|
55
|
+
raise SystemCallError.new(message, exit_status)
|
56
|
+
end
|
57
|
+
|
58
|
+
def command_parts
|
59
|
+
BASE_COMMAND + OPTIONS
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/reviewer/loader.rb
CHANGED
@@ -1,32 +1,59 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require "active_support/core_ext/hash/indifferent_access"
|
3
|
+
require 'yaml'
|
5
4
|
|
6
|
-
# Provides a collection of the configured tools
|
7
5
|
module Reviewer
|
6
|
+
# Provides a collection of the configured tools
|
8
7
|
class Loader
|
9
8
|
class MissingConfigurationError < StandardError; end
|
9
|
+
|
10
10
|
class InvalidConfigurationError < StandardError; end
|
11
11
|
|
12
|
-
|
12
|
+
class MissingReviewCommandError < StandardError; end
|
13
|
+
|
14
|
+
attr_reader :configuration, :file
|
15
|
+
|
16
|
+
def initialize(file = Reviewer.configuration.file)
|
17
|
+
@file = file
|
18
|
+
@configuration = configuration_hash
|
13
19
|
|
14
|
-
|
15
|
-
@configuration = HashWithIndifferentAccess.new(configuration_hash)
|
20
|
+
validate_configuration!
|
16
21
|
end
|
17
22
|
|
18
23
|
def to_h
|
19
24
|
configuration
|
20
25
|
end
|
21
26
|
|
27
|
+
def self.configuration
|
28
|
+
new.configuration
|
29
|
+
end
|
30
|
+
|
22
31
|
private
|
23
32
|
|
33
|
+
def validate_configuration!
|
34
|
+
# Any additional guidance for configuration issues will live here
|
35
|
+
require_review_commands!
|
36
|
+
end
|
37
|
+
|
38
|
+
def require_review_commands!
|
39
|
+
configuration.each do |key, value|
|
40
|
+
commands = value[:commands]
|
41
|
+
|
42
|
+
next if commands.key?(:review)
|
43
|
+
|
44
|
+
# Ideally, folks would want to fill out everything to receive the most benefit,
|
45
|
+
# but realistically, the 'review' command is the only required value. If the key
|
46
|
+
# is missing, or maybe there was a typo, fail right away.
|
47
|
+
raise MissingReviewCommandError, "'#{key}' does not have a 'review' key under 'commands' in `#{file}`"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
24
51
|
def configuration_hash
|
25
|
-
@configuration_hash ||=
|
52
|
+
@configuration_hash ||= Psych.safe_load_file(@file, symbolize_names: true)
|
26
53
|
rescue Errno::ENOENT
|
27
|
-
raise MissingConfigurationError, "Tools configuration file couldn't be found
|
54
|
+
raise MissingConfigurationError, "Tools configuration file couldn't be found at `#{file}`"
|
28
55
|
rescue Psych::SyntaxError => e
|
29
|
-
raise InvalidConfigurationError, "Tools configuration file has a syntax error
|
56
|
+
raise InvalidConfigurationError, "Tools configuration file (#{file}) has a syntax error: #{e.message}"
|
30
57
|
end
|
31
58
|
end
|
32
59
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'io/console' # For determining console width/height
|
4
|
+
|
5
|
+
module Reviewer
|
6
|
+
class Output
|
7
|
+
# Wrapper to encapsulate some lower-level details of printing to $stdout
|
8
|
+
class Printer
|
9
|
+
attr_reader :stream
|
10
|
+
|
11
|
+
# Creates an instance of Output to print Reviewer activity and results to the console
|
12
|
+
def initialize(stream = $stdout)
|
13
|
+
@stream = stream.tap do |str|
|
14
|
+
# If the IO channel supports flushing the output immediately, then ensure it's enabled
|
15
|
+
str.sync = str.respond_to?(:sync=)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def print(style, content)
|
20
|
+
text(style, content)
|
21
|
+
end
|
22
|
+
|
23
|
+
def puts(style, content)
|
24
|
+
text(style, content)
|
25
|
+
stream.puts
|
26
|
+
end
|
27
|
+
|
28
|
+
def tty?
|
29
|
+
stream.tty?
|
30
|
+
end
|
31
|
+
alias style_enabled? tty?
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def text(style, content)
|
36
|
+
if style_enabled?
|
37
|
+
stream.print Token.new(style, content).to_s
|
38
|
+
else
|
39
|
+
stream.print content
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reviewer
|
4
|
+
class Output
|
5
|
+
# Provides a structure interface for the results of running a command
|
6
|
+
class Scrubber
|
7
|
+
# A lot of tools are run via rake which inclues some unhelpful drive when there's a non-zero
|
8
|
+
# exit status. This is what it starts with so Reviewer can recognize and remove it.
|
9
|
+
RAKE_ABORTED_TEXT = <<~DRIVEL
|
10
|
+
rake aborted!
|
11
|
+
DRIVEL
|
12
|
+
|
13
|
+
attr_accessor :raw
|
14
|
+
|
15
|
+
# Creates a scrubber instance for the provided text content
|
16
|
+
# @param raw [String] the text to be scrubbed of unhelpful content
|
17
|
+
#
|
18
|
+
# @return [self]
|
19
|
+
def initialize(raw)
|
20
|
+
@raw = raw || ''
|
21
|
+
end
|
22
|
+
|
23
|
+
def clean
|
24
|
+
rake_aborted_text? ? preceding_text : raw
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def rake_aborted_text?
|
30
|
+
raw.include?(RAKE_ABORTED_TEXT)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Removes any unhelpful rake exit status details from $stderr. Reviewew uses `exit` when a
|
34
|
+
# command fails so that the resulting command-line exit status can be interpreted correctly
|
35
|
+
# in CI and similar environments. Without that exit status, those environments wouldn't
|
36
|
+
# recognize the failure. As a result, Rake almost always adds noise that begins with the value
|
37
|
+
# in RAKE_EXIT_DRIVEL when `exit` is called. Frequently, that RAKE_EXIT_DRIVEL is the only
|
38
|
+
# information in $stderr, and it's not helpful in the human-readable output, but other times
|
39
|
+
# when a valid exception occurs, there's useful error information preceding RAKE_EXIT_DRIVEL.
|
40
|
+
# So this ensures that the unhelpful part is always removed so the output is cluttered with
|
41
|
+
# red herrings since the command is designed to fail with an exit status of 1 under normal
|
42
|
+
# operation with tool failures.
|
43
|
+
def preceding_text
|
44
|
+
raw.split(RAKE_ABORTED_TEXT).first
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reviewer
|
4
|
+
class Output
|
5
|
+
# Simple class for streamlining the output of 'tokens' representing a style and content
|
6
|
+
#
|
7
|
+
# @author [garrettdimon]
|
8
|
+
#
|
9
|
+
class Token
|
10
|
+
ESC = "\e["
|
11
|
+
|
12
|
+
attr_accessor :style, :content
|
13
|
+
|
14
|
+
def initialize(style, content)
|
15
|
+
@style = style
|
16
|
+
@content = content
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
[
|
21
|
+
style_string,
|
22
|
+
content,
|
23
|
+
reset_string
|
24
|
+
].join
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def style_string
|
30
|
+
"#{ESC}#{weight};#{color}m"
|
31
|
+
end
|
32
|
+
|
33
|
+
def reset_string
|
34
|
+
"#{ESC}0m"
|
35
|
+
end
|
36
|
+
|
37
|
+
def weight_key
|
38
|
+
style_components[0]
|
39
|
+
end
|
40
|
+
|
41
|
+
def color_key
|
42
|
+
style_components[1]
|
43
|
+
end
|
44
|
+
|
45
|
+
def weight
|
46
|
+
{
|
47
|
+
default: 0,
|
48
|
+
bold: 1,
|
49
|
+
light: 2,
|
50
|
+
italic: 3
|
51
|
+
}.fetch(weight_key)
|
52
|
+
end
|
53
|
+
|
54
|
+
def color
|
55
|
+
{
|
56
|
+
black: 30,
|
57
|
+
red: 31,
|
58
|
+
green: 32,
|
59
|
+
yellow: 33,
|
60
|
+
blue: 34,
|
61
|
+
magenta: 35,
|
62
|
+
cyan: 36,
|
63
|
+
gray: 37,
|
64
|
+
default: 39
|
65
|
+
}.fetch(color_key)
|
66
|
+
end
|
67
|
+
|
68
|
+
def style_components
|
69
|
+
{
|
70
|
+
success_bold: %i[bold green],
|
71
|
+
success: %i[default green],
|
72
|
+
success_light: %i[light green],
|
73
|
+
error: %i[bold red],
|
74
|
+
failure: %i[default red],
|
75
|
+
warning: %i[bold yellow],
|
76
|
+
warning_light: %i[light yellow],
|
77
|
+
source: %i[italic default],
|
78
|
+
bold: %i[default default],
|
79
|
+
default: %i[default default],
|
80
|
+
muted: %i[light gray]
|
81
|
+
}.fetch(style)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'io/console/size' # For determining console width/height
|
4
|
+
|
5
|
+
require_relative 'output/printer'
|
6
|
+
require_relative 'output/scrubber'
|
7
|
+
require_relative 'output/token'
|
8
|
+
|
9
|
+
module Reviewer
|
10
|
+
# Friendly API for printing nicely-formatted output to the console
|
11
|
+
class Output
|
12
|
+
DEFAULT_CONSOLE_WIDTH = 120
|
13
|
+
DIVIDER = '·'
|
14
|
+
|
15
|
+
attr_reader :printer
|
16
|
+
|
17
|
+
# Creates an instance of Output to print Reviewer activity and results to the console
|
18
|
+
def initialize(printer = Printer.new)
|
19
|
+
@printer = printer
|
20
|
+
end
|
21
|
+
|
22
|
+
def clear
|
23
|
+
system('clear')
|
24
|
+
end
|
25
|
+
|
26
|
+
def newline
|
27
|
+
printer.puts(:default, '')
|
28
|
+
end
|
29
|
+
|
30
|
+
def divider
|
31
|
+
newline
|
32
|
+
printer.print(:muted, DIVIDER * console_width)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Prints plain text to the console
|
36
|
+
# @param message [String] the text to write to the console
|
37
|
+
#
|
38
|
+
# @return [void]
|
39
|
+
def help(message)
|
40
|
+
printer.puts(:default, message)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Prints a summary of the total time and results for a batch run. If multiple tools, it will
|
44
|
+
# show the total tool count
|
45
|
+
# @param tool_count [Integer] the number of commands run in the batch
|
46
|
+
# @param seconds [Float] the total number of seconds the batch ran in realtime
|
47
|
+
#
|
48
|
+
# @return [void]
|
49
|
+
def batch_summary(tool_count, seconds)
|
50
|
+
printer.print(:bold, "~#{seconds.round(1)} seconds")
|
51
|
+
printer.puts(:muted, " for #{tool_count} tools") if tool_count > 1
|
52
|
+
end
|
53
|
+
|
54
|
+
# Print a tool summary using the name and description. Used before running a command to help
|
55
|
+
# identify which tool is running at any given moment.
|
56
|
+
# @param tool [Tool] the tool to identify and describe
|
57
|
+
#
|
58
|
+
# @return [void]
|
59
|
+
def tool_summary(tool)
|
60
|
+
printer.print(:bold, tool.name)
|
61
|
+
printer.puts(:muted, " #{tool.description}")
|
62
|
+
end
|
63
|
+
|
64
|
+
# Prints the text of a command to the console to help proactively expose potentials issues with
|
65
|
+
# syntax if Reviewer translated thte provided options in an unexpected way
|
66
|
+
# @param command [String, Command] the command to identify on the console
|
67
|
+
#
|
68
|
+
# @return [void] [description]
|
69
|
+
def current_command(command)
|
70
|
+
printer.puts(:bold, 'Now Running:')
|
71
|
+
printer.puts(:default, String(command))
|
72
|
+
end
|
73
|
+
|
74
|
+
def success(timer)
|
75
|
+
printer.print(:success, 'Success')
|
76
|
+
printer.print(:success_light, " #{timer.total_seconds}s")
|
77
|
+
printer.print(:warning_light, " (#{timer.prep_percent}% prep ~#{timer.prep_seconds}s)") if timer.prepped?
|
78
|
+
newline
|
79
|
+
newline
|
80
|
+
end
|
81
|
+
|
82
|
+
def failure(details, command: nil)
|
83
|
+
printer.print(:failure, 'Failure')
|
84
|
+
printer.puts(:muted, " #{details}")
|
85
|
+
|
86
|
+
return if command.nil?
|
87
|
+
|
88
|
+
newline
|
89
|
+
printer.puts(:bold, 'Failed Command:')
|
90
|
+
printer.puts(:muted, String(command))
|
91
|
+
end
|
92
|
+
|
93
|
+
def unrecoverable(details)
|
94
|
+
printer.puts(:error, 'Unrecoverable Error:')
|
95
|
+
printer.puts(:muted, details)
|
96
|
+
end
|
97
|
+
|
98
|
+
def guidance(summary, details)
|
99
|
+
return if details.nil?
|
100
|
+
|
101
|
+
newline
|
102
|
+
printer.puts(:bold, summary)
|
103
|
+
printer.puts(:muted, details)
|
104
|
+
end
|
105
|
+
|
106
|
+
def unfiltered(value)
|
107
|
+
return if value.nil? || value.strip.empty?
|
108
|
+
|
109
|
+
printer.stream << value
|
110
|
+
end
|
111
|
+
|
112
|
+
protected
|
113
|
+
|
114
|
+
def console_width
|
115
|
+
return DEFAULT_CONSOLE_WIDTH if IO.console.nil?
|
116
|
+
|
117
|
+
_height, width = IO.console.winsize
|
118
|
+
|
119
|
+
width
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reviewer
|
4
|
+
class Runner
|
5
|
+
module Strategies
|
6
|
+
# Execution strategy for a runner to run a command quietly by capturing the output and only
|
7
|
+
# displaying it if there's a failure that justifies it
|
8
|
+
# @attr_reader runner [Runner] the instance of the runner that will be executed with this strategy
|
9
|
+
# @attr_reader start_time [Time] the start time for the strategy_for timing purposes
|
10
|
+
class Captured
|
11
|
+
attr_reader :runner, :start_time
|
12
|
+
|
13
|
+
# Create an instance of the captured strategy for a command runner so that any output is
|
14
|
+
# fully suppressed so as to not create too much noise when running multiple commands.
|
15
|
+
# @param runner [Runner] the instance of the runner to apply the strategy to
|
16
|
+
#
|
17
|
+
# @return [self]
|
18
|
+
def initialize(runner)
|
19
|
+
@runner = runner
|
20
|
+
@start_time = Time.now
|
21
|
+
end
|
22
|
+
|
23
|
+
# The prepare command strategy when running a command and capturing the results
|
24
|
+
#
|
25
|
+
# @return [void]
|
26
|
+
def prepare
|
27
|
+
command = runner.prepare_command
|
28
|
+
|
29
|
+
display_progress(command) { runner.shell.capture_prep(command) }
|
30
|
+
|
31
|
+
# Running the prepare command, so make sure the timestamp is updated
|
32
|
+
runner.update_last_prepared_at
|
33
|
+
end
|
34
|
+
|
35
|
+
# The run command strategy when running a command and capturing the results
|
36
|
+
#
|
37
|
+
# @return [void]
|
38
|
+
def run
|
39
|
+
command = runner.command
|
40
|
+
|
41
|
+
display_progress(command) { runner.shell.capture_main(command) }
|
42
|
+
|
43
|
+
# If it's successful, show that it was a success and how long it took to run, otherwise,
|
44
|
+
# it wasn't successful and we got some explaining to do...
|
45
|
+
runner.success? ? show_timing_result : show_command_output
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# Displays the progress of the current command since the output is captured/suppressed.
|
51
|
+
# Helps people know that the sub-command is running within expectations.
|
52
|
+
# @param command [String] the precise command string generated by Reviewer. Serves as the
|
53
|
+
# key for looking up the command's history.
|
54
|
+
# @param &block [Block] the runner for the command that's being timed and having its
|
55
|
+
# progress updated and printed
|
56
|
+
#
|
57
|
+
# @return [void]
|
58
|
+
def display_progress(command, &block) # rubocop:disable Metrics/AbcSize
|
59
|
+
start_time = Time.now
|
60
|
+
average_time = runner.tool.average_time(command)
|
61
|
+
|
62
|
+
thread = Thread.new { block.call }
|
63
|
+
|
64
|
+
while thread.alive?
|
65
|
+
elapsed = (Time.now - start_time).to_f.round(1)
|
66
|
+
progress = if average_time.zero?
|
67
|
+
"#{elapsed}s"
|
68
|
+
else
|
69
|
+
"~#{((elapsed / average_time) * 100).round}%"
|
70
|
+
end
|
71
|
+
|
72
|
+
$stdout.print "> #{progress} \r"
|
73
|
+
$stdout.flush
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Determines if stdout or stderr captured any useful output that can be displayed in order
|
78
|
+
# to more rapidly display output when a command fails. As long as both aren't nil or
|
79
|
+
# otherwise 'blank' strings, then that's enough.
|
80
|
+
#
|
81
|
+
# @return [Boolean] true if either stdout or stderr contain printable content
|
82
|
+
def usable_output_captured?
|
83
|
+
[runner.stdout, runner.stderr].reject { |value| value.nil? || value.strip.empty? }.any?
|
84
|
+
end
|
85
|
+
|
86
|
+
# Prints "Success" and the resulting timing details before moving on to the next tool
|
87
|
+
#
|
88
|
+
# @return [void]
|
89
|
+
def show_timing_result
|
90
|
+
runner.record_timing
|
91
|
+
runner.output.success(runner.timer)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Prints "Failure" and the resulting exit status. Shows the precise command that led to the
|
95
|
+
# failure for easier copy and paste or making it easier to see any incorrect syntax or
|
96
|
+
# options that could be corrected.
|
97
|
+
#
|
98
|
+
# @return [void]
|
99
|
+
def show_command_output
|
100
|
+
# If there's a failure, clear the successful command output to focus on the issues
|
101
|
+
runner.output.clear
|
102
|
+
|
103
|
+
# Show the exit status and failed command
|
104
|
+
runner.output.failure("Exit Status #{runner.exit_status}", command: runner.command)
|
105
|
+
|
106
|
+
# If it can't be rerun, then don't try
|
107
|
+
usable_output_captured? ? show_captured_output : rerun_via_passthrough
|
108
|
+
end
|
109
|
+
|
110
|
+
# If the command sent output to stdout/stderr as most will, simply display what was captured
|
111
|
+
#
|
112
|
+
# @return [void]
|
113
|
+
def show_captured_output
|
114
|
+
show_captured_stdout
|
115
|
+
show_captured_stderr
|
116
|
+
end
|
117
|
+
|
118
|
+
# If there's a useful stdout value, display it with a divider to visually separate it.
|
119
|
+
#
|
120
|
+
# @return [void]
|
121
|
+
def show_captured_stdout # rubocop:disable Metrics/AbcSize
|
122
|
+
return if runner.stdout.nil? || runner.stdout.empty?
|
123
|
+
|
124
|
+
runner.output.divider
|
125
|
+
runner.output.newline
|
126
|
+
runner.output.unfiltered(runner.stdout)
|
127
|
+
end
|
128
|
+
|
129
|
+
# If there's a useful stderr value, display it with a divider to visually separate it.
|
130
|
+
#
|
131
|
+
# @return [void]
|
132
|
+
def show_captured_stderr # rubocop:disable Metrics/AbcSize
|
133
|
+
return if runner.stderr.nil? || runner.stderr.empty?
|
134
|
+
|
135
|
+
scrubbed_stderr = Reviewer::Output::Scrubber.new(runner.stderr).clean
|
136
|
+
|
137
|
+
runner.output.divider
|
138
|
+
runner.output.newline
|
139
|
+
runner.output.guidance('Runtime Errors:', scrubbed_stderr)
|
140
|
+
end
|
141
|
+
|
142
|
+
# If for some reason, the command didn't send anything to stdout/stderr, the only option to
|
143
|
+
# show results is to rerun it via the passthrough strategy
|
144
|
+
#
|
145
|
+
# @return [void]
|
146
|
+
def rerun_via_passthrough
|
147
|
+
return unless runner.rerunnable?
|
148
|
+
|
149
|
+
runner.strategy = Strategies::Passthrough
|
150
|
+
|
151
|
+
runner.output.divider
|
152
|
+
runner.run
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Reviewer
|
4
|
+
class Runner
|
5
|
+
module Strategies
|
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.
|
16
|
+
# @param runner [Runner] the instance of the runner to apply the strategy to
|
17
|
+
#
|
18
|
+
# @return [self]
|
19
|
+
def initialize(runner)
|
20
|
+
@runner = runner
|
21
|
+
end
|
22
|
+
|
23
|
+
# The prepare command strategy when running a command transparently
|
24
|
+
#
|
25
|
+
# @return [void]
|
26
|
+
def prepare
|
27
|
+
# Running the prepare command, so make sure the timestamp is updated
|
28
|
+
runner.update_last_prepared_at
|
29
|
+
|
30
|
+
# Display the exact command syntax that's being run. This can come in handy if there's an
|
31
|
+
# issue and the command can be copied/pasted or if the generated command somehow has some
|
32
|
+
# incorrect syntax or options that need to be corrected.
|
33
|
+
runner.output.current_command(runner.prepare_command)
|
34
|
+
|
35
|
+
# Add a divider to visually delineate the results
|
36
|
+
runner.output.divider
|
37
|
+
|
38
|
+
# Run the command through the shell directly so no output is suppressed
|
39
|
+
runner.shell.direct(runner.prepare_command)
|
40
|
+
end
|
41
|
+
|
42
|
+
# The run command strategy when running a command transparently
|
43
|
+
#
|
44
|
+
# @return [void]
|
45
|
+
def run
|
46
|
+
# Display the exact command syntax that's being run. This can come in handy if there's an
|
47
|
+
# issue and the command can be copied/pasted or if the generated command somehow has some
|
48
|
+
# incorrect syntax or options that need to be corrected.
|
49
|
+
runner.output.current_command(runner.command)
|
50
|
+
|
51
|
+
# Add a divider to visually delineate the results
|
52
|
+
runner.output.divider
|
53
|
+
|
54
|
+
# Run the command through the shell directly so no output is suppressed
|
55
|
+
runner.shell.direct(runner.command)
|
56
|
+
|
57
|
+
# Add a final divider to visually delineate the results
|
58
|
+
runner.output.divider
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|