quiet_quality 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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