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,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
module Setup
|
|
5
|
+
# Parses a Gemfile.lock to extract gem names from the specs section
|
|
6
|
+
class GemfileLock
|
|
7
|
+
# Spec lines are indented with 4 spaces: " gem-name (version)"
|
|
8
|
+
SPEC_LINE = /\A {4}(\S+)\s/
|
|
9
|
+
|
|
10
|
+
attr_reader :path
|
|
11
|
+
|
|
12
|
+
# Creates a parser for extracting gem names from a Gemfile.lock
|
|
13
|
+
# @param path [Pathname] the path to the Gemfile.lock file
|
|
14
|
+
#
|
|
15
|
+
# @return [GemfileLock]
|
|
16
|
+
def initialize(path)
|
|
17
|
+
@path = path
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the set of gem names found in the specs section
|
|
21
|
+
#
|
|
22
|
+
# @return [Set<String>] gem names
|
|
23
|
+
def gem_names
|
|
24
|
+
return Set.new unless path.exist?
|
|
25
|
+
|
|
26
|
+
parse_specs
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def parse_specs
|
|
32
|
+
in_specs = false
|
|
33
|
+
gems = Set.new
|
|
34
|
+
|
|
35
|
+
path.each_line do |line|
|
|
36
|
+
in_specs, gem_name = process_line(line, in_specs)
|
|
37
|
+
gems.add(gem_name) if gem_name
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
gems
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def process_line(line, in_specs)
|
|
44
|
+
return [true, nil] if line.strip == 'specs:'
|
|
45
|
+
return [in_specs, nil] unless in_specs
|
|
46
|
+
|
|
47
|
+
match = line.match(SPEC_LINE)
|
|
48
|
+
return [true, match[1]] if match
|
|
49
|
+
|
|
50
|
+
still_in_specs = line.start_with?(' ') || line.strip.empty?
|
|
51
|
+
[still_in_specs, nil]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
require_relative 'tool_block'
|
|
6
|
+
|
|
7
|
+
module Reviewer
|
|
8
|
+
module Setup
|
|
9
|
+
# Produces .reviewer.yml YAML content from a list of detected tool keys.
|
|
10
|
+
# Orchestrates which tools to include; delegates per-tool rendering to ToolBlock.
|
|
11
|
+
class Generator
|
|
12
|
+
attr_reader :tool_keys, :project_dir
|
|
13
|
+
|
|
14
|
+
# Creates a generator for producing .reviewer.yml configuration
|
|
15
|
+
# @param tool_keys [Array<Symbol>] catalog tool keys to include in config
|
|
16
|
+
# @param project_dir [Pathname] the project root (used for package manager detection)
|
|
17
|
+
#
|
|
18
|
+
# @return [Generator]
|
|
19
|
+
def initialize(tool_keys, project_dir: Pathname.pwd)
|
|
20
|
+
@tool_keys = tool_keys
|
|
21
|
+
@project_dir = Pathname(project_dir)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Generates YAML configuration string for the detected tools
|
|
25
|
+
#
|
|
26
|
+
# @return [String] valid YAML for .reviewer.yml
|
|
27
|
+
def generate
|
|
28
|
+
return "--- {}\n" if tool_keys.empty?
|
|
29
|
+
|
|
30
|
+
blocks = tool_keys.filter_map do |key|
|
|
31
|
+
definition = Catalog.config_for(key)
|
|
32
|
+
next unless definition
|
|
33
|
+
|
|
34
|
+
ToolBlock.new(key, definition, js_runner: js_runner).to_s
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
"---\n#{blocks.join("\n")}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Detects the JS package manager based on lockfile presence
|
|
43
|
+
def js_runner
|
|
44
|
+
@js_runner ||= if project_dir.join('yarn.lock').exist?
|
|
45
|
+
'yarn'
|
|
46
|
+
elsif project_dir.join('pnpm-lock.yaml').exist?
|
|
47
|
+
'pnpm exec'
|
|
48
|
+
else
|
|
49
|
+
'npx'
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
module Setup
|
|
5
|
+
# Renders the YAML configuration block for a single tool definition.
|
|
6
|
+
# Owns the definition data so rendering methods reference self's state
|
|
7
|
+
# rather than reaching into parameters.
|
|
8
|
+
class ToolBlock
|
|
9
|
+
YAML_BARE_WORDS = %w[true false yes no on off null ~].freeze
|
|
10
|
+
|
|
11
|
+
# Creates a renderer for a single tool's YAML configuration block
|
|
12
|
+
# @param key [Symbol] the tool key (e.g., :rubocop)
|
|
13
|
+
# @param definition [Hash] the catalog definition for this tool
|
|
14
|
+
# @param js_runner [String] the JS package runner to substitute for npx
|
|
15
|
+
#
|
|
16
|
+
# @return [ToolBlock]
|
|
17
|
+
def initialize(key, definition, js_runner:)
|
|
18
|
+
@key = key
|
|
19
|
+
@definition = definition
|
|
20
|
+
@js_runner = js_runner
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Renders the full YAML block for this tool
|
|
24
|
+
#
|
|
25
|
+
# @return [String] the YAML text for this tool's configuration
|
|
26
|
+
def to_s
|
|
27
|
+
lines = header_lines
|
|
28
|
+
lines.concat(commands_block)
|
|
29
|
+
lines.concat(files_block) if @definition[:files]
|
|
30
|
+
lines << ''
|
|
31
|
+
lines.join("\n")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def header_lines
|
|
37
|
+
lines = []
|
|
38
|
+
lines << "# #{@definition[:description]}"
|
|
39
|
+
lines << "#{@key}:"
|
|
40
|
+
lines << " name: #{quote(@definition[:name])}"
|
|
41
|
+
lines << " description: #{quote(@definition[:description])}"
|
|
42
|
+
lines << tags_line if @definition[:tags]&.any?
|
|
43
|
+
lines
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def tags_line
|
|
47
|
+
" tags: [#{@definition[:tags].join(', ')}]"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def commands_block
|
|
51
|
+
commands = @definition[:commands]
|
|
52
|
+
lines = [' commands:']
|
|
53
|
+
%i[install prepare review format].each do |type|
|
|
54
|
+
next unless commands[type]
|
|
55
|
+
|
|
56
|
+
lines << " #{type}: #{quote(apply_js_runner(commands[type].to_s))}"
|
|
57
|
+
end
|
|
58
|
+
lines
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def files_block
|
|
62
|
+
lines = [' files:']
|
|
63
|
+
lines.concat(files_command_lines)
|
|
64
|
+
lines.concat(files_targeting_lines)
|
|
65
|
+
lines
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def files_command_lines
|
|
69
|
+
%i[review format].filter_map do |type|
|
|
70
|
+
" #{type}: #{quote(apply_js_runner(tool_files[type].to_s))}" if tool_files[type]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def files_targeting_lines
|
|
75
|
+
[
|
|
76
|
+
(file_setting_line(:flag) if tool_files.key?(:flag)),
|
|
77
|
+
(file_setting_line(:separator) if tool_files.key?(:separator)),
|
|
78
|
+
(file_setting_line(:pattern) if tool_files[:pattern]),
|
|
79
|
+
(" map_to_tests: #{tool_files[:map_to_tests]}" if tool_files[:map_to_tests])
|
|
80
|
+
].compact
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def file_setting_line(key)
|
|
84
|
+
" #{key}: #{quote(tool_files[key])}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def tool_files
|
|
88
|
+
@definition[:files]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def apply_js_runner(command)
|
|
92
|
+
return command unless command.start_with?('npx ')
|
|
93
|
+
|
|
94
|
+
command.sub('npx ', "#{@js_runner} ")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def quote(value)
|
|
98
|
+
str = value.to_s
|
|
99
|
+
return "''" if str.empty?
|
|
100
|
+
|
|
101
|
+
needs_quoting?(str) ? "'#{str.gsub("'", "''")}'" : str
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def needs_quoting?(str)
|
|
105
|
+
str.match?(/[:#\[\]{}&*!|>'"@`,]/) ||
|
|
106
|
+
str.strip != str ||
|
|
107
|
+
str.empty? ||
|
|
108
|
+
YAML_BARE_WORDS.include?(str.downcase)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'setup/catalog'
|
|
4
|
+
require_relative 'setup/detector'
|
|
5
|
+
require_relative 'setup/formatter'
|
|
6
|
+
require_relative 'setup/gemfile_lock'
|
|
7
|
+
require_relative 'setup/generator'
|
|
8
|
+
|
|
9
|
+
module Reviewer
|
|
10
|
+
# Handles first-run setup: detecting tools and generating .reviewer.yml
|
|
11
|
+
module Setup
|
|
12
|
+
# URL to the configuration documentation for setup output messages
|
|
13
|
+
CONFIG_URL = 'https://github.com/garrettdimon/reviewer#configuration'
|
|
14
|
+
|
|
15
|
+
# Runs the full setup flow: detect tools, generate config, display results
|
|
16
|
+
# @param project_dir [Pathname, String] the project root to scan (defaults to pwd)
|
|
17
|
+
# @param output [Output] the console output handler
|
|
18
|
+
#
|
|
19
|
+
# @return [void]
|
|
20
|
+
def self.run(configuration:, project_dir: Pathname.pwd, output: Output.new)
|
|
21
|
+
config_file = configuration.file
|
|
22
|
+
formatter = Formatter.new(output)
|
|
23
|
+
|
|
24
|
+
if config_file.exist?
|
|
25
|
+
formatter.setup_already_exists(config_file)
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
results = Detector.new(project_dir).detect
|
|
30
|
+
|
|
31
|
+
if results.empty?
|
|
32
|
+
formatter.setup_no_tools_detected
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
yaml = Generator.new(results.map(&:key), project_dir: project_dir).generate
|
|
37
|
+
config_file.write(yaml)
|
|
38
|
+
formatter.setup_success(results)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -18,7 +18,16 @@ module Reviewer
|
|
|
18
18
|
executable_not_found: "can't find executable"
|
|
19
19
|
}.freeze
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
# @!attribute stdout
|
|
22
|
+
# @return [String, nil] standard output from the command
|
|
23
|
+
# @!attribute stderr
|
|
24
|
+
# @return [String, nil] standard error from the command
|
|
25
|
+
# @!attribute status
|
|
26
|
+
# @return [Process::Status, nil] the process status object
|
|
27
|
+
# @!attribute exit_status
|
|
28
|
+
# @return [Integer, nil] the exit status code from the command
|
|
29
|
+
attr_reader :stdout, :stderr, :status
|
|
30
|
+
attr_accessor :exit_status
|
|
22
31
|
|
|
23
32
|
# An instance of a result from running a local command. Captures the values for `$stdout`,
|
|
24
33
|
# `$stderr`, and the exit status of the command to provide a reliable way of interpreting
|
|
@@ -39,24 +48,18 @@ module Reviewer
|
|
|
39
48
|
@exit_status = status&.exitstatus
|
|
40
49
|
end
|
|
41
50
|
|
|
42
|
-
def exists?
|
|
43
|
-
[stdout, stderr, exit_status].compact.any?
|
|
44
|
-
end
|
|
51
|
+
def exists? = [stdout, stderr, exit_status].compact.any?
|
|
45
52
|
|
|
46
53
|
# Determines if re-running a command is entirely futile. Primarily to help when a command
|
|
47
54
|
# fails within a batch and needs to be re-run to show the output
|
|
48
55
|
#
|
|
49
56
|
# @return [Boolean] true if the exit status code is greater than or equal to 126
|
|
50
|
-
def rerunnable?
|
|
51
|
-
exit_status < EXIT_STATUS_CODES[:cannot_execute]
|
|
52
|
-
end
|
|
57
|
+
def rerunnable? = exit_status < EXIT_STATUS_CODES[:cannot_execute]
|
|
53
58
|
|
|
54
59
|
# Determines whether a command simply cannot be executed.
|
|
55
60
|
#
|
|
56
61
|
# @return [Boolean] true if the exit sttaus code equals 126
|
|
57
|
-
def cannot_execute?
|
|
58
|
-
exit_status == EXIT_STATUS_CODES[:cannot_execute]
|
|
59
|
-
end
|
|
62
|
+
def cannot_execute? = exit_status == EXIT_STATUS_CODES[:cannot_execute]
|
|
60
63
|
|
|
61
64
|
# Determines whether the command failed because the executable cannot be found. Since this is
|
|
62
65
|
# an error that can be corrected fairly predictably and easily, it provides the ability to
|
|
@@ -73,11 +76,7 @@ module Reviewer
|
|
|
73
76
|
#
|
|
74
77
|
# @return [String] stdout if present, otherwise stderr
|
|
75
78
|
def to_s
|
|
76
|
-
|
|
77
|
-
result_string += stderr
|
|
78
|
-
result_string += stdout
|
|
79
|
-
|
|
80
|
-
result_string.strip
|
|
79
|
+
[stderr, stdout].compact.join("\n").strip
|
|
81
80
|
end
|
|
82
81
|
end
|
|
83
82
|
end
|
data/lib/reviewer/shell/timer.rb
CHANGED
|
@@ -6,67 +6,72 @@ module Reviewer
|
|
|
6
6
|
class Shell
|
|
7
7
|
# Provides a structured interface for measuring realtime main while running comamnds
|
|
8
8
|
class Timer
|
|
9
|
-
|
|
9
|
+
# @!attribute prep
|
|
10
|
+
# @return [Float, nil] the preparation time in seconds
|
|
11
|
+
# @!attribute main
|
|
12
|
+
# @return [Float, nil] the main execution time in seconds
|
|
13
|
+
attr_reader :prep, :main
|
|
10
14
|
|
|
11
|
-
# A
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# @param
|
|
15
|
-
# @param main: nil [Float] the amount of time in seconds the primary command ran
|
|
15
|
+
# A timer that tracks preparation and main execution times separately.
|
|
16
|
+
# Times can be passed directly or recorded using `record_prep` and `record_main`.
|
|
17
|
+
# @param prep [Float, nil] the preparation time in seconds
|
|
18
|
+
# @param main [Float, nil] the main execution time in seconds
|
|
16
19
|
#
|
|
17
|
-
# @return [
|
|
20
|
+
# @return [Timer]
|
|
18
21
|
def initialize(prep: nil, main: nil)
|
|
19
22
|
@prep = prep
|
|
20
23
|
@main = main
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
# Records the execution time for the block and assigns it to the `prep` time
|
|
24
|
-
# @param
|
|
27
|
+
# @param block [Block] the commands to be timed
|
|
25
28
|
#
|
|
26
29
|
# @return [Float] the execution time for the preparation
|
|
27
|
-
def record_prep(&
|
|
28
|
-
@prep = record(&block)
|
|
29
|
-
end
|
|
30
|
+
def record_prep(&) = @prep = record(&)
|
|
30
31
|
|
|
31
32
|
# Records the execution time for the block and assigns it to the `main` time
|
|
32
|
-
# @param
|
|
33
|
+
# @param block [Block] the commands to be timed
|
|
33
34
|
#
|
|
34
35
|
# @return [Float] the execution time for the main command
|
|
35
|
-
def record_main(&
|
|
36
|
-
@main = record(&block)
|
|
37
|
-
end
|
|
36
|
+
def record_main(&) = @main = record(&)
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
# The preparation time rounded to two decimal places
|
|
39
|
+
#
|
|
40
|
+
# @return [Float] prep time in seconds
|
|
41
|
+
def prep_seconds = prep.round(2)
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
# The main execution time rounded to two decimal places
|
|
44
|
+
#
|
|
45
|
+
# @return [Float] main time in seconds
|
|
46
|
+
def main_seconds = main.round(2)
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
# The total execution time (prep + main) rounded to two decimal places
|
|
49
|
+
#
|
|
50
|
+
# @return [Float] total time in seconds
|
|
51
|
+
def total_seconds = total.round(2)
|
|
52
|
+
|
|
53
|
+
# The total time (prep + main) without rounding
|
|
54
|
+
#
|
|
55
|
+
# @return [Float] total time in seconds
|
|
56
|
+
def total = [prep, main].compact.sum
|
|
57
|
+
|
|
58
|
+
# Whether both prep and main times have been recorded
|
|
59
|
+
#
|
|
60
|
+
# @return [Boolean] true if both phases were timed
|
|
61
|
+
def prepped? = [prep, main].all?
|
|
50
62
|
|
|
63
|
+
# The percentage of total time spent on preparation
|
|
64
|
+
#
|
|
65
|
+
# @return [Integer, nil] percentage (0-100) or nil if not prepped
|
|
51
66
|
def prep_percent
|
|
52
67
|
return nil unless prepped?
|
|
53
68
|
|
|
54
69
|
(prep / total.to_f * 100).round
|
|
55
70
|
end
|
|
56
71
|
|
|
57
|
-
def total
|
|
58
|
-
[prep, main].compact.sum
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def prepped?
|
|
62
|
-
!(prep.nil? || main.nil?)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
72
|
private
|
|
66
73
|
|
|
67
|
-
def record(&
|
|
68
|
-
Benchmark.realtime(&block)
|
|
69
|
-
end
|
|
74
|
+
def record(&) = Benchmark.realtime(&)
|
|
70
75
|
end
|
|
71
76
|
end
|
|
72
77
|
end
|
data/lib/reviewer/shell.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'open3'
|
|
4
|
+
require 'pty'
|
|
4
5
|
|
|
5
6
|
require_relative 'shell/result'
|
|
6
7
|
require_relative 'shell/timer'
|
|
@@ -15,40 +16,68 @@ module Reviewer
|
|
|
15
16
|
def_delegators :@result, :exit_status
|
|
16
17
|
|
|
17
18
|
# Initializes a Reviewer shell for running and benchmarking commands, and capturing output
|
|
19
|
+
# @param stream [IO] the output stream for direct (passthrough) output
|
|
18
20
|
#
|
|
19
21
|
# @return [Shell] a shell instance for running and benchmarking commands
|
|
20
|
-
def initialize
|
|
22
|
+
def initialize(stream: $stdout)
|
|
23
|
+
@stream = stream
|
|
21
24
|
@timer = Timer.new
|
|
22
25
|
@result = Result.new
|
|
26
|
+
@captured_results = nil
|
|
23
27
|
end
|
|
24
28
|
|
|
25
|
-
# Run a command
|
|
26
|
-
#
|
|
27
|
-
#
|
|
29
|
+
# Run a command via PTY, streaming output in real-time while capturing it for later use
|
|
30
|
+
# (e.g. failed file extraction). PTY allocates a pseudo-terminal so the child process
|
|
31
|
+
# preserves ANSI colors and interactive behavior.
|
|
28
32
|
# @param command [String] the command to run
|
|
29
33
|
#
|
|
30
|
-
# @return [
|
|
34
|
+
# @return [Result] the captured result including stdout and exit status
|
|
31
35
|
def direct(command)
|
|
32
36
|
command = String(command)
|
|
37
|
+
buffer = +''
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
reader, _writer, pid = PTY.spawn(command)
|
|
40
|
+
begin
|
|
41
|
+
reader.each_line do |line|
|
|
42
|
+
@stream.print line
|
|
43
|
+
buffer << line
|
|
44
|
+
end
|
|
45
|
+
rescue Errno::EIO
|
|
46
|
+
# Expected when child process exits before all output is read
|
|
47
|
+
end
|
|
36
48
|
|
|
37
|
-
|
|
38
|
-
|
|
49
|
+
_, status = Process.waitpid2(pid)
|
|
50
|
+
@result = Result.new(buffer, nil, status)
|
|
51
|
+
rescue Errno::ENOENT
|
|
52
|
+
@result = Result.new(buffer, nil, nil)
|
|
53
|
+
@result.exit_status = Result::EXIT_STATUS_CODES[:executable_not_found]
|
|
39
54
|
end
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
# Captures and times the preparation command execution
|
|
57
|
+
# @param command [String, Command] the command to run
|
|
58
|
+
#
|
|
59
|
+
# @return [Result] the captured result including stdout, stderr, and exit status
|
|
60
|
+
def capture_prep(command) = timer.record_prep { capture_results(command) }
|
|
61
|
+
|
|
62
|
+
# Captures and times the main command execution
|
|
63
|
+
# @param command [String, Command] the command to run
|
|
64
|
+
#
|
|
65
|
+
# @return [Result] the captured result including stdout, stderr, and exit status
|
|
66
|
+
def capture_main(command) = timer.record_main { capture_results(command) }
|
|
44
67
|
|
|
45
68
|
private
|
|
46
69
|
|
|
70
|
+
# Open3.capture3 returns stdout, stderr, and status separately. Keeping them
|
|
71
|
+
# separate matters for FailedFiles, which merges the streams intentionally
|
|
72
|
+
# when scanning for file paths after a failure.
|
|
47
73
|
def capture_results(command)
|
|
48
74
|
command = String(command)
|
|
49
75
|
|
|
50
76
|
@captured_results = Open3.capture3(command)
|
|
51
77
|
@result = Result.new(*@captured_results)
|
|
78
|
+
rescue Errno::ENOENT
|
|
79
|
+
@result = Result.new(nil, nil, nil)
|
|
80
|
+
@result.exit_status = Result::EXIT_STATUS_CODES[:executable_not_found]
|
|
52
81
|
end
|
|
53
82
|
end
|
|
54
83
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
class Tool
|
|
5
|
+
# Conversion functions for coercing values to Tool instances
|
|
6
|
+
module Conversions
|
|
7
|
+
# Coerces a value into a Tool instance
|
|
8
|
+
# @param value [Tool] the value to convert
|
|
9
|
+
# @return [Tool] the resulting Tool instance
|
|
10
|
+
# @raise [TypeError] if the value is not a Tool
|
|
11
|
+
def Tool(value) # rubocop:disable Naming/MethodName
|
|
12
|
+
case value
|
|
13
|
+
in Tool then value
|
|
14
|
+
else raise TypeError, "Cannot convert #{value.class} to Tool"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
module_function :Tool
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
class Tool
|
|
5
|
+
# Resolves which files a tool should process by mapping and filtering.
|
|
6
|
+
class FileResolver
|
|
7
|
+
# Creates a FileResolver for a tool's settings
|
|
8
|
+
# @param settings [Tool::Settings] the tool's settings containing file configuration
|
|
9
|
+
#
|
|
10
|
+
# @return [FileResolver] a resolver instance for the tool
|
|
11
|
+
def initialize(settings)
|
|
12
|
+
@settings = settings
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Resolves input files by mapping source files to test files (if configured) and
|
|
16
|
+
# filtering by the tool's file pattern
|
|
17
|
+
# @param files [Array<String>] the input files to resolve
|
|
18
|
+
#
|
|
19
|
+
# @return [Array<String>] files after mapping and filtering
|
|
20
|
+
def resolve(files)
|
|
21
|
+
return files unless pattern
|
|
22
|
+
|
|
23
|
+
filter(map(files))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Determines if the tool should be skipped because files were requested but none match
|
|
27
|
+
# @param files [Array<String>] the requested files
|
|
28
|
+
#
|
|
29
|
+
# @return [Boolean] true if files were requested but none remain after resolution
|
|
30
|
+
def skip?(files)
|
|
31
|
+
files.any? && resolve(files).empty?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
attr_reader :settings
|
|
37
|
+
|
|
38
|
+
def map(files)
|
|
39
|
+
mapper = settings.map_to_tests
|
|
40
|
+
return files unless mapper
|
|
41
|
+
|
|
42
|
+
TestFileMapper.new(mapper).map(files)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def filter(files)
|
|
46
|
+
files.select { |file| File.fnmatch(pattern, File.basename(file)) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def pattern
|
|
50
|
+
settings.files_pattern
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|