reviewer 0.1.1 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.alexignore +1 -0
  3. data/.flayignore +1 -0
  4. data/.github/workflows/main.yml +14 -4
  5. data/.gitignore +5 -0
  6. data/.inch.yml +4 -0
  7. data/.reviewer.example.yml +63 -0
  8. data/.reviewer.future.yml +221 -0
  9. data/.reviewer.yml +140 -0
  10. data/.reviewer_stdout +0 -0
  11. data/.rubocop.yml +20 -0
  12. data/CHANGELOG.md +38 -3
  13. data/Gemfile +2 -4
  14. data/Gemfile.lock +103 -20
  15. data/LICENSE.txt +4 -20
  16. data/README.md +23 -29
  17. data/Rakefile +5 -5
  18. data/bin/console +4 -4
  19. data/exe/fmt +7 -0
  20. data/exe/rvw +7 -0
  21. data/lib/reviewer/arguments/files.rb +94 -0
  22. data/lib/reviewer/arguments/keywords.rb +133 -0
  23. data/lib/reviewer/arguments/tags.rb +71 -0
  24. data/lib/reviewer/arguments.rb +54 -10
  25. data/lib/reviewer/batch.rb +91 -0
  26. data/lib/reviewer/command/string/env.rb +44 -0
  27. data/lib/reviewer/command/string/flags.rb +51 -0
  28. data/lib/reviewer/command/string.rb +66 -0
  29. data/lib/reviewer/command.rb +75 -0
  30. data/lib/reviewer/configuration.rb +32 -7
  31. data/lib/reviewer/conversions.rb +16 -0
  32. data/lib/reviewer/guidance.rb +77 -0
  33. data/lib/reviewer/history.rb +69 -0
  34. data/lib/reviewer/keywords/git/staged.rb +64 -0
  35. data/lib/reviewer/keywords/git.rb +14 -0
  36. data/lib/reviewer/keywords.rb +9 -0
  37. data/lib/reviewer/loader.rb +36 -9
  38. data/lib/reviewer/output/printer.rb +44 -0
  39. data/lib/reviewer/output/scrubber.rb +48 -0
  40. data/lib/reviewer/output/token.rb +85 -0
  41. data/lib/reviewer/output.rb +122 -0
  42. data/lib/reviewer/runner/strategies/captured.rb +157 -0
  43. data/lib/reviewer/runner/strategies/passthrough.rb +63 -0
  44. data/lib/reviewer/runner.rb +131 -0
  45. data/lib/reviewer/shell/result.rb +84 -0
  46. data/lib/reviewer/shell/timer.rb +72 -0
  47. data/lib/reviewer/shell.rb +54 -0
  48. data/lib/reviewer/tool/settings.rb +38 -19
  49. data/lib/reviewer/tool.rb +137 -23
  50. data/lib/reviewer/tools.rb +87 -8
  51. data/lib/reviewer/version.rb +1 -1
  52. data/lib/reviewer.rb +107 -28
  53. data/reviewer.gemspec +34 -19
  54. data/structure.svg +1 -0
  55. metadata +244 -12
  56. data/bin/review +0 -5
  57. data/lib/reviewer/tool/command.rb +0 -78
  58. data/lib/reviewer/tool/env.rb +0 -39
  59. data/lib/reviewer/tool/flags.rb +0 -39
  60. data/lib/reviewer/tool/verbosity.rb +0 -39
@@ -2,43 +2,87 @@
2
2
 
3
3
  require 'slop'
4
4
 
5
- # Handles option parsing for bin/review
5
+ require_relative 'arguments/keywords'
6
+ require_relative 'arguments/files'
7
+ require_relative 'arguments/tags'
8
+
6
9
  module Reviewer
10
+ # Handles option parsing for `rvw` and `fmt` commands
11
+ #
12
+ # @example
13
+ #
14
+ # `rvw`
15
+ # `rvw -t ruby`
16
+ # `rvw -f ./example.rb,./example_test.rb`
17
+ # `rvw staged`
18
+ # `rvw --files ./example.rb,./example_test.rb --tags syntax`
19
+ # `rvw ruby staged`
20
+ #
7
21
  class Arguments
8
22
  attr_accessor :options
9
23
 
