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
|
@@ -18,34 +18,48 @@ 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
|
-
# An instance of a result from running a local command
|
|
32
|
+
# An instance of a result from running a local command. Captures the values for `$stdout`,
|
|
33
|
+
# `$stderr`, and the exit status of the command to provide a reliable way of interpreting
|
|
34
|
+
# the results for commands that otherwise use these values inconsistently.
|
|
24
35
|
# @param stdout = nil [String] standard out output from a command
|
|
25
36
|
# @param stderr = nil [String] standard error output from a command
|
|
26
37
|
# @param status = nil [ProcessStatus] an instance of ProcessStatus for a command
|
|
27
38
|
#
|
|
28
|
-
# @
|
|
39
|
+
# @example Using with `Open3.capture3`
|
|
40
|
+
# captured_results = Open3.capture3(command)
|
|
41
|
+
# result = Result.new(*captured_results)
|
|
42
|
+
#
|
|
43
|
+
# @return [self]
|
|
29
44
|
def initialize(stdout = nil, stderr = nil, status = nil)
|
|
30
45
|
@stdout = stdout
|
|
31
46
|
@stderr = stderr
|
|
47
|
+
@status = status
|
|
32
48
|
@exit_status = status&.exitstatus
|
|
33
49
|
end
|
|
34
50
|
|
|
35
|
-
|
|
51
|
+
def exists? = [stdout, stderr, exit_status].compact.any?
|
|
52
|
+
|
|
53
|
+
# Determines if re-running a command is entirely futile. Primarily to help when a command
|
|
36
54
|
# fails within a batch and needs to be re-run to show the output
|
|
37
55
|
#
|
|
38
56
|
# @return [Boolean] true if the exit status code is greater than or equal to 126
|
|
39
|
-
def
|
|
40
|
-
exit_status >= EXIT_STATUS_CODES[:cannot_execute]
|
|
41
|
-
end
|
|
57
|
+
def rerunnable? = exit_status < EXIT_STATUS_CODES[:cannot_execute]
|
|
42
58
|
|
|
43
59
|
# Determines whether a command simply cannot be executed.
|
|
44
60
|
#
|
|
45
61
|
# @return [Boolean] true if the exit sttaus code equals 126
|
|
46
|
-
def cannot_execute?
|
|
47
|
-
exit_status == EXIT_STATUS_CODES[:cannot_execute]
|
|
48
|
-
end
|
|
62
|
+
def cannot_execute? = exit_status == EXIT_STATUS_CODES[:cannot_execute]
|
|
49
63
|
|
|
50
64
|
# Determines whether the command failed because the executable cannot be found. Since this is
|
|
51
65
|
# an error that can be corrected fairly predictably and easily, it provides the ability to
|
|
@@ -62,7 +76,7 @@ module Reviewer
|
|
|
62
76
|
#
|
|
63
77
|
# @return [String] stdout if present, otherwise stderr
|
|
64
78
|
def to_s
|
|
65
|
-
stderr.strip
|
|
79
|
+
[stderr, stdout].compact.join("\n").strip
|
|
66
80
|
end
|
|
67
81
|
end
|
|
68
82
|
end
|
data/lib/reviewer/shell/timer.rb
CHANGED
|
@@ -6,52 +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
|
|
|
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
|
|
19
|
+
#
|
|
20
|
+
# @return [Timer]
|
|
11
21
|
def initialize(prep: nil, main: nil)
|
|
12
22
|
@prep = prep
|
|
13
23
|
@main = main
|
|
14
24
|
end
|
|
15
25
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
# Records the execution time for the block and assigns it to the `prep` time
|
|
27
|
+
# @param block [Block] the commands to be timed
|
|
28
|
+
#
|
|
29
|
+
# @return [Float] the execution time for the preparation
|
|
30
|
+
def record_prep(&) = @prep = record(&)
|
|
19
31
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
32
|
+
# Records the execution time for the block and assigns it to the `main` time
|
|
33
|
+
# @param block [Block] the commands to be timed
|
|
34
|
+
#
|
|
35
|
+
# @return [Float] the execution time for the main command
|
|
36
|
+
def record_main(&) = @main = record(&)
|
|
23
37
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
38
|
+
# The preparation time rounded to two decimal places
|
|
39
|
+
#
|
|
40
|
+
# @return [Float] prep time in seconds
|
|
41
|
+
def prep_seconds = prep.round(2)
|
|
27
42
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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)
|
|
31
47
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
35
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?
|
|
62
|
+
|
|
63
|
+
# The percentage of total time spent on preparation
|
|
64
|
+
#
|
|
65
|
+
# @return [Integer, nil] percentage (0-100) or nil if not prepped
|
|
36
66
|
def prep_percent
|
|
37
67
|
return nil unless prepped?
|
|
38
68
|
|
|
39
69
|
(prep / total.to_f * 100).round
|
|
40
70
|
end
|
|
41
71
|
|
|
42
|
-
def total
|
|
43
|
-
[prep, main].compact.sum
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def prepped?
|
|
47
|
-
!(prep.nil? || main.nil?)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
72
|
private
|
|
51
73
|
|
|
52
|
-
def record(&
|
|
53
|
-
Benchmark.realtime(&block)
|
|
54
|
-
end
|
|
74
|
+
def record(&) = Benchmark.realtime(&)
|
|
55
75
|
end
|
|
56
76
|
end
|
|
57
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'
|
|
@@ -10,49 +11,73 @@ module Reviewer
|
|
|
10
11
|
class Shell
|
|
11
12
|
extend Forwardable
|
|
12
13
|
|
|
13
|
-
attr_reader :timer, :result
|
|
14
|
+
attr_reader :timer, :result, :captured_results
|
|
14
15
|
|
|
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
|
-
|
|
33
|
-
|
|
36
|
+
command = String(command)
|
|
37
|
+
buffer = +''
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
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]
|
|
41
54
|
end
|
|
42
55
|
|
|
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) }
|
|
44
61
|
|
|
45
|
-
|
|
46
|
-
|
|
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) }
|
|
47
67
|
|
|
48
|
-
|
|
49
|
-
@result = Result.new(*captured_results)
|
|
50
|
-
end
|
|
68
|
+
private
|
|
51
69
|
|
|
52
|
-
|
|
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.
|
|
73
|
+
def capture_results(command)
|
|
53
74
|
command = String(command)
|
|
54
75
|
|
|
55
|
-
|
|
76
|
+
@captured_results = Open3.capture3(command)
|
|
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]
|
|
56
81
|
end
|
|
57
82
|
end
|
|
58
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
|
|
@@ -2,80 +2,131 @@
|
|
|
2
2
|
|
|
3
3
|
module Reviewer
|
|
4
4
|
class Tool
|
|
5
|
-
# Converts/casts tool configuration values and provides default values if not set.
|
|
5
|
+
# Converts/casts tool configuration values and provides appropriate default values if not set.
|
|
6
6
|
class Settings
|
|
7
7
|
attr_reader :tool_key, :config
|
|
8
8
|
|
|
9
9
|
alias key tool_key
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# Creates an instance of settings for retrieving values from the configuration file
|
|
12
|
+
# @param tool_key [Symbol] the unique identifier for the tool in the config file
|
|
13
|
+
# @param config [Hash] the configuration values to examine for the settings
|
|
14
|
+
#
|
|
15
|
+
# @return [Settings]
|
|
16
|
+
def initialize(tool_key, config:)
|
|
12
17
|
@tool_key = tool_key.to_sym
|
|
13
|
-
@config = config
|
|
18
|
+
@config = config
|
|
14
19
|
end
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
# Returns a hash code for comparing settings instances
|
|
22
|
+
#
|
|
23
|
+
# @return [Integer] hash code based on configuration state
|
|
24
|
+
def hash = state.hash
|
|
19
25
|
|
|
26
|
+
# Compares two settings instances for equality based on their configuration
|
|
27
|
+
# @param other [Settings] the settings to compare against
|
|
28
|
+
# @return [Boolean] true if both have the same configuration
|
|
20
29
|
def eql?(other)
|
|
21
30
|
self.class == other.class &&
|
|
22
31
|
state == other.state
|
|
23
32
|
end
|
|
24
33
|
alias :== eql?
|
|
25
34
|
|
|
26
|
-
def
|
|
27
|
-
config.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
35
|
+
def skip_in_batch?
|
|
36
|
+
if config.key?(:skip_in_batch)
|
|
37
|
+
config.fetch(:skip_in_batch) { false }
|
|
38
|
+
else
|
|
39
|
+
config.fetch(:disabled) { false }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def disabled? = skip_in_batch?
|
|
44
|
+
def enabled? = !skip_in_batch?
|
|
45
|
+
|
|
46
|
+
# The human-readable name of the tool
|
|
47
|
+
#
|
|
48
|
+
# @return [String] the configured name or capitalized tool key
|
|
49
|
+
def name = config.fetch(:name) { tool_key.to_s.capitalize }
|
|
50
|
+
|
|
51
|
+
# The human-readable description of what the tool does
|
|
52
|
+
#
|
|
53
|
+
# @return [String] the configured description or a default placeholder
|
|
54
|
+
def description = config.fetch(:description) { "(No description provided for '#{name}')" }
|
|
55
|
+
|
|
56
|
+
# The tags used to categorize and filter the tool
|
|
57
|
+
#
|
|
58
|
+
# @return [Array<String>] configured tags or empty array
|
|
59
|
+
def tags = config.fetch(:tags) { [] }
|
|
60
|
+
|
|
61
|
+
# The collection of reference links for the tool (home, install, usage, etc.)
|
|
62
|
+
#
|
|
63
|
+
# @return [Hash] configured links or empty hash if none
|
|
64
|
+
def links = config.fetch(:links) { {} }
|
|
65
|
+
|
|
66
|
+
# The environment variables to set when running the tool
|
|
67
|
+
#
|
|
68
|
+
# @return [Hash] configured env vars or empty hash
|
|
69
|
+
def env = config.fetch(:env) { {} }
|
|
70
|
+
|
|
71
|
+
# The CLI flags to pass to the tool's review command
|
|
72
|
+
#
|
|
73
|
+
# @return [Hash] configured flags or empty hash
|
|
74
|
+
def flags = config.fetch(:flags) { {} }
|
|
75
|
+
|
|
76
|
+
# The CLI flag used to pass files to the tool (e.g., '--files')
|
|
77
|
+
#
|
|
78
|
+
# @return [String] the configured flag or empty string if files are passed directly
|
|
79
|
+
def files_flag = config.dig(:files, :flag) || ''
|
|
80
|
+
|
|
81
|
+
# The separator used to join multiple file paths in the command
|
|
82
|
+
#
|
|
83
|
+
# @return [String] the configured separator or a space by default
|
|
84
|
+
def files_separator = config.dig(:files, :separator) || ' '
|
|
85
|
+
|
|
86
|
+
# The glob pattern used to filter which files this tool should process
|
|
87
|
+
#
|
|
88
|
+
# @return [String, nil] the pattern (e.g., '*.rb') or nil if not configured
|
|
89
|
+
def files_pattern = config.dig(:files, :pattern)
|
|
90
|
+
|
|
91
|
+
# The test framework to use for mapping source files to test files
|
|
92
|
+
#
|
|
93
|
+
# @return [String, nil] the framework name ('minitest' or 'rspec') or nil if not configured
|
|
94
|
+
def map_to_tests = config.dig(:files, :map_to_tests)
|
|
95
|
+
|
|
96
|
+
def supports_files? = config.key?(:files)
|
|
97
|
+
|
|
98
|
+
# The regex pattern for extracting a summary detail from tool output
|
|
99
|
+
#
|
|
100
|
+
# @return [String, nil] the configured pattern or nil
|
|
101
|
+
def summary_pattern = config.dig(:summary, :pattern)
|
|
102
|
+
|
|
103
|
+
# The label template for displaying the extracted summary detail
|
|
104
|
+
#
|
|
105
|
+
# @return [String, nil] the configured label or nil
|
|
106
|
+
def summary_label = config.dig(:summary, :label)
|
|
107
|
+
|
|
108
|
+
# Returns the file-scoped command override for a given command type.
|
|
109
|
+
# When configured, this command replaces the standard command when files are passed.
|
|
110
|
+
#
|
|
111
|
+
# @param command_type [Symbol] the command type (:review, :format)
|
|
112
|
+
# @return [String, nil] the file-scoped command or nil if not configured
|
|
113
|
+
def files_command(command_type) = config.dig(:files, command_type)
|
|
114
|
+
|
|
115
|
+
# The collection of configured commands for the tool
|
|
116
|
+
#
|
|
117
|
+
# @return [Hash] all of the commands configured for the tool
|
|
118
|
+
def commands = config.fetch(:commands) { {} }
|
|
119
|
+
|
|
120
|
+
# The largest exit status that can still be considered a success for the command
|
|
121
|
+
#
|
|
122
|
+
# @return [Integer] the configured `max_exit_status` for the tool or 0 if one isn't configured
|
|
123
|
+
def max_exit_status = commands.fetch(:max_exit_status) { 0 }
|
|
69
124
|
|
|
70
125
|
protected
|
|
71
126
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def load_config
|
|
77
|
-
Reviewer.tools.to_h.fetch(key) { {} }
|
|
78
|
-
end
|
|
127
|
+
# Returns the configuration as a plain hash for comparison
|
|
128
|
+
# @return [Hash] the configuration state
|
|
129
|
+
def state = config.to_hash
|
|
79
130
|
end
|
|
80
131
|
end
|
|
81
132
|
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Reviewer
|
|
4
|
+
class Tool
|
|
5
|
+
# Maps source files to their corresponding test files based on framework conventions.
|
|
6
|
+
class TestFileMapper
|
|
7
|
+
FRAMEWORKS = {
|
|
8
|
+
minitest: { dir: 'test', suffix: '_test.rb', source_dirs: %w[app lib] },
|
|
9
|
+
rspec: { dir: 'spec', suffix: '_spec.rb', source_dirs: %w[app lib] }
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
# Creates a mapper for the specified test framework
|
|
13
|
+
# @param framework [Symbol, String, nil] the test framework (:minitest or :rspec)
|
|
14
|
+
#
|
|
15
|
+
# @return [TestFileMapper] a mapper instance for the framework
|
|
16
|
+
def initialize(framework)
|
|
17
|
+
@framework = framework&.to_sym
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Maps source files to their corresponding test files
|
|
21
|
+
# @param files [Array<String>] source files to map
|
|
22
|
+
#
|
|
23
|
+
# @return [Array<String>] mapped test files (only those that exist on disk)
|
|
24
|
+
def map(files)
|
|
25
|
+
return files unless supported?
|
|
26
|
+
|
|
27
|
+
files.map { |file| map_file(file) }.compact.uniq
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Checks if the framework is supported for mapping
|
|
31
|
+
#
|
|
32
|
+
# @return [Boolean] true if the framework is :minitest or :rspec
|
|
33
|
+
def supported?
|
|
34
|
+
@framework && FRAMEWORKS.key?(@framework)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def map_file(file)
|
|
40
|
+
return file if test_file?(file)
|
|
41
|
+
|
|
42
|
+
mapped = source_to_test(file)
|
|
43
|
+
mapped && File.exist?(mapped) ? mapped : nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_file?(file)
|
|
47
|
+
file.end_with?(config[:suffix])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def source_to_test(file)
|
|
51
|
+
return nil unless file.end_with?('.rb')
|
|
52
|
+
|
|
53
|
+
replace_source_dir_with_test_dir(file).sub(/\.rb$/, config[:suffix])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def replace_source_dir_with_test_dir(path)
|
|
57
|
+
config[:source_dirs].each do |dir|
|
|
58
|
+
return path.sub(%r{^#{dir}/}, "#{config[:dir]}/") if path.start_with?("#{dir}/")
|
|
59
|
+
end
|
|
60
|
+
prepend_test_dir(path)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def prepend_test_dir(path)
|
|
64
|
+
dir = config[:dir]
|
|
65
|
+
path.start_with?(dir) ? path : "#{dir}/#{path}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def config
|
|
69
|
+
FRAMEWORKS[@framework]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|