reviewer 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.flayignore +1 -0
  3. data/.github/workflows/main.yml +11 -3
  4. data/.gitignore +5 -0
  5. data/.reviewer.example.yml +27 -23
  6. data/.reviewer.yml +58 -5
  7. data/.rubocop.yml +2 -0
  8. data/.ruby-version +1 -1
  9. data/CHANGELOG.md +14 -0
  10. data/Gemfile +0 -3
  11. data/Gemfile.lock +54 -29
  12. data/README.md +5 -50
  13. data/exe/fmt +1 -1
  14. data/exe/rvw +1 -1
  15. data/lib/reviewer.rb +39 -26
  16. data/lib/reviewer/arguments.rb +25 -9
  17. data/lib/reviewer/arguments/files.rb +37 -5
  18. data/lib/reviewer/arguments/keywords.rb +23 -9
  19. data/lib/reviewer/arguments/tags.rb +26 -3
  20. data/lib/reviewer/batch.rb +64 -0
  21. data/lib/reviewer/command.rb +100 -0
  22. data/lib/reviewer/command/string.rb +72 -0
  23. data/lib/reviewer/command/string/env.rb +40 -0
  24. data/lib/reviewer/command/string/flags.rb +40 -0
  25. data/lib/reviewer/command/string/verbosity.rb +51 -0
  26. data/lib/reviewer/command/verbosity.rb +65 -0
  27. data/lib/reviewer/configuration.rb +24 -4
  28. data/lib/reviewer/conversions.rb +27 -0
  29. data/lib/reviewer/guidance.rb +73 -0
  30. data/lib/reviewer/history.rb +38 -0
  31. data/lib/reviewer/keywords.rb +9 -0
  32. data/lib/reviewer/keywords/git.rb +14 -0
  33. data/lib/reviewer/keywords/git/staged.rb +48 -0
  34. data/lib/reviewer/loader.rb +2 -3
  35. data/lib/reviewer/output.rb +92 -0
  36. data/lib/reviewer/printer.rb +25 -0
  37. data/lib/reviewer/runner.rb +43 -72
  38. data/lib/reviewer/runner/strategies/quiet.rb +90 -0
  39. data/lib/reviewer/runner/strategies/verbose.rb +63 -0
  40. data/lib/reviewer/shell.rb +58 -0
  41. data/lib/reviewer/shell/result.rb +69 -0
  42. data/lib/reviewer/shell/timer.rb +57 -0
  43. data/lib/reviewer/tool.rb +109 -40
  44. data/lib/reviewer/tool/settings.rb +18 -32
  45. data/lib/reviewer/tools.rb +38 -3
  46. data/lib/reviewer/version.rb +1 -1
  47. data/reviewer.gemspec +10 -2
  48. metadata +143 -16
  49. data/lib/reviewer/arguments/keywords/git.rb +0 -16
  50. data/lib/reviewer/arguments/keywords/git/staged.rb +0 -64
  51. data/lib/reviewer/logger.rb +0 -62
  52. data/lib/reviewer/tool/command.rb +0 -80
  53. data/lib/reviewer/tool/env.rb +0 -38
  54. data/lib/reviewer/tool/flags.rb +0 -38
  55. data/lib/reviewer/tool/verbosity.rb +0 -39
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reviewer
4
+ class Runner
5
+ module Strategies
6
+ # Execution strategy to run a command quietly
7
+ class Quiet
8
+ attr_accessor :runner
9
+
10
+ # Create an instance of the quiet strategy for a command runner so that any output is fully
11
+ # suppressed so as to not create too much noise when running multiple commands.
12
+ # @param runner [Runner] the instance of the runner to apply the strategy to
13
+ #
14
+ # @return [Runner::Strategies::Quiet] an instance of the relevant quiet strategy
15
+ def initialize(runner)
16
+ @runner = runner
17
+ @runner.command.verbosity = Reviewer::Command::Verbosity::TOTAL_SILENCE
18
+ end
19
+
20
+ # The prepare command strategy when running a command quietly
21
+ #
22
+ # @return [void]
23
+ def prepare
24
+ # Running the prepare command, so make sure the timestamp is updated
25
+ runner.update_last_prepared_at
26
+
27
+ # Run the prepare command, suppressing the output and capturing the realtime benchmark
28
+ runner.shell.capture_prep(runner.prepare_command)
29
+ end
30
+
31
+ # The run command strategy when running a command verbosely
32
+ #
33
+ # @return [void]
34
+ def run
35
+ # Run the primary command, suppressing the output and capturing the realtime benchmark
36
+ runner.shell.capture_main(runner.command)
37
+
38
+ # If it's successful, show that it was a success and how long it took to run, otherwise,
39
+ # it wasn't successful and we got some explaining to do...
40
+ runner.success? ? show_timing_result : show_command_output
41
+ end
42
+
43
+ private
44
+
45
+ # Prints "Success" and the resulting timing details before moving on to the next tool
46
+ #
47
+ # @return [void]
48
+ def show_timing_result
49
+ runner.output.success(runner.timer)
50
+ end
51
+
52
+ # Prints "Failure" and the resulting exit status. Shows the precise command that led to the
53
+ # failure for easier copy and paste or making it easier to see any incorrect syntax or
54
+ # options that could be corrected.
55
+ #
56
+ # @return [void]
57
+ def show_command_output
58
+ runner.output.failure("Exit Status #{runner.exit_status}", command: runner.command)
59
+
60
+ # If it can't be rerun, then don't try
61
+ return if runner.result.total_failure?
62
+
63
+ # If it can be rerun, set the strategy to verbose so the output will be visible, and then
64
+ # run it with the verbose strategy so the output isn't suppressed. Long-term, it makes
65
+ # sense to add an option for whether to focus on speed or rich output.
66
+ #
67
+ # For now, the simplest strategy is to re-run the exact same command without suppressing
68
+ # the output. However, running a command twice isn't exactly efficient. Since we've
69
+ # already run it once and captured the output, we could just display that output, but it
70
+ # would be filtered through as a dumb string. That would mean it strips out color and
71
+ # formatting. It would save time, but what's the threshold where the time savings is worth
72
+ # it?
73
+ #
74
+ # Ultimately, this will be a tradeoff between how much the tool's original formatting
75
+ # makes it easier to scan the results and take action. For example, if a command takes
76
+ # 60 seconds to run, it might be faster to show the low-fidelity output immediately rather
77
+ # than waiting another 60 seconds for higher-fidelity output.
78
+ #
79
+ # Most likely, this will be a tool-by-tool decision and need to be added as an option to
80
+ # the tool configuration, but that will create additional complexity/overhead when adding
81
+ # a new tool.
82
+ #
83
+ # So for now, we punt. And pay close attention.
84
+ runner.strategy = Strategies::Verbose
85
+ runner.run
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reviewer
4
+ class Runner
5
+ module Strategies
6
+ # Execution strategy to run a command verbosely
7
+ class Verbose
8
+ attr_accessor :runner
9
+
10
+ # Create an instance of the verbose strategy for a command runner. This strategy ensures
11
+ # that when a command is run, the output isn't suppressed. Essentially, it's a pass-through
12
+ # wrapper for running a command and displaying the results.
13
+ # @param runner [Runner] the instance of the runner to apply the strategy to
14
+ #
15
+ # @return [Runner::Strategies::Verbose] an instance of the relevant verbose strategy
16
+ def initialize(runner)
17
+ @runner = runner
18
+ @runner.command.verbosity = Reviewer::Command::Verbosity::NO_SILENCE
19
+ end
20
+
21
+ # The prepare command strategy when running a command verbosely
22
+ #
23
+ # @return [void]
24
+ def prepare
25
+ # Running the prepare command, so make sure the timestamp is updated
26
+ runner.update_last_prepared_at
27
+
28
+ # Display the exact command syntax that's being run. This can come in handy if there's an
29
+ # issue and the command can be copied/pasted or if the generated command somehow has some
30
+ # incorrect syntax or options that need to be corrected.
31
+ runner.output.current_command(runner.prepare_command)
32
+
33
+ # Add a divider to visually delineate the results
34
+ runner.output.divider
35
+
36
+ # Run the command through the shell directly so no output is suppressed
37
+ runner.shell.direct(runner.prepare_command)
38
+ end
39
+
40
+ # The run command strategy when running a command verbosely
41
+ #
42
+ # @return [void]
43
+ def run
44
+ # Display the exact command that's being run
45
+
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
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ require_relative 'shell/result'
6
+ require_relative 'shell/timer'
7
+
8
+ module Reviewer
9
+ # Handles running, timing, and capturing results for a command
10
+ class Shell
11
+ extend Forwardable
12
+
13
+ attr_reader :timer, :result
14
+
15
+ def_delegators :@result, :exit_status
16
+
17
+ # Initializes a Reviewer shell for running and benchmarking commands, and capturing output
18
+ #
19
+ # @return [Shell] a shell instance for running and benchmarking commands
20
+ def initialize
21
+ @timer = Timer.new
22
+ @result = Result.new
23
+ end
24
+
25
+ # Run a command without capturing the output. This ensures the results are displayed the same as
26
+ # if the command was run directly in the shell. So it keeps any color or other formatting that
27
+ # would be stripped out by capturing $stdout as a basic string.
28
+ # @param command [String] the command to run
29
+ #
30
+ # @return [Integer] exit status vaue of 0 when successful or 1 when unsuccessful
31
+ def direct(command)
32
+ result.exit_status = print_results(command) ? 0 : 1
33
+ end
34
+
35
+ def capture_prep(command)
36
+ timer.record_prep { capture_results(command) }
37
+ end
38
+
39
+ def capture_main(command)
40
+ timer.record_main { capture_results(command) }
41
+ end
42
+
43
+ private
44
+
45
+ def capture_results(command)
46
+ command = String(command)
47
+
48
+ captured_results = Open3.capture3(command)
49
+ @result = Result.new(*captured_results)
50
+ end
51
+
52
+ def print_results(command)
53
+ command = String(command)
54
+
55
+ system(command)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reviewer
4
+ class Shell
5
+ # Provides a structure interface for the results of running a command
6
+ class Result
7
+ EXIT_STATUS_CODES = {
8
+ success: 0,
9
+ cannot_execute: 126,
10
+ executable_not_found: 127,
11
+ terminated: 130
12
+ }.freeze
13
+
14
+ # Not all command line tools use the 127 exit status when an executable cannot be found, so
15
+ # this provides a home for recognizeable strings in those tools' error messages that we can
16
+ # translate to the appropriate exit status for internal consistency
17
+ STD_ERROR_STRINGS = {
18
+ executable_not_found: "can't find executable"
19
+ }.freeze
20
+
21
+ attr_accessor :stdout, :stderr, :exit_status
22
+
23
+ # An instance of a result from running a local command
24
+ # @param stdout = nil [String] standard out output from a command
25
+ # @param stderr = nil [String] standard error output from a command
26
+ # @param status = nil [ProcessStatus] an instance of ProcessStatus for a command
27
+ #
28
+ # @return [Shell::Result] result from running a command-line command
29
+ def initialize(stdout = nil, stderr = nil, status = nil)
30
+ @stdout = stdout
31
+ @stderr = stderr
32
+ @exit_status = status&.exitstatus
33
+ end
34
+
35
+ # Determines whether re-running a command is entirely futile. Primarily to help when a command
36
+ # fails within a batch and needs to be re-run to show the output
37
+ #
38
+ # @return [Boolean] true if the exit status code is greater than or equal to 126
39
+ def total_failure?
40
+ exit_status >= EXIT_STATUS_CODES[:cannot_execute]
41
+ end
42
+
43
+ # Determines whether a command simply cannot be executed.
44
+ #
45
+ # @return [Boolean] true if the exit sttaus code equals 126
46
+ def cannot_execute?
47
+ exit_status == EXIT_STATUS_CODES[:cannot_execute]
48
+ end
49
+
50
+ # Determines whether the command failed because the executable cannot be found. Since this is
51
+ # an error that can be corrected fairly predictably and easily, it provides the ability to
52
+ # tailor the error guidance to help folks recover
53
+ #
54
+ # @return [Boolean] true if the exit sttaus code is 127 or there's a recognizable equivalent
55
+ # value in the standard error string
56
+ def executable_not_found?
57
+ exit_status == EXIT_STATUS_CODES[:executable_not_found] ||
58
+ stderr&.include?(STD_ERROR_STRINGS[:executable_not_found])
59
+ end
60
+
61
+ # Returns a string representation of the result
62
+ #
63
+ # @return [String] stdout if present, otherwise stderr
64
+ def to_s
65
+ stderr.strip.empty? ? stdout : stderr
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module Reviewer
6
+ class Shell
7
+ # Provides a structured interface for measuring realtime main while running comamnds
8
+ class Timer
9
+ attr_accessor :prep, :main
10
+
11
+ def initialize(prep: nil, main: nil)
12
+ @prep = prep
13
+ @main = main
14
+ end
15
+
16
+ def record_prep(&block)
17
+ @prep = record(&block)
18
+ end
19
+
20
+ def record_main(&block)
21
+ @main = record(&block)
22
+ end
23
+
24
+ def prep_seconds
25
+ prep.round(2)
26
+ end
27
+
28
+ def main_seconds
29
+ main.round(2)
30
+ end
31
+
32
+ def total_seconds
33
+ total.round(2)
34
+ end
35
+
36
+ def prep_percent
37
+ return nil unless prepped?
38
+
39
+ (prep / total.to_f * 100).round
40
+ end
41
+
42
+ def total
43
+ [prep, main].compact.sum
44
+ end
45
+
46
+ def prepped?
47
+ !(prep.nil? || main.nil?)
48
+ end
49
+
50
+ private
51
+
52
+ def record(&block)
53
+ Benchmark.realtime(&block)
54
+ end
55
+ end
56
+ end
57
+ end
data/lib/reviewer/tool.rb CHANGED
@@ -1,67 +1,136 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'tool/command'
4
- require_relative 'tool/env'
5
- require_relative 'tool/flags'
6
3
  require_relative 'tool/settings'