24
+ attr_reader :output
25
+
26
+ # A catch all for aguments passed to reviewer via the command-line so they can be interpreted
27
+ # and made available via the relevant classes.
28
+ # @param options = ARGV [Hash] options to parse and extract the relevant values for a run
29
+ #
30
+ # @example Using all options: `rvw keyword_one keyword_two --files ./example.rb,./example_test.rb --tags syntax`
31
+ # reviewer = Reviewer::Arguments.new
32
+ # reviewer.files.to_a # => ['./example.rb','./example_test.rb']
33
+ # reviewer.tags.to_a # => ['syntax']
34
+ # reviewer.keywords.to_a # => ['keyword_one', 'keyword_two']
35
+ #
36
+ # @return [self]
10
37
  def initialize(options = ARGV)
38
+ @output = Output.new
11
39
  @options = Slop.parse options do |opts|
12
40
  opts.array '-f', '--files', 'a list of comma-separated files or paths', delimiter: ',', default: []
13
41
  opts.array '-t', '--tags', 'a list of comma-separated tags', delimiter: ',', default: []
14
42
 
15
43
  opts.on '-v', '--version', 'print the version' do
16
- puts VERSION
44
+ @output.help VERSION
17
45
  exit
18
46
  end
19
47
 
20
48
  opts.on '-h', '--help', 'print the help' do
21
- puts opts
49
+ @output.help opts
22
50
  exit
23
51
  end
24
52
  end
25
53
  end
26
54
 
27
- def files
28
- options[:files]
55
+ # Converts the arguments to a hash for versatility
56
+ #
57
+ # @return [Hash] The files, tags, and keywords collected from the command line options
58
+ def to_h
59
+ {
60
+ files: files.raw,
61
+ tags: tags.raw,
62
+ keywords: keywords.raw
63
+ }
29
64
  end
65
+ alias inspect to_h
30
66
 
67
+ # The tag arguments collected from the command line via the `-t` or `--tags` flag
68
+ #
69
+ # @return [Arguments::Tags] an colelction of the tag arguments collected from the command-line
31
70
  def tags
32
- options[:tags]
71
+ @tags ||= Arguments::Tags.new(provided: options[:tags])
33
72
  end
34
73
 
35
- def arguments
36
- options.arguments
74
+ # The file arguments collected from the command line via the `-f` or `--files` flag
75
+ #
76
+ # @return [Arguments::Files] an collection of the file arguments collected from the command-line
77
+ def files
78
+ @files ||= Arguments::Files.new(provided: options[:files])
37
79
  end
38
80
 
81
+ # The leftover arguments collected from the command line without being associated with a flag
82
+ #
83
+ # @return [Arguments::Keywords] an collection of the leftover arguments as keywords
39
84
  def keywords
40
- # TODO: Filter arguments to only for allowed keywords to be defined later.
41
- arguments
85
+ @keywords ||= Arguments::Keywords.new(options.arguments)
42
86
  end
43
87
  end
44
88
  end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reviewer
