cuprum-cli 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 (65) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +34 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE +21 -0
  5. data/README.md +163 -0
  6. data/lib/cuprum/cli/argument.rb +172 -0
  7. data/lib/cuprum/cli/arguments/class_methods.rb +283 -0
  8. data/lib/cuprum/cli/arguments.rb +16 -0
  9. data/lib/cuprum/cli/coercion.rb +131 -0
  10. data/lib/cuprum/cli/command.rb +102 -0
  11. data/lib/cuprum/cli/commands/ci/report.rb +121 -0
  12. data/lib/cuprum/cli/commands/ci/rspec_command.rb +108 -0
  13. data/lib/cuprum/cli/commands/ci/rspec_each_command.rb +185 -0
  14. data/lib/cuprum/cli/commands/ci.rb +12 -0
  15. data/lib/cuprum/cli/commands/echo_command.rb +76 -0
  16. data/lib/cuprum/cli/commands/file/generate_file.rb +141 -0
  17. data/lib/cuprum/cli/commands/file/new_command.rb +86 -0
  18. data/lib/cuprum/cli/commands/file/render_erb.rb +88 -0
  19. data/lib/cuprum/cli/commands/file/resolve_template.rb +136 -0
  20. data/lib/cuprum/cli/commands/file/templates/rspec.rb.erb +14 -0
  21. data/lib/cuprum/cli/commands/file/templates/ruby.rb.erb +29 -0
  22. data/lib/cuprum/cli/commands/file/templates.rb +71 -0
  23. data/lib/cuprum/cli/commands/file.rb +14 -0
  24. data/lib/cuprum/cli/commands.rb +12 -0
  25. data/lib/cuprum/cli/dependencies/file_system/mock.rb +297 -0
  26. data/lib/cuprum/cli/dependencies/file_system.rb +247 -0
  27. data/lib/cuprum/cli/dependencies/standard_io/helpers.rb +138 -0
  28. data/lib/cuprum/cli/dependencies/standard_io/mock.rb +85 -0
  29. data/lib/cuprum/cli/dependencies/standard_io.rb +110 -0
  30. data/lib/cuprum/cli/dependencies/system_command/mock.rb +57 -0
  31. data/lib/cuprum/cli/dependencies/system_command.rb +147 -0
  32. data/lib/cuprum/cli/dependencies.rb +25 -0
  33. data/lib/cuprum/cli/errors/files/file_not_writeable.rb +42 -0
  34. data/lib/cuprum/cli/errors/files/missing_parameter.rb +71 -0
  35. data/lib/cuprum/cli/errors/files/missing_template.rb +36 -0
  36. data/lib/cuprum/cli/errors/files/template_error.rb +37 -0
  37. data/lib/cuprum/cli/errors/files/template_not_resolved.rb +54 -0
  38. data/lib/cuprum/cli/errors/files.rb +19 -0
  39. data/lib/cuprum/cli/errors/system_command_failure.rb +44 -0
  40. data/lib/cuprum/cli/errors.rb +11 -0
  41. data/lib/cuprum/cli/integrations/thor/arguments_parser.rb +99 -0
  42. data/lib/cuprum/cli/integrations/thor/registry.rb +42 -0
  43. data/lib/cuprum/cli/integrations/thor/task.rb +211 -0
  44. data/lib/cuprum/cli/integrations/thor.rb +14 -0
  45. data/lib/cuprum/cli/integrations.rb +8 -0
  46. data/lib/cuprum/cli/metadata.rb +215 -0
  47. data/lib/cuprum/cli/option.rb +165 -0
  48. data/lib/cuprum/cli/options/class_methods.rb +232 -0
  49. data/lib/cuprum/cli/options/quiet.rb +32 -0
  50. data/lib/cuprum/cli/options/verbose.rb +32 -0
  51. data/lib/cuprum/cli/options.rb +18 -0
  52. data/lib/cuprum/cli/registry.rb +141 -0
  53. data/lib/cuprum/cli/rspec/deferred/arguments_examples.rb +203 -0
  54. data/lib/cuprum/cli/rspec/deferred/ci/report_examples.rb +450 -0
  55. data/lib/cuprum/cli/rspec/deferred/ci.rb +8 -0
  56. data/lib/cuprum/cli/rspec/deferred/dependencies/file_system_examples.rb +1469 -0
  57. data/lib/cuprum/cli/rspec/deferred/dependencies.rb +8 -0
  58. data/lib/cuprum/cli/rspec/deferred/metadata_examples.rb +856 -0
  59. data/lib/cuprum/cli/rspec/deferred/options_examples.rb +234 -0
  60. data/lib/cuprum/cli/rspec/deferred/registry_examples.rb +451 -0
  61. data/lib/cuprum/cli/rspec/deferred.rb +8 -0
  62. data/lib/cuprum/cli/rspec.rb +8 -0
  63. data/lib/cuprum/cli/version.rb +59 -0
  64. data/lib/cuprum/cli.rb +47 -0
  65. metadata +173 -0
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/cli'
4
+
5
+ module Cuprum::Cli
6
+ # Namespace for functionality that implements command positional arguments.
7
+ module Arguments
8
+ autoload :ClassMethods, 'cuprum/cli/arguments/class_methods'
9
+
10
+ # Exception raised when a command receives too many positional arguments.
11
+ class ExtraArgumentsError < StandardError; end
12
+
13
+ # Exception raised when a command receives an invalid value for an argument.
14
+ class InvalidArgumentError < StandardError; end
15
+ end
16
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/cli'
4
+
5
+ module Cuprum::Cli
6
+ # Utility for coercing raw string values to other literal types.
7
+ module Coercion
8
+ FALSY_VALUES = Set.new(%w[f false n no]).freeze
9
+ private_constant :FALSY_VALUES
10
+
11
+ INTEGER_PATTERN = /\A-?\d+([\d_,]+\d)?\z/i
12
+ private_constant :INTEGER_PATTERN
13
+
14
+ NULLISH_VALUES = Set.new(['', 'nil', 'null']).freeze
15
+ private_constant :NULLISH_VALUES
16
+
17
+ TRUTHY_VALUES = Set.new(%w[t true y yes]).freeze
18
+ private_constant :TRUTHY_VALUES
19
+
20
+ # Exception raised when trying to coerce an invalid value.
21
+ class CoercionError < StandardError; end
22
+
23
+ # Converts the value to a predicted type based on the value format.
24
+ #
25
+ # @param value [Object] the value to convert.
26
+ #
27
+ # @return [nil, true, false, Integer, String] the coerced value.
28
+ def self.coerce(value)
29
+ skip_validation = true
30
+
31
+ return coerce_nil(value, skip_validation:) if coerce_nil?(value)
32
+ return coerce_boolean(value, skip_validation:) if coerce_boolean?(value)
33
+ return coerce_integer(value, skip_validation:) if coerce_integer?(value)
34
+
35
+ return value if value.is_a?(String)
36
+
37
+ value.inspect
38
+ end
39
+
40
+ # @overload coerce_boolean(value)
41
+ # Converts the value to true or false.
42
+ #
43
+ # @param value [Object] the value to convert.
44
+ #
45
+ # @return [true, false] the coerced value.
46
+ #
47
+ # @raise [CoercionError] if the value cannot be coerced to either true or
48
+ # false.
49
+ def self.coerce_boolean(value, skip_validation: false)
50
+ return false if value.nil?
51
+
52
+ if !skip_validation && !coerce_boolean?(value)
53
+ raise CoercionError,
54
+ "unable to coerce #{value.inspect} to true or false"
55
+ end
56
+
57
+ value = value.downcase
58
+
59
+ return false if FALSY_VALUES.include?(value)
60
+ return true if TRUTHY_VALUES.include?(value)
61
+
62
+ nil
63
+ end
64
+
65
+ # @return [true, false] true if the value can be safely coerced to either
66
+ # true or false, otherwise false.
67
+ def self.coerce_boolean?(value)
68
+ return true if value.nil?
69
+ return false unless value.is_a?(String)
70
+
71
+ value = value.downcase
72
+
73
+ FALSY_VALUES.include?(value) || TRUTHY_VALUES.include?(value)
74
+ end
75
+
76
+ # @overload coerce_integer(value)
77
+ # Converts the value to an Integer.
78
+ #
79
+ # @param value [Object] the value to convert.
80
+ #
81
+ # @return [Integer] the coerced value.
82
+ #
83
+ # @raise [CoercionError] if the value cannot be coerced to an Integer.
84
+ def self.coerce_integer(value, skip_validation: false)
85
+ if !skip_validation && !coerce_integer?(value)
86
+ raise CoercionError,
87
+ "unable to coerce #{value.inspect} to an Integer"
88
+ end
89
+
90
+ value.tr('_,', '').to_i
91
+ end
92
+
93
+ # @return [true, false] true if the value can be safely coerced to an
94
+ # Integer, otherwise false.
95
+ def self.coerce_integer?(value)
96
+ return false unless value.is_a?(String)
97
+
98
+ value = value.downcase
99
+
100
+ INTEGER_PATTERN.match?(value)
101
+ end
102
+
103
+ # @overload coerce_nil(value)
104
+ # Converts the value to nil.
105
+ #
106
+ # @param value [Object] the value to convert.
107
+ #
108
+ # @return [nil] the coerced value.
109
+ #
110
+ # @raise [CoercionError] if the value cannot be coerced to nil.
111
+ def self.coerce_nil(value, skip_validation: false)
112
+ return nil if value.nil?
113
+
114
+ if !skip_validation && !coerce_nil?(value)
115
+ raise CoercionError,
116
+ "unable to coerce #{value.inspect} to nil"
117
+ end
118
+
119
+ nil
120
+ end
121
+
122
+ # @return [true, false] true if the value can be safely coerced to nil,
123
+ # otherwise false.
124
+ def self.coerce_nil?(value)
125
+ return true if value.nil?
126
+ return false unless value.is_a?(String)
127
+
128
+ NULLISH_VALUES.include?(value.downcase)
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum'
4
+ require 'plumbum'
5
+
6
+ require 'cuprum/cli'
7
+
8
+ module Cuprum::Cli
9
+ # Abstract base class for defining CLI commands.
10
+ class Command < Cuprum::Command
11
+ include Plumbum::Consumer
12
+ prepend Plumbum::Parameters
13
+ include Cuprum::Cli::Metadata
14
+ extend Cuprum::Cli::Arguments::ClassMethods
15
+ extend Cuprum::Cli::Options::ClassMethods
16
+
17
+ abstract
18
+
19
+ provider Cuprum::Cli::Dependencies.provider
20
+
21
+ class << self
22
+ # (see Cuprum::Cli::Arguments::ClassMethods#argument)
23
+ def argument(argument_name, **)
24
+ return super unless abstract?
25
+
26
+ raise Cuprum::Cli::Metadata::AbstractCommandError,
27
+ abstract_command_message("define argument :#{argument_name}")
28
+ end
29
+
30
+ # (see Cuprum::Cli::Dependencies::ClassMethods#dependency)
31
+ def dependency(dependency_name, **)
32
+ return super unless abstract?
33
+
34
+ raise Cuprum::Cli::Metadata::AbstractCommandError,
35
+ abstract_command_message("add dependency :#{dependency_name}")
36
+ end
37
+
38
+ # (see Cuprum::Cli::Dependencies::ClassMethods#option)
39
+ def option(option_name, **)
40
+ return super unless abstract?
41
+
42
+ raise Cuprum::Cli::Metadata::AbstractCommandError,
43
+ abstract_command_message("define option :#{option_name}")
44
+ end
45
+ end
46
+
47
+ def initialize(**)
48
+ super()
49
+
50
+ @arguments = {}
51
+ @options = {}
52
+ end
53
+
54
+ # @overload call(*arguments, **options)
55
+ # Resolves the parameters and calls the command.
56
+ #
57
+ # @param arguments [Array] arguments passed to the command.
58
+ # @param options [Hash] options passed to the command.
59
+ #
60
+ # @return [Cuprum::Result] the command result.
61
+ #
62
+ # @raise [Cuprum::Cli::Arguments::ExtraArgumentsError] when given too many
63
+ # positional arguments.
64
+ # @raise [Cuprum::Cli::Arguments::InvalidArgumentError] when given an
65
+ # invalid value for an argument.
66
+ # @raise [Cuprum::Cli::Options::InvalidOptionError] when given an invalid
67
+ # value for an option.
68
+ # @raise [Cuprum::Cli::Options::UnknownOptionError] when given a value for
69
+ # an undefined option.
70
+ #
71
+ # @overload call(resolved_arguments:, resolved_options:)
72
+ # Calls the command with the given arguments and options.
73
+ #
74
+ # This variant assumes that the arguments and options have already been
75
+ # parsed and validated against the command's expected parameters.
76
+ #
77
+ # @param resolved_arguments [Hash] the arguments passed to the command,
78
+ # formatted as an Hash with the matching argument name as key and the
79
+ # argument value as the corresponding value.
80
+ # @param resolved_options [Hash] the options passed to the command.
81
+ #
82
+ # @return [Cuprum::Result] the command result.
83
+ def call(*, resolved_arguments: nil, resolved_options: nil, **)
84
+ @arguments =
85
+ resolved_arguments || self.class.resolve_arguments(*)
86
+ @options =
87
+ resolved_options || self.class.resolve_options(**)
88
+
89
+ super()
90
+ end
91
+
92
+ private
93
+
94
+ attr_reader :arguments
95
+
96
+ attr_reader :options
97
+
98
+ def tools
99
+ SleepingKingStudios::Tools::Toolbelt.instance
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sleeping_king_studios/tools/toolbox/heritable_data'
4
+
5
+ require 'cuprum/cli/commands/ci'
6
+
7
+ module Cuprum::Cli::Commands::Ci
8
+ # Data class for recording the results of a CI command.
9
+ Report = SleepingKingStudios::Tools::Toolbox::HeritableData.define( # rubocop:disable Metrics/BlockLength
10
+ :label,
11
+ :duration,
12
+ :error_count,
13
+ :failure_count,
14
+ :pending_count,
15
+ :success_count,
16
+ :total_count
17
+ ) do
18
+ # @param duration [Integer, Float] the duration of the CI step, in
19
+ # seconds.
20
+ # @param error_count [Integer] the number of static errors reported by the
21
+ # step.
22
+ # @param failure_count [Integer] the number of failures reported by the
23
+ # step.
24
+ # @param label [String] a human-readable representation of the step.
25
+ # @param pending_count [Integer] the number of pending items reported by
26
+ # the step.
27
+ # @param success_count [Integer] the number of successful items reported
28
+ # by the step. Defaults to calculated from the total minus the pending
29
+ # and failing items.
30
+ # @param total_count [Integer] the total number of items reported by the
31
+ # step.
32
+ def initialize( # rubocop:disable Metrics/ParameterLists
33
+ duration:,
34
+ total_count:,
35
+ label: nil,
36
+ error_count: 0,
37
+ failure_count: 0,
38
+ pending_count: 0,
39
+ success_count: nil,
40
+ **
41
+ )
42
+ success_count ||= total_count - (failure_count + pending_count)
43
+
44
+ super
45
+ end
46
+
47
+ # @return [true, false] true if the step reported any errors, otherwise
48
+ # false.
49
+ def errored? = !error_count.zero?
50
+
51
+ # @return [true, false] true if the step reported any failing items,
52
+ # otherwise false.
53
+ def failure? = !failure_count.zero?
54
+
55
+ # Combines the given report with the current report.
56
+ #
57
+ # @param other_report [Cuprum::Cli::Commands::Ci::Report] the given
58
+ # report.
59
+ #
60
+ # @return [Cuprum::Cli::Commands::Ci::Report] the combined report.
61
+ def merge(other_report, with_label: nil) # rubocop:disable Metrics/MethodLength
62
+ tools.assertions.validate_instance_of(
63
+ other_report,
64
+ as: 'report',
65
+ expected: self.class
66
+ )
67
+
68
+ with_label ||= [label, other_report.label].compact.join(' + ')
69
+ with_label = nil if with_label.empty?
70
+
71
+ self.class.new(
72
+ label: with_label,
73
+ **merge_properties(other_report, except: :label)
74
+ )
75
+ end
76
+
77
+ # @return [true, false] true if the step reported any pending items,
78
+ # otherwise false.
79
+ def pending? = !pending_count.zero?
80
+
81
+ # @return [true, false] true if the step did not report any errors or
82
+ # failing items; otherwise false.
83
+ def success? = !errored? && !failure?
84
+
85
+ # Generates a human-readable summary of the report.
86
+ def summary
87
+ summary = "#{total_count} #{tools.string_tools.pluralize(item_name)}"
88
+ summary << ", #{failure_count} failures"
89
+ summary << ", #{pending_count} pending" if pending?
90
+ summary << ", #{error_count} errors" if errored?
91
+
92
+ summary.freeze
93
+ end
94
+
95
+ private
96
+
97
+ def item_name = 'item'
98
+
99
+ def merge_properties(other, except: []) # rubocop:disable Metrics/MethodLength
100
+ excepted = Set.new(Array(except))
101
+
102
+ self
103
+ .class
104
+ .members
105
+ .reject { |member_name| excepted.include?(member_name) }
106
+ .to_h do |member_name|
107
+ [
108
+ member_name,
109
+ [
110
+ public_send(member_name),
111
+ other.public_send(member_name)
112
+ ]
113
+ .compact
114
+ .reduce(&:+)
115
+ ]
116
+ end
117
+ end
118
+
119
+ def tools = SleepingKingStudios::Tools::Toolbelt.instance
120
+ end
121
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ require 'cuprum/cli/commands/ci'
6
+
7
+ module Cuprum::Cli::Commands::Ci
8
+ # Command for running an RSpec test suite.
9
+ class RSpecCommand < Cuprum::Cli::Command
10
+ # Data class for recording the results of an RSpec command.
11
+ Report = Cuprum::Cli::Commands::Ci::Report.define do
12
+ private
13
+
14
+ def item_name = 'example'
15
+ end
16
+
17
+ dependency :file_system
18
+ dependency :standard_io
19
+ dependency :system_command
20
+
21
+ include Cuprum::Cli::Dependencies::StandardIo::Helpers
22
+ include Cuprum::Cli::Options::Quiet
23
+
24
+ arguments :file_patterns
25
+
26
+ option :color, type: :boolean, default: true
27
+ option :coverage, type: :boolean, default: false
28
+ option :env, type: :hash, default: {}
29
+ option :format, type: :string, default: 'progress'
30
+ option :gemfile
31
+
32
+ description 'Runs an RSpec command.'
33
+
34
+ full_name 'ci:rspec'
35
+
36
+ private
37
+
38
+ def collect_stats
39
+ file_system.with_tempfile do |tempfile|
40
+ yield tempfile
41
+
42
+ file_system
43
+ .read_file(tempfile)
44
+ .then { |raw| JSON.parse(raw) }
45
+ .then { |hsh| hsh['summary'] }
46
+ end
47
+ rescue JSON::ParserError => exception
48
+ failure(json_error(exception.message))
49
+ end
50
+
51
+ def command_arguments(file_path)
52
+ [
53
+ *file_patterns,
54
+ color? ? '--force-color' : '--no-color',
55
+ '--format=json',
56
+ "--out=#{file_path}"
57
+ ]
58
+ end
59
+
60
+ def command_environment
61
+ env = self.env.dup
62
+ env[:coverage] = false unless coverage?
63
+ env[:bundler_gemfile] = gemfile if gemfile
64
+ env
65
+ end
66
+
67
+ def command_options
68
+ { format: quiet? ? nil : format }
69
+ end
70
+
71
+ def generate_report(data)
72
+ Report.new(
73
+ label: 'ci:rspec',
74
+ duration: data.fetch('duration'),
75
+ error_count: data.fetch('errors_outside_of_examples_count'),
76
+ failure_count: data.fetch('failure_count'),
77
+ pending_count: data.fetch('pending_count'),
78
+ total_count: data.fetch('example_count')
79
+ )
80
+ end
81
+
82
+ def json_error(message)
83
+ message = "unable to parse JSON results - #{message}"
84
+
85
+ Cuprum::Error.new(message:)
86
+ end
87
+
88
+ def process
89
+ json = step do
90
+ collect_stats { |tempfile| run_command(tempfile) }
91
+ end
92
+
93
+ report = generate_report(json)
94
+
95
+ return success(report) unless report.errored? || report.failure?
96
+
97
+ build_result(status: :failure, value: report)
98
+ end
99
+
100
+ def run_command(tempfile)
101
+ arguments = command_arguments(tempfile.path)
102
+ environment = command_environment
103
+ options = command_options
104
+
105
+ system_command.spawn('rspec', arguments:, environment:, options:)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/cli/commands/ci'
4
+ require 'cuprum/cli/commands/ci/rspec_command'
5
+
6
+ module Cuprum::Cli::Commands::Ci
7
+ # Command for running each RSpec file in its own process.
8
+ class RSpecEachCommand < Cuprum::Cli::Command # rubocop:disable Metrics/ClassLength
9
+ dependency :file_system
10
+ dependency :standard_io
11
+
12
+ include Cuprum::Cli::Dependencies::StandardIo::Helpers
13
+ include Cuprum::Cli::Options::Quiet
14
+
15
+ arguments :file_patterns
16
+
17
+ option :color, type: :boolean, default: true
18
+ option :env, type: :hash, default: {}
19
+ option :gemfile
20
+
21
+ description 'Runs each RSpec file in an isolated process.'
22
+
23
+ full_name 'ci:rspec_each'
24
+
25
+ private
26
+
27
+ attr_reader :errored_files
28
+
29
+ attr_reader :failing_files
30
+
31
+ attr_reader :pending_files
32
+
33
+ attr_reader :report
34
+
35
+ def aggregate_file_results(filename:, result:) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
36
+ if result.value.is_a?(Cuprum::Cli::Commands::Ci::Report)
37
+ merge_report(result)
38
+ end
39
+
40
+ if result.value&.errored? || result.failure?
41
+ errored_files << filename
42
+ elsif result.value&.failure?
43
+ failing_files << filename
44
+ elsif result.value&.pending?
45
+ pending_files << filename
46
+ end
47
+ end
48
+
49
+ def color(text, color)
50
+ return text unless color?
51
+
52
+ standard_io.color(text, color)
53
+ end
54
+
55
+ def configured_file_patterns
56
+ patterns = arguments.fetch(:file_patterns, [])
57
+
58
+ return patterns unless patterns.empty?
59
+
60
+ ['spec/**{,/*/**}/*_spec.rb']
61
+ end
62
+
63
+ def format_status(result)
64
+ if result.failure? || result.value.errored?
65
+ color('Errored', 'red')
66
+ elsif result.value.failure?
67
+ color('Failing', 'red')
68
+ elsif result.value.pending?
69
+ color('Pending', 'yellow')
70
+ else
71
+ color('Passing', 'green')
72
+ end
73
+ end
74
+
75
+ def matching_files
76
+ return @matching_files if @matching_files
77
+
78
+ all_files = Set.new
79
+
80
+ configured_file_patterns.each do |pattern|
81
+ file_system.each_file(pattern) { |filename| all_files << filename }
82
+ end
83
+
84
+ @matching_files = all_files.sort
85
+ end
86
+
87
+ def merge_report(result) = @report = report.merge(result.value)
88
+
89
+ def process
90
+ reset_report
91
+
92
+ say "Running #{matching_files.count} spec files...\n"
93
+ say "\n" unless matching_files.none?
94
+
95
+ matching_files.each { |filename| run_file(filename) }
96
+
97
+ summarize_report
98
+
99
+ report = @report.with(label: 'ci:rspec_each')
100
+
101
+ return success(report) unless report.errored? || report.failure?
102
+
103
+ build_result(status: :failure, value: report)
104
+ end
105
+
106
+ def reset_report
107
+ @errored_files = []
108
+ @failing_files = []
109
+ @pending_files = []
110
+ @report = Cuprum::Cli::Commands::Ci::RSpecCommand::Report.new(
111
+ duration: 0.0,
112
+ total_count: 0
113
+ )
114
+ end
115
+
116
+ def run_file(filename)
117
+ command =
118
+ Cuprum::Cli::Commands::Ci::RSpecCommand.new(file_system:, standard_io:)
119
+ result = command.call(filename, env:, gemfile:, quiet: true)
120
+ filename = trim_filename(filename)
121
+
122
+ say "#{format_status(result)} #{filename}"
123
+
124
+ aggregate_file_results(filename:, result:)
125
+
126
+ result
127
+ end
128
+
129
+ def say_errored
130
+ return if errored_files.empty?
131
+
132
+ say "\nErrored:\n\n"
133
+
134
+ errored_files.each do |filename|
135
+ say color(" #{filename}", 'red')
136
+ end
137
+ end
138
+
139
+ def say_failures
140
+ return if failing_files.empty?
141
+
142
+ say "\nFailures:\n\n"
143
+
144
+ failing_files.each do |filename|
145
+ say color(" #{filename}", 'red')
146
+ end
147
+ end
148
+
149
+ def say_pending
150
+ return if pending_files.empty?
151
+
152
+ say "\nPending:\n\n"
153
+
154
+ pending_files.each do |filename|
155
+ say color(" #{filename}", 'yellow')
156
+ end
157
+ end
158
+
159
+ def say_summary # rubocop:disable Metrics/AbcSize
160
+ say "\nFinished in #{report.duration.round(2)} seconds"
161
+
162
+ summary_color =
163
+ if report.errored? || report.failure? || errored_files.any?
164
+ 'red'
165
+ elsif report.pending? || report.total_count.zero?
166
+ 'yellow'
167
+ else
168
+ 'green'
169
+ end
170
+
171
+ say color(report.summary, summary_color)
172
+ end
173
+
174
+ def summarize_report
175
+ say_pending
176
+ say_failures
177
+ say_errored
178
+ say_summary
179
+ end
180
+
181
+ def trim_filename(filename)
182
+ filename.sub(%r{\A#{file_system.root_path}/?}, '')
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/cli/commands'
4
+
5
+ module Cuprum::Cli::Commands
6
+ # Namespace for commands used in continuous integration.
7
+ module Ci
8
+ autoload :Report, 'cuprum/cli/commands/ci/report'
9
+ autoload :RSpecCommand, 'cuprum/cli/commands/ci/rspec_command'
10
+ autoload :RSpecEachCommand, 'cuprum/cli/commands/ci/rspec_each_command'
11
+ end
12
+ end