7
- require_relative 'tool/verbosity'
8
4
 
9
5
  module Reviewer
10
- # Provides an instance of a specific tool
6
+ # Provides an instance of a specific tool for accessing its settings and run history
11
7
  class Tool
12
- attr_reader :settings
13
-
14
- delegate :name,
15
- :description,
16
- :tags,
17
- :key,
18
- :enabled?,
19
- :disabled?,
20
- :max_exit_status,
21
- :prepare_command?,
22
- :install_command?,
23
- :format_command?,
24
- :install_link?,
25
- to: :settings
26
-
27
- def initialize(tool)
28
- @settings = Settings.new(tool)
8
+ extend Forwardable
9
+ include Comparable
10
+
11
+ # In general, Reviewer tries to save time where it can. In the case of the "prepare" command
12
+ # used by some tools to retrieve data, it only runs it occasionally in order to save time.
13
+ # This is the default window that it uses to determine if the tool's preparation step should be
14
+ # considered stale and needs to be rerun. Frequent enough that it shouldn't get stale, but
15
+ # infrequent enough that it's not cumbersome.
16
+ SIX_HOURS_IN_SECONDS = 60 * 60 * 6
17
+
18
+ attr_reader :settings, :history
19
+
20
+ def_delegators :@settings,
21
+ :key,
22
+ :name,
23
+ :hash,
24
+ :description,
25
+ :tags,
26
+ :commands,
27
+ :links,
28
+ :enabled?,
29
+ :disabled?,
30
+ :max_exit_status
31
+
32
+ alias to_sym key
33
+ alias to_s name
34
+
35
+ # Create an instance of a tool
36
+ # @param tool_key [Symbol] the key to the tool from the configuration file
37
+ #
38
+ # @return [Tool] an instance of tool for accessing settings information and facts about the tool
39
+ def initialize(tool_key)
40
+ @settings = Settings.new(tool_key)
29
41
  end