4
+ # Provides a structure for running commands for a given set of tools
5
+ class Batch
6
+ class UnrecognizedCommandError < ArgumentError; end
7
+
8
+ attr_reader :command_type, :tools, :output, :results
9
+
10
+ # Generates an instance of Batch for running multiple tools together
11
+ # @param command_type [Symbol] the type of command to run for each tool.
12
+ # @param tools [Array<Tool>] the tools to run the commands for
13
+ # @param output: Reviewer.output [Output] the output channel to print results to
14
+ #
15
+ # @return [self]
16
+ def initialize(command_type, tools, output: Reviewer.output)
17
+ @command_type = command_type
18
+ @tools = tools
19
+ @output = output
20
+ @results = {}
21
+ end
22
+
23
+ # Iterates over the tools in the batch to successfully run the commands. Also times the entire
24
+ # batch in order to provide a total execution time.
25
+ #
26
+ # @return [Results] the results summary for all commands run
27
+ def run
28
+ benchmark_batch do
29
+ matching_tools.each do |tool|
30
+ # Create and execute a runner for the given tool, command type, and strategy
31
+ runner = Runner.new(tool, command_type, strategy)
32
+ runner.run
33
+
34
+ # Record the exit status for this tool
35
+ record_exit_status(runner)
36
+
37
+ # If the tool fails, stop running other tools
38
+ break unless runner.success?
39
+ end
40
+ end
41
+
42
+ results
43
+ end
44
+
45
+ private
46
+
47
+ def multiple_tools?
48
+ tools.size > 1
49
+ end
50
+
51
+ def strategy
52
+ multiple_tools? ? Runner::Strategies::Captured : Runner::Strategies::Passthrough
53
+ end
54
+
55
+ # Notes the exit status for the runner based on whether the runner was considered successful or
56
+ # not based on the configured `max_exit_status` for the tool. For example, some tools use exit
57
+ # status to convey significance. So even though it returns a non-zero exit status like 2, it
58
+ # can still be successful.
59
+ # @param runner [Runner] the instance of the runner that's being inspected
60
+ #
61
+ # @return [Integer] the adjusted exit status for the runner
62
+ def record_exit_status(runner)
63
+ # Since some tools can "succeed" with a positive exit status, the overall batch is only
64
+ # interested in subjective failure. So if the runner succeeded according to the tool's max
65
+ # exit status, it should record the tool's run as a success for the purposes of the larger
66
+ # batch success/failure
67
+ @results[runner.tool.key] = runner.success? ? 0 : runner.exit_status
68
+ end
69
+
70
+ # Records and prints the total runtime of a block
71
+ # @param &block [type] section of code to be timed
72
+ #
73
+ # @return [void] prints the elapsed time
74
+ def benchmark_batch(&block)
75
+ elapsed_time = Benchmark.realtime(&block)
76
+
77
+ # If there's failures, skip showing the total time to focus on the issues
78
+ return if @results.values.sum.positive?
79
+
80
+ output.batch_summary(results.size, elapsed_time)
81
+ end
82
+
83
+ # Returns the set of tools matching the provided command. So when formatting, if a tool does not
84
+ # have a format command, then it will be skipped.
85
+ #
86
+ # @return [Array<Tool>] the enabled tools that support the provided command
87
+ def matching_tools
88
+ tools.select { |tool| tool.settings.commands.key?(command_type) }
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reviewer
4
+ class Command
5
+ class String
6
+ # Assembles tool environment variables into a single string or array
7
+ class Env
8
+ attr_reader :env_pairs
9
+
10
+ # Creates an instance of env variables for a tool to help generate the command string
11
+ # @param env_pairs [Hash] [description]
12
+ #
13
+ # @return [self]
14
+ def initialize(env_pairs)
15
+ @env_pairs = env_pairs
16
+ end
17
+
18
+ def to_s
19
+ to_a.compact.join(' ')
20
+ end
21
+
22
+ def to_a
23
+ env = []
24
+ env_pairs.each { |key, value| env << env(key, value) }
25
+ env
26
+ end
27
+
28
+ private
29
+
30
+ def env(key, value)
31
+ return nil if key.to_s.strip.empty? || value.to_s.strip.empty?
32
+
33
+ value = needs_quotes?(value) ? "'#{value}'" : value
34
+
35
+ "#{key.to_s.strip.upcase}=#{value.to_s.strip}"
36
+ end
37
+
38
+ def needs_quotes?(value)
39
+ value.is_a?(::String) && value.include?(' ')
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reviewer
4
+ class Command
5
+ class String
6
+ # Translates tool flag settings from the tool's configuration values into a single string or
7
+ # array that can be used to generate the command string
8
+ class Flags
9
+ attr_reader :flag_pairs
10
+
11
+ # Creates an instance of command-string friendly flags
12
+ # @param flag_pairs [Hash] the flags (keys) and their values
13
+ #
14
+ # @return [self]
15
+ def initialize(flag_pairs)
16
+ @flag_pairs = flag_pairs
17
+ end
18
+
19
+ # Creates a string-friendly format to use in a command
20
+ #
21
+ # @return [String] a string of flags that can be safely passed to a command
22
+ def to_s
23
+ to_a.join(' ')
24
+ end
25
+
26
+ # Creates an array of all flag name/value pairs
27
+ #
28
+ # @return [Array<String>] array of all flag strings to use to when running the command
29
+ def to_a
30
+ flags = []
31
+ flag_pairs.each { |key, value| flags << flag(key, value) }
32
+ flags
33
+ end
34
+
35
+ private
36
+
37
+ def flag(key, value)
38
+ dash = key.to_s.size == 1 ? '-' : '--'
39
+
40
+ value = needs_quotes?(value) ? "'#{value}'" : value
41
+
42
+ "#{dash}#{key} #{value}".strip
43
+ end
44
+
45
+ def needs_quotes?(value)
46
+ value.is_a?(::String) && value.include?(' ')
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'string/env'
4
+ require_relative 'string/flags'
5
+
6
+ module Reviewer
7
+ class Command
8
+ # Assembles tool tool_settings into a usable command string for the command type
9
+ class String
10
+ include Conversions
11
+
12
+ attr_reader :command_type, :tool_settings
13
+
14
+ def initialize(command_type, tool_settings:)
15
+ @command_type = command_type
16
+ @tool_settings = tool_settings
17
+ end
18
+
19
+ def to_s
20
+ to_a
21
+ .map(&:strip) # Remove extra spaces on the components
22
+ .join(' ') # Merge the components
23
+ .strip # Strip extra spaces from the end result
24
+ end
25
+
26
+ def to_a
27
+ [
28
+ env_variables,
29
+ body,
30
+ flags
31
+ ].compact
32
+ end
33
+
34
+ # The string of environment variables built from a tool's configuration settings
35
+ #
36
+ # @return [String] the environment variable names and values concatened for the command
37
+ def env_variables
38
+ Env.new(tool_settings.env).to_s
39
+ end
40
+
41
+ def body
42
+ tool_settings.commands.fetch(command_type)
43
+ end
44
+
45
+ # Gets the flags to be used in conjunction with the review command for a tool
46
+ # 1. The `review` commands are the only commands that use flags
47
+ # 2. If no flags are configured, this won't do anything
48
+ #
49
+ # @return [String] the concatenated list of flags to pass to the review command
50
+ def flags
51
+ return nil unless flags?
52
+
53
+ Flags.new(tool_settings.flags).to_s
54
+ end
55
+
56
+ private
57
+
58
+ # Determines whether the string needs flags added
59
+ #
60
+ # @return [Boolean] true if it's a review command and it has flags configured
61
+ def flags?
62
+ command_type == :review && tool_settings.flags.any?
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'command/string'
4
+
5
+ module Reviewer
6
+ # The core funtionality to translate a tool, command type, and verbosity into a runnable command
7
+ class Command
8
+ include Conversions
9
+
10
+ SEED_SUBSTITUTION_VALUE = '$SEED'
11
+
12
+ attr_reader :tool
13
+
14
+ attr_accessor :type
15
+
16
+ # Creates an instance of the Command class to synthesize a command string using the tool,
17
+ # command type, and verbosity.
18
+ # @param tool [Tool, Symbol] a tool or tool key to use to look up the command and options
19
+ # @param type [Symbol] the desired command type (:install, :prepare, :review, :format)
20
+ #
21
+ # @return [Command] the intersection of a tool, command type, and verbosity
22
+ def initialize(tool, type)
23
+ @tool = Tool(tool)
24
+ @type = type.to_sym
25
+ end
26
+
27
+ # The final command string with all of the conditions appled
28
+ #
29
+ # @return [String] the final, valid command string to run
30
+ def string
31
+ @string ||= seed_substitution? ? seeded_string : raw_string
32
+ end
33
+ alias to_s string
34
+
35
+ # Generates a seed that can be re-used across runs so that the results are consistent across
36
+ # related runs for tools that would otherwise change the seed automatically every run.
37
+ # Since not all tools will use the seed, there's no need to generate it in the initializer.
38
+ # Instead, it's memoized if it's used.
39
+ #
40
+ # @return [Integer] a random integer to pass to tools that use seeds
41
+ def seed
42
+ @seed ||= Random.rand(100_000)
43
+
44
+ # Store the seed for reference
45
+ Reviewer.history.set(tool.key, :last_seed, @seed)
46
+
47
+ @seed
48
+ end
49
+
50
+ # The raw command string before any substitutions. For example, since seeds need to remain
51
+ # consistent from one run to the next, they're
52
+ #
53
+ # @return [type] [description]
54
+ def raw_string
55
+ @raw_string ||= String.new(type, tool_settings: tool.settings).to_s
56
+ end
57
+
58
+ private
59
+
60
+ # The version of the command with the SEED_SUBSTITUTION_VALUE replaced
61
+ #
62
+ # @return [String] the command string with the SEED_SUBSTITUTION_VALUE replaced
63
+ def seeded_string
64
+ # Update the string with the memoized seed value
65
+ raw_string.gsub(SEED_SUBSTITUTION_VALUE, seed.to_s)
66
+ end
67
+
68
+ # Determines if the raw command string has a SEED_SUBSTITUTION_VALUE that needs replacing
69
+ #
70
+ # @return [Boolean] true if the raw command string contains the SEED_SUBSTITUTION_VALUE
71
+ def seed_substitution?
72
+ raw_string.include?(SEED_SUBSTITUTION_VALUE)
73
+ end
74
+ end
75
+ end
@@ -1,18 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pathname'
4
+
3
5
  module Reviewer
