quiet_quality 0.1.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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/dogfood.yml +28 -0
  3. data/.github/workflows/linters.yml +31 -0
  4. data/.github/workflows/rspec.yml +33 -0
  5. data/.gitignore +6 -0
  6. data/.rspec +1 -0
  7. data/.rubocop.yml +21 -0
  8. data/Gemfile +3 -0
  9. data/LICENSE +20 -0
  10. data/README.md +34 -0
  11. data/bin/qq +35 -0
  12. data/lib/quiet_quality/annotation_locator.rb +33 -0
  13. data/lib/quiet_quality/annotators/github_stdout.rb +36 -0
  14. data/lib/quiet_quality/annotators.rb +19 -0
  15. data/lib/quiet_quality/changed_file.rb +40 -0
  16. data/lib/quiet_quality/changed_files.rb +35 -0
  17. data/lib/quiet_quality/cli/option_parser.rb +103 -0
  18. data/lib/quiet_quality/cli/options.rb +27 -0
  19. data/lib/quiet_quality/cli/options_builder.rb +49 -0
  20. data/lib/quiet_quality/cli.rb +12 -0
  21. data/lib/quiet_quality/executors/base_executor.rb +46 -0
  22. data/lib/quiet_quality/executors/concurrent_executor.rb +20 -0
  23. data/lib/quiet_quality/executors/pipeline.rb +59 -0
  24. data/lib/quiet_quality/executors/serial_executor.rb +15 -0
  25. data/lib/quiet_quality/executors.rb +17 -0
  26. data/lib/quiet_quality/message.rb +25 -0
  27. data/lib/quiet_quality/message_filter.rb +33 -0
  28. data/lib/quiet_quality/messages.rb +54 -0
  29. data/lib/quiet_quality/tool_options.rb +32 -0
  30. data/lib/quiet_quality/tools/outcome.rb +26 -0
  31. data/lib/quiet_quality/tools/rspec/parser.rb +45 -0
  32. data/lib/quiet_quality/tools/rspec/runner.rb +57 -0
  33. data/lib/quiet_quality/tools/rspec.rb +11 -0
  34. data/lib/quiet_quality/tools/rubocop/parser.rb +46 -0
  35. data/lib/quiet_quality/tools/rubocop/runner.rb +67 -0
  36. data/lib/quiet_quality/tools/rubocop.rb +11 -0
  37. data/lib/quiet_quality/tools/standardrb/parser.rb +8 -0
  38. data/lib/quiet_quality/tools/standardrb/runner.rb +11 -0
  39. data/lib/quiet_quality/tools/standardrb.rb +13 -0
  40. data/lib/quiet_quality/tools.rb +21 -0
  41. data/lib/quiet_quality/version.rb +3 -0
  42. data/lib/quiet_quality/version_control_systems/git.rb +117 -0
  43. data/lib/quiet_quality/version_control_systems.rb +8 -0
  44. data/lib/quiet_quality.rb +15 -0
  45. data/quiet_quality.gemspec +44 -0
  46. data/tmp/.gitkeep +0 -0
  47. metadata +219 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f9c943644f5453e92b2b839bf42f8f031f67b13d6c8385c89822b17b02dccf60