30
42
 
31
- def to_s
32
- name
43
+ # For determining if the tool should run it's prepration command. It will only be run both if
44
+ # the tool has a preparation command, and the command hasn't been run 6 hours
45
+ #
46
+ # @return [Boolean] true if the tool has a configured `prepare` command that hasn't been run in
47
+ # the last 6 hours
48
+ def prepare?
49
+ preparable? && stale?
33
50
  end
34
51
 
35
- def to_sym
36
- key
52
+ # Determines whether a tool has a specific command type configured
53
+ # @param command_type [Symbol] one of the available command types defined in Command::TYPES
54
+ #
55
+ # @return [Boolean] true if the command type is configured and not blank
56
+ def command?(command_type)
57
+ commands.key?(command_type) && !commands[command_type].nil?
37
58
  end
38
59
 
39
- def ==(other)
40
- settings == other.settings
60
+ # Determines if the tool can run a `install` command
61
+ #
62
+ # @return [Boolean] true if there is a non-blank `install` command configured
63
+ def installable?
64
+ command?(:install)
65
+ end
66
+
67
+ # Determines if the tool can run a `prepare` command
68
+ #
69
+ # @return [Boolean] true if there is a non-blank `prepare` command configured
70
+ def preparable?
71
+ command?(:prepare)
41
72
  end