6
+ # Configuration values container for Reviewer
7
+ #
8
+ # @!attribute file
9
+ # @return [Pathname] the pathname for the primary configuraton file
10
+ # @!attribute history_file
11
+ # @return [Pathname] the pathname for the history file to store data across runs
12
+ #
13
+ # @author [garrettdimon]
14
+ #
4
15
  class Configuration
5
- DEFAULT_CONFIGURATION_PATH = Dir.pwd.freeze
6
- DEFAULT_CONFIGURATION_FILE = '.reviewer.yml'.freeze
16
+ DEFAULT_PATH = Dir.pwd.freeze
17
+
18
+ DEFAULT_CONFIG_FILE_NAME = '.reviewer.yml'
19
+ DEFAULT_HISTORY_FILE_NAME = '.reviewer_history.yml'
7
20
 
8
- attr_accessor :file
21
+ DEFAULT_CONFIG_LOCATION = "#{DEFAULT_PATH}/#{DEFAULT_CONFIG_FILE_NAME}"
22
+ DEFAULT_HISTORY_LOCATION = "#{DEFAULT_PATH}/#{DEFAULT_HISTORY_FILE_NAME}"
23
+
24
+ attr_accessor :file, :history_file, :printer
9
25
 
10
26
  def initialize
11
- @file = "#{DEFAULT_CONFIGURATION_PATH}/#{DEFAULT_CONFIGURATION_FILE}"
12
- end
27
+ @file = Pathname(DEFAULT_CONFIG_LOCATION)
28
+ @history_file = Pathname(DEFAULT_HISTORY_LOCATION)
13
29
 