4
+ data.tar.gz: 780325cd5db5fdac051880d404756887e60882929503e76876f178945d167f92
5
+ SHA512:
6
+ metadata.gz: 3afd25c6af0810f3c6195ba0947173be2b926e1d689d7e095458c18e97e445a40935ce62cb730195a0b33a93f57568d4328a4260651183df08a7cac41b5cb8eb
7
+ data.tar.gz: c345f28884539737a9d37b1aaa8480ee06957c43d6b83b8eaa7d302855285f3f098f8f2b110b880da54cfdec7e4105a81a9701358494e92b9425849fc7231576
@@ -0,0 +1,28 @@
1
+ name: QuietQuality Itself
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ QuietQuality:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v3
10
+
11
+ - name: Set up ruby
12
+ uses: ruby/setup-ruby@v1
13
+ with:
14
+ ruby-version: 3.2
15
+
16
+ - name: Cache gems
17
+ uses: actions/cache@v3
18
+ with:
19
+ path: vendor/bundle
20
+ key: ${{ runner.os }}-dogfood-${{ hashFiles('Gemfile.lock') }}
21
+ restore-keys:
22
+ ${{ runner.os }}-dogfood-
23
+
24
+ - name: Install gems
25
+ run: bundle install --jobs 4 --retry 3
26
+
27
+ - name: Run QuietQuality
28
+ run: bundle exec bin/qq standardrb rubocop rspec --all-files --unfiltered --annotate github_stdout
@@ -0,0 +1,31 @@
1
+ name: Linters
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ StandardRB:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v3
10
+
11
+ - name: Set up ruby
12
+ uses: ruby/setup-ruby@v1
13
+ with:
14
+ ruby-version: 3.2
15
+
16
+ - name: Cache gems
17
+ uses: actions/cache@v3
18
+ with:
19
+ path: vendor/bundle
20
+ key: ${{ runner.os }}-linters-${{ hashFiles('Gemfile.lock') }}
21
+ restore-keys:
22
+ ${{ runner.os }}-linters-
23
+
24
+ - name: Install gems
25
+ run: bundle install --jobs 4 --retry 3
26
+
27
+ - name: Run standard
28
+ run: bundle exec standardrb
29
+
30
+ - name: Run rubocop (complexity checks)
31
+ run: bundle exec rubocop --parallel
@@ -0,0 +1,33 @@
1
+ name: RSpec
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ RSpec:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ fail-fast: false
10
+ matrix:
11
+ ruby-version: ['2.7', '3.0', '3.1', '3.2', 'head']
12
+
13
+ steps:
14
+ - uses: actions/checkout@v3
15
+
16
+ - name: Set up ruby
17
+ uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: ${{ matrix.ruby-version }}
20
+
21
+ - name: Cache gems
22
+ uses: actions/cache@v3
23
+ with:
24
+ path: vendor/bundle
25
+ key: ${{ runner.os }}-rspec-${{ matrix.ruby-version }}-${{ hashFiles('Gemfile.lock') }}
26
+ restore-keys:
27
+ ${{ runner.os }}-rspec-${{ matrix.ruby-version }}-
28
+
29
+ - name: Install gems
30
+ run: bundle install --jobs 4 --retry 3
31
+
32
+ - name: Run RSpec
33
+ run: bundle exec rspec
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ .ruby-version
2
+ .ruby-gemset
3
+ Gemfile.lock
4
+ *.gem
5
+ coverage/
6
+ tmp/*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,21 @@
1
+ ---
2
+ AllCops:
3
+ SuggestExtensions: false
4
+ DisabledByDefault: true
5
+
6
+ Metrics/AbcSize:
7
+ Max: 15
8
+ Metrics/CyclomaticComplexity:
9
+ Max: 8
10
+ Metrics/PerceivedComplexity:
11
+ Max: 7
12
+
13
+ Metrics/ClassLength:
14
+ CountComments: false
15
+ Max: 150
16
+ Metrics/MethodLength:
17
+ CountComments: false
18
+ Max: 15
19
+ Metrics/ParameterLists:
20
+ Max: 5
21
+ CountKeywordArgs: true
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2023 Eric Mueller
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # QuietQuality
2
+
3
+ Work in Progress
4
+
5
+ But essentially, QuietQuality is intended for two purposes:
6
+
7
+ 1. Let you conveniently run tools like rubocop, rspec, and standard against the _relevant_
8
+ files locally (the files that have changed locally relative to the default branch)
9
+ 2. Let you run those tools in CI (probably github actions) and annotate any issues found
10
+ with _new or modified_ code, without bothering you about existing issues that you didn't
11
+ touch.
12
+
13
+
14
+ Basic usage examples:
15
+
16
+ ```
17
+ # you have five commits in your feature branch and 3 more files changes but not committed.
18
+ # this will run rubocop against all of those files.
19
+ qq rubocop
20
+
21
+ # run rspec against the changed specs, and annotate any failing specs (well, the first 10
22
+ # of them) against the commit using github's inline output-based annotation approach. Which
23
+ # will of course only produce actual annotations if this happens to have been run in a
24
+ # github action.
25
+ qq rspec --annotate=stdout
26
+
27
+ # run standardrb against all of the files (not just the changed ones). Still only print out
28
+ # problems to lines that have changed, so not particularly useful :-)
29
+ qq standard --all --incremental
30
+
31
+ # run all of the tools against the entire repository, and print the first three messages
32
+ # out for each tool.
33
+ qq all --all --full --limit-per-tool=3
34
+ ```
data/bin/qq ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative "../lib/quiet_quality"
3
+
4
+ opt_parser = QuietQuality::Cli::OptionParser.new(ARGV)
5
+ tool_names, global_options, tool_options = opt_parser.parse!
6
+ options = QuietQuality::Cli::OptionsBuilder.new(tool_names: tool_names, global_options: global_options, tool_options: tool_options).options
7
+
8
+ executor = options.executor.new(tools: options.tools)
9
+ executor.execute!
10
+
11
+ executor.outcomes.each do |outcome|
12
+ result = outcome.success? ? "Passed" : "Failed"
13
+ warn "--- #{result}: #{outcome.tool}"
14
+ end
15
+
16
+ messages = executor.messages
17
+ if messages.any?
18
+ warn "\n\n#{messages.count} messages:"
19
+ messages.each do |msg|
20
+ line_range = msg.start_line == msg.stop_line ? msg.start_line.to_s : "#{msg.start_line}-#{msg.stop_line}"
21
+ body = msg.body.gsub(/ *\n */, "\\n").slice(0, 120)
22
+ warn " #{msg.path}:#{line_range} #{msg.rule}"
23
+ warn " #{body}"
24
+ end
25
+ end
26
+
27
+ if options.annotator
28
+ warn "\n\n"
29
+ options.annotator.new.annotate!(messages)
30
+ end
31
+
32
+ if executor.any_failure?
33
+ warn "failures detected in one or more tools"
34
+ exit(1)
35
+ end
@@ -0,0 +1,33 @@
1
+ module QuietQuality
2
+ class AnnotationLocator
3
+ def initialize(changed_files:)
4
+ @changed_files = changed_files
5
+ end
6
+
7
+ def update!(message)
8
+ changed_file = changed_files.file(message.path)
9
+ message.annotated_line = changed_file ? file_line_for(message, changed_file) : nil
10
+ end
11
+
12
+ def update_all!(messages)
13
+ messages.map { |m| update!(m) }.compact.length
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :changed_files
19
+
20
+ def file_line_for(message, changed_file)
21
+ return message.stop_line if changed_file.entire?
22
+ message_range = (message.start_line..message.stop_line)
23
+ last_match(changed_file.line_numbers, message_range)
24
+ end
25
+
26
+ def last_match(array, range)
27
+ array.reverse_each do |value|
28
+ return value if range.cover?(value)
29
+ end
30
+ nil
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ module QuietQuality
2
+ module Annotators
3
+ class GithubStdout
4
+ # github will only accept the first 10 annotations of each type in this form.
5
+ MAX_ANNOTATIONS = 10
6
+
7
+ def initialize(output_stream: $stdout)
8
+ @output_stream = output_stream
9
+ end
10
+
11
+ def annotate!(messages)
12
+ messages.first(MAX_ANNOTATIONS).each do |message|
13
+ output_stream.puts self.class.format(message)
14
+ end
15
+ end
16
+
17
+ # Example annotation output from github's docs:
18
+ # ::warning file={name},line={line},title={title}::{message}
19
+ # See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-warning-message
20
+ def self.format(message)
21
+ attributes = {
22
+ file: message.path,
23
+ line: message.annotated_line || message.start_line,
24
+ title: message.rule
25
+ }.compact
26
+
27
+ attributes_string = attributes.map { |k, v| "#{k}=#{v}" }.join(",")
28
+ "::warning #{attributes_string}::#{message.body}"
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :output_stream
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,19 @@
1
+ glob = File.expand_path("../annotators/*.rb", __FILE__)
2
+ Dir.glob(glob).sort.each { |f| require(f) }
3
+
4
+ module QuietQuality
5
+ module Annotators
6
+ Unrecognized = Class.new(Error)
7
+
8
+ ANNOTATOR_TYPES = {
9
+ github_stdout: Annotators::GithubStdout
10
+ }.freeze
11
+
12
+ def self.annotate!(annotator:, messages:, limit: nil)
13
+ limited_messages = limit ? messages.first(limit) : messages
14
+ ANNOTATOR_TYPES.fetch(annotator.to_sym).new.annotate!(limited_messages)
15
+ rescue KeyError
16
+ fail Unrecognized, "Unrecognized annotator_type '#{annotator}'"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,40 @@
1
+ module QuietQuality
2
+ class ChangedFile
3
+ attr_reader :path
4
+
5
+ def initialize(path:, lines:)
6
+ @path = path
7
+
8
+ if lines == :all || lines == "all"
9
+ @entire = true
10
+ @lines = nil
11
+ else
12
+ @entire = false
13
+ @lines = lines
14
+ end
15
+ end
16
+
17
+ def entire?
18
+ @entire
19
+ end
20
+
21
+ def lines
22
+ return nil if @lines.nil?
23
+ @_lines ||= @lines.to_set
24
+ end
25
+
26
+ def line_numbers
27
+ return nil if @lines.nil?
28
+ @_line_numbers ||= @lines.sort
29
+ end
30
+
31
+ def merge(other)
32
+ if path != other.path
33
+ fail ArgumentError, "Cannot merge ChangedFiles '#{path}' and '#{other.path}', they're different files"
34
+ end
35
+
36
+ new_lines = (entire? || other.entire?) ? :all : (lines + other.lines).to_a
37
+ self.class.new(path: path, lines: new_lines)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,35 @@
1
+ module QuietQuality
2
+ class ChangedFiles
3
+ attr_reader :files
4
+
5
+ def initialize(files)
6
+ @files = files
7
+ end
8
+
9
+ def paths
10
+ @_paths ||= files.map(&:path)
11
+ end
12
+
13
+ def file(path)
14
+ files_by_path.fetch(path, nil)
15
+ end
16
+
17
+ def include?(path)
18
+ files_by_path.include?(path)
19
+ end
20
+
21
+ def merge(other)
22
+ merged_files = []
23
+ (files + other.files)
24
+ .group_by(&:path)
25
+ .each_pair { |_path, pfiles| merged_files << pfiles.reduce(&:merge) }
26
+ self.class.new(merged_files)
27
+ end
28
+
29
+ private
30
+
31
+ def files_by_path
32
+ @_files_by_path ||= files.map { |f| [f.path, f] }.to_h
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,103 @@
1
+ require "optparse"
2
+
3
+ module QuietQuality
4
+ module Cli
5
+ class OptionParser
6
+ attr_reader :options, :tool_options, :output
7
+
8
+ def initialize(args)
9
+ @args = args
10
+ @options = {
11
+ executor: :concurrent
12
+ }
13
+ @tool_options = {}
14
+ @output = nil
15
+ end
16
+
17
+ def parse!
18
+ parser.parse!(@args)
19
+ [positional, options, tool_options]
20
+ end
21
+
22
+ def positional
23
+ @args
24
+ end
25
+
26
+ private
27
+
28
+ def parser
29
+ ::OptionParser.new do |parser|
30
+ setup_banner(parser)
31
+ setup_help_output(parser)
32
+ setup_executor_options(parser)
33
+ setup_annotation_options(parser)
34
+ setup_file_target_options(parser)
35
+ setup_filter_messages_options(parser)
36
+ end
37
+ end
38
+
39
+ def setup_banner(parser)
40
+ parser.banner = "Usage: qq [TOOLS] [GLOBAL_OPTIONS] [TOOL_OPTIONS]"
41
+ end
42
+
43
+ def setup_help_output(parser)
44
+ parser.on("-h", "--help", "Prints this help") do
45
+ @output = parser.to_s
46
+ @options[:exit_immediately] = true
47
+ end
48
+ end
49
+
50
+ def setup_executor_options(parser)
51
+ parser.on("-E", "--executor EXECUTOR", "Which executor to use") do |name|
52
+ fail(UsageError, "Executor not recognized: #{name}") unless Executors::AVAILABLE.include?(name.to_sym)
53
+ @options[:executor] = name.to_sym
54
+ end
55
+ end
56
+
57
+ def setup_annotation_options(parser)
58
+ parser.on("-A", "--annotate ANNOTATOR", "Annotate with this annotator") do |name|
59
+ fail(UsageError, "Annotator not recognized: #{name}") unless Annotators::ANNOTATOR_TYPES.include?(name.to_sym)
60
+ @options[:annotator] = name.to_sym
61
+ end
62
+
63
+ # shortcut option
64
+ parser.on("-G", "--annotate-github-stdout", "Annotate with GitHub Workflow commands") do
65
+ @options[:annotator] = :github_stdout
66
+ end
67
+ end
68
+
69
+ def read_tool_or_global_option(name, tool, value)
70
+ if tool
71
+ @tool_options[tool.to_sym] ||= {}
72
+ @tool_options[tool.to_sym][name] = value
73
+ else
74
+ @options[name] = value
75
+ end
76
+ end
77
+
78
+ def setup_file_target_options(parser)
79
+ parser.on("-a", "--all-files [tool]", "Use the tool(s) on all files") do |tool|
80
+ read_tool_or_global_option(:all_files, tool, true)
81
+ end
82
+
83
+ parser.on("-c", "--changed-files [tool]", "Use the tool(s) only on changed files") do |tool|
84
+ read_tool_or_global_option(:all_files, tool, false)
85
+ end
86
+
87
+ parser.on("-B", "--comparison-branch BRANCH", "Specify the branch to compare against") do |branch|
88
+ @options[:comparison_branch] = branch
89
+ end
90
+ end
91
+
92
+ def setup_filter_messages_options(parser)
93
+ parser.on("-f", "--filter-messages [tool]", "Filter messages from tool(s) based on changed lines") do |tool|
94
+ read_tool_or_global_option(:filter_messages, tool, true)
95
+ end
96
+
97
+ parser.on("-u", "--unfiltered [tool]", "Don't filter messages from tool(s)") do |tool|
98
+ read_tool_or_global_option(:filter_messages, tool, false)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,27 @@
1
+ module QuietQuality
2
+ module Cli
3
+ class Options
4
+ def initialize
5
+ @annotator = nil
6
+ @executor = Executors::ConcurrentExecutor
7
+ @tools = nil
8
+ @comparison_branch = nil
9
+ end
10
+
11
+ attr_reader :annotator, :executor
12
+ attr_accessor :tools, :comparison_branch
13
+
14
+ def annotator=(name)
15
+ @annotator = Annotators::ANNOTATOR_TYPES.fetch(name.to_sym)
16
+ rescue KeyError
17
+ fail(UsageError, "Unrecognized annotator: #{name}")
18
+ end
19
+
20
+ def executor=(name)
21
+ @executor = Executors::AVAILABLE.fetch(name.to_sym)
22
+ rescue KeyError
23
+ fail(UsageError, "Unrecognized executor: #{name}")
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ module QuietQuality
2
+ module Cli
3
+ class OptionsBuilder
4
+ def initialize(tool_names:, global_options:, tool_options:)
5
+ @raw_tool_names = tool_names
6
+ @raw_global_options = global_options
7
+ @raw_tool_options = tool_options
8
+ end
9
+
10
+ def options
11
+ return @_options if defined?(@_options)
12
+ options = Options.new
13
+ set_unless_nil(options, :annotator, @raw_global_options[:annotator])
14
+ set_unless_nil(options, :executor, @raw_global_options[:executor])
15
+ set_unless_nil(options, :comparison_branch, @raw_global_options[:comparison_branch])
16
+ options.tools = tool_names.map { |tool_name| tool_options_for(tool_name) }
17
+ @_options = options
18
+ end
19
+
20
+ private
21
+
22
+ def set_unless_nil(object, method, value)
23
+ return if value.nil?
24
+ object.send("#{method}=", value)
25
+ end
26
+
27
+ def tool_options_for(tool_name)
28
+ raw_tool_opts = @raw_tool_options.fetch(tool_name.to_sym, {})
29
+ ToolOptions.new(tool_name).tap do |tool_options|
30
+ set_unless_nil(tool_options, :limit_targets, @raw_global_options[:limit_targets])
31
+ set_unless_nil(tool_options, :limit_targets, raw_tool_opts[:limit_targets])
32
+
33
+ set_unless_nil(tool_options, :filter_messages, @raw_global_options[:filter_messages])
34
+ set_unless_nil(tool_options, :filter_messages, raw_tool_opts[:filter_messages])
35
+ end
36
+ end
37
+
38
+ def tool_names
39
+ names = @raw_tool_names.empty? ? Tools::AVAILABLE.keys : @raw_tool_names
40
+ names.map(&:to_sym).tap do |names|
41
+ unexpected_names = names - Tools::AVAILABLE.keys
42
+ if unexpected_names.any?
43
+ fail(UsageError, "Tool(s) not recognized: #{unexpected_names.join(", ")}")
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,12 @@
1
+ require_relative "./annotators"
2
+ require_relative "./tools"
3
+
4
+ module QuietQuality
5
+ module Cli
6
+ Error = Class.new(QuietQuality::Error)
7
+ UsageError = Class.new(Error)
8
+ end
9
+ end
10
+
11
+ glob = File.expand_path("../cli/*.rb", __FILE__)
12
+ Dir.glob(glob).sort.each { |f| require f }
@@ -0,0 +1,46 @@
1
+ module QuietQuality
2
+ module Executors
3
+ class BaseExecutor
4
+ def initialize(tools:, changed_files: nil)
5
+ @tools = tools
6
+ @changed_files = changed_files
7
+ end
8
+
9
+ def execute!
10
+ fail NoMethodError, "execute! should be implemented by the subclass of BaseExecutor"
11
+ end
12
+
13
+ def outcomes
14
+ @_outcomes ||= pipelines.map(&:outcome)
15
+ end
16
+
17
+ def messages
18
+ @_messages ||= Messages.new(pipelines.map(&:messages).map(&:all).reduce(&:+))
19
+ end
20
+
21
+ def any_failure?
22
+ pipelines.any?(&:failure?)
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :tools, :changed_files
28
+
29
+ def pipelines
30
+ @_pipelines ||= tools.map do |topts|
31
+ Pipeline.new(tool_options: topts, changed_files: changed_files)
32
+ end
33
+ end
34
+
35
+ def pipeline_by_tool
36
+ @_pipeline_by_tool ||= pipelines
37
+ .map { |p| [p.tool_name, p] }
38
+ .to_h
39
+ end
40
+
41
+ def pipeline_for(tool)
42
+ pipeline_by_tool.fetch(tool.to_sym)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ module QuietQuality
2
+ module Executors
3
+ class ConcurrentExecutor < BaseExecutor
4
+ def execute!
5
+ threads = pipelines.map { |pipeline| threaded_pipeline(pipeline) }
6
+ threads.each(&:join)
7
+ pipelines.none?(&:failure?)
8
+ end
9
+
10
+ private
11
+
12
+ def threaded_pipeline(pipeline)
13
+ Thread.new do
14
+ pipeline.outcome
15
+ pipeline.messages
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end