42
73
 
43
- def installation_command(verbosity_level = :no_silence)
44
- command_string(:install, verbosity_level: verbosity_level)
74
+ # Determines if the tool can run a `review` command
75
+ #
76
+ # @return [Boolean] true if there is a non-blank `review` command configured
77
+ def reviewable?
78
+ command?(:review)
45
79
  end
46
80
 
47
- def preparation_command(verbosity_level = :total_silence)
48
- command_string(:prepare, verbosity_level: verbosity_level)
81
+ # Determines if the tool can run a `format` command
82
+ #
83
+ # @return [Boolean] true if there is a non-blank `format` command configured
84
+ def formattable?
85
+ command?(:format)
49
86
  end
50
87
 
51
- def review_command(verbosity_level = :total_silence, seed: nil)
52
- command_string(:review, verbosity_level: verbosity_level).gsub('$SEED', seed.to_s)
88
+ # Specifies when the tool last had it's `prepare` command run
89
+ #
90
+ # @return [DateTime] timestamp of when the `prepare` command was last run
91
+ def last_prepared_at
92
+ Reviewer.history.get(key, :last_prepared_at)
53
93
  end
54
94
 
55
- def format_command(verbosity_level = :no_silence)
56
- command_string(:format, verbosity_level: verbosity_level)
95
+ # Sets the timestamp for when the tool last ran its `prepare` command
96
+ # @param last_prepared_at [DateTime] the value to record for when the `prepare` command last ran
97
+ #
98
+ # @return [DateTime] timestamp of when the `prepare` command was last run
99
+ def last_prepared_at=(last_prepared_at)
100
+ Reviewer.history.set(key, :last_prepared_at, last_prepared_at)
57
101
  end