14
- def tools
15
- @tools ||= Loader.new.to_h
30
+ # Future Configuration Options:
31
+ # - seed_substitution_value(string): Currently a constant of `$SEED` in Reviewer::Command, but
32
+ # may need to be configurable in case any command-line strings have other legitimate uses
33
+ # for the value such that it may need to be override. Ideally, it woudl be changed to3
34
+ # something obscure enough that conflicts wouldn't happen, but you never know
35
+ # - benchmark_everything(:dev, :optimize): Use the `time_up` gem to measure and show all the results
36
+ # for each tool and step to help identify and reduce bottlenecks. It would mainly be a flag
37
+ # for use in development, but it could also help folks troubleshoot their speed in finer
38
+ # detail than the standard Reviewer output
39
+ # - default_preparation_refresh(integer time): Right now, it's hard-coded at 6 hours, but that may require
40
+ # tuning for individual tools
16
41
  end
17
42
  end
18
43
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reviewer
4
+ # Conversion functions for special types in Reviewer
5
+ module Conversions
6
+ def Tool(value) # rubocop:disable Naming/MethodName
7
+ case value
8
+ when Tool then value
9
+ when Symbol then Tool.new(value)
10
+ when String then Tool.new(value.to_sym)
11
+ else raise TypeError, "Cannot convert #{value} to Tool"
12
+ end
13
+ end
14
+ module_function :Tool
15
+ end
16
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml/store'
4
+
5
+ module Reviewer
6
+ # Handles the logic around what to display after a command has been run
7
+ class Guidance
8
+ attr_reader :command, :result, :output
9
+
10
+ # Create an instance of guidance for suggesting recovery steps after errors
11
+ # @param command: [Command] the command that was run and needs recovery guidance
12
+ # @param result: [Result] the result of the command
13
+ # @param output: Reviewer.output [Output] the output channel for displaying content
14
+ #
15
+ # @return [Guidance] the guidance class to suggest relevant recovery steps
16
+ def initialize(command:, result:, output: Reviewer.output)
17
+ @command = command
18
+ @result = result
19
+ @output = output
20
+ end
21
+
22
+ # Prints the relevant guidance based on the command and result context
23
+ #
24
+ # @return [void] prints the relevant guidance to the stream
25
+ def show
26
+ case result
27
+ when executable_not_found? then show_missing_executable_guidance
28
+ when cannot_execute? then show_unrecoverable_guidance
29
+ else show_syntax_guidance
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ # Conditional check for when the command result was that the executable couldn't be found
36
+ #
37
+ # @return [Boolean] true if the result indicates the command couldn't be found
38
+ def executable_not_found?
39
+ ->(result) { result.executable_not_found? }
40
+ end
41
+
42
+ # Conditional check for when the command result was that it was unable to be executed
43
+ #
44
+ # @return [Boolean] true if the result indicates the command couldn't be executed
45
+ def cannot_execute?
46
+ ->(result) { result.cannot_execute? }
47
+ end
48
+
49
+ # Shows the recovery guidance for when a command is missing
50
+ #
51
+ # @return [void] prints missing executable guidance
52
+ def show_missing_executable_guidance
53
+ tool = command.tool
54
+ installation_command = Command.new(tool, :install).string if tool.installable?
55
+ install_link = tool.install_link
56
+
57
+ output.failure("Missing executable for '#{tool}'", command: command)
58
+ output.guidance('Try installing the tool:', installation_command)
59
+ output.guidance('Read the installation guidance:', install_link)
60
+ end
61
+
62
+ # Shows the recovery guidance for when a command generates an unrecoverable error
63
+ #
64
+ # @return [void] prints unrecoverable error guidance
65
+ def show_unrecoverable_guidance
66
+ output.unrecoverable(result.stderr)
67
+ end
68
+
69
+ # Shows suggestions for ignoring or disable rules when a command fails after reviewing code
70
+ #
71
+ # @return [void] prints syntax guidance
72
+ def show_syntax_guidance
73
+ output.guidance('Selectively Ignore a Rule:', command.tool.links[:ignore_syntax])
74
+ output.guidance('Fully Disable a Rule:', command.tool.links[:disable_syntax])
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml/store'
4
+
5
+ module Reviewer
6
+ # Provides an interface to a local storage resource for persisting data across runs. For example,
7
+ # it enables remembering when `prepare` commands were run for reviews so they can be run less
8
+ # frequently and thus improve performance.
9
+ #
10
+ # It also enables remembering seeds across runs. Eventually `rvw rerun` could reuse the seeds from
11
+ # the immediately preceding run to more easily facilitate fixing tests that are accidentally
12
+ # order-dependent. Or it could automatically record a list of seeds that led to failures.
13
+ #
14
+ # Long term, it could serve to record timing details across runs to provide insight to min, max,
15
+ # and means. Those times could then be used for reviewer to make more informed decisions about
16
+ # default behavior to ensure each run remains fast.
17
+ class History
18
+ attr_reader :file, :store
19
+
20
+ # Creates an instance of a YAML::Store-backed history file.
21
+ # @param file = Reviewer.configuration.history_file [Pathname] the history file to store data
22
+ #
23
+ # @return [History] an instance of history
24
+ def initialize(file = Reviewer.configuration.history_file)
25
+ @file = file
26
+ @store = YAML::Store.new(file)
27
+ end
28
+
29
+ # Saves a value to a given location in the history
30
+ # @param group [Symbol] the first-level key to use for saving the value--frequently a tool name
31
+ # @param attribute [Symbol] the second-level key to use for retrieving the value
32
+ # @param value [Primitive] any value that can be cleanly stored in YAML
33
+ #
34
+ # @return [Primitive] the value being stored
35
+ def set(group, attribute, value)
36
+ store.transaction do |s|
37
+ s[group] = {} if s[group].nil?
38
+ s[group][attribute] = value
39
+ end
40
+ end
41
+
42
+ # Retrieves a stored value from the history file
43
+ # @param group [Symbol] the first-level key to use for retrieving the value
44
+ # @param attribute [Symbol] the second-level key to use for retrieving the value
45
+ #
46
+ # @return [Primitive] the value being stored
47
+ def get(group, attribute)
48
+ store.transaction do |s|
49
+ s[group].nil? ? nil : s[group][attribute]
50
+ end
51
+ end
52
+
53
+ # Removes the existing history file.
54
+ #
55
+ # @return [void]
56
+ def reset!
57
+ return unless File.exist?(file)
58
+
59
+ FileUtils.rm(file)
60
+ end
61
+
62
+ # Convenience class method for removing the history file.
63
+ #
64
+ # @return [void]
65
+ def self.reset!
66
+ new.reset!
67
+ end
68
+ end
69
+ end