58
102
 
59
- private
103
+ # Determines whether the `prepare` command was run recently enough
104
+ #
105
+ # @return [Boolean] true if a prepare command exists, a timestamp exists, and it was run more
106
+ # than six hours ago
107
+ def stale?
108
+ return false unless preparable?
109
+
110
+ last_prepared_at.nil? || last_prepared_at < Time.now - SIX_HOURS_IN_SECONDS
111
+ end
60
112
 
61
- def command_string(command_type, verbosity_level: :no_silence)
62
- cmd = Command.new(command_type, tool_settings: settings, verbosity_level: verbosity_level)
113
+ # Convenience method for determining if a tool has a configured install link
114
+ #
115
+ # @return [Boolean] true if there is an `install` key under links and the value isn't blank
116
+ def install_link?
117
+ links.key?(:install) && !links[:install].nil?
118
+ end
63
119
 
64
- cmd.to_s
120
+ # Returns the text for the install link if available
121
+ #
122
+ # @return [String, nil] the link if it exists, nil otherwise
123
+ def install_link
124
+ install_link? ? links.fetch(:install) : nil
125
+ end
126
+
127
+ # Determines if two tools are equal
128
+ # @param other [Tool] the tool to compare to the current instance
129
+ #
130
+ # @return [Boolean] true if the settings match
131
+ def eql?(other)
132
+ settings == other.settings
65
133
  end
134
+ alias :== eql?
66
135
  end
67
136
  end