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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +34 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE +21 -0
- data/README.md +163 -0
- data/lib/cuprum/cli/argument.rb +172 -0
- data/lib/cuprum/cli/arguments/class_methods.rb +283 -0
- data/lib/cuprum/cli/arguments.rb +16 -0
- data/lib/cuprum/cli/coercion.rb +131 -0
- data/lib/cuprum/cli/command.rb +102 -0
- data/lib/cuprum/cli/commands/ci/report.rb +121 -0
- data/lib/cuprum/cli/commands/ci/rspec_command.rb +108 -0
- data/lib/cuprum/cli/commands/ci/rspec_each_command.rb +185 -0
- data/lib/cuprum/cli/commands/ci.rb +12 -0
- data/lib/cuprum/cli/commands/echo_command.rb +76 -0
- data/lib/cuprum/cli/commands/file/generate_file.rb +141 -0
- data/lib/cuprum/cli/commands/file/new_command.rb +86 -0
- data/lib/cuprum/cli/commands/file/render_erb.rb +88 -0
- data/lib/cuprum/cli/commands/file/resolve_template.rb +136 -0
- data/lib/cuprum/cli/commands/file/templates/rspec.rb.erb +14 -0
- data/lib/cuprum/cli/commands/file/templates/ruby.rb.erb +29 -0
- data/lib/cuprum/cli/commands/file/templates.rb +71 -0
- data/lib/cuprum/cli/commands/file.rb +14 -0
- data/lib/cuprum/cli/commands.rb +12 -0
- data/lib/cuprum/cli/dependencies/file_system/mock.rb +297 -0
- data/lib/cuprum/cli/dependencies/file_system.rb +247 -0
- data/lib/cuprum/cli/dependencies/standard_io/helpers.rb +138 -0
- data/lib/cuprum/cli/dependencies/standard_io/mock.rb +85 -0
- data/lib/cuprum/cli/dependencies/standard_io.rb +110 -0
- data/lib/cuprum/cli/dependencies/system_command/mock.rb +57 -0
- data/lib/cuprum/cli/dependencies/system_command.rb +147 -0
- data/lib/cuprum/cli/dependencies.rb +25 -0
- data/lib/cuprum/cli/errors/files/file_not_writeable.rb +42 -0
- data/lib/cuprum/cli/errors/files/missing_parameter.rb +71 -0
- data/lib/cuprum/cli/errors/files/missing_template.rb +36 -0
- data/lib/cuprum/cli/errors/files/template_error.rb +37 -0
- data/lib/cuprum/cli/errors/files/template_not_resolved.rb +54 -0
- data/lib/cuprum/cli/errors/files.rb +19 -0
- data/lib/cuprum/cli/errors/system_command_failure.rb +44 -0
- data/lib/cuprum/cli/errors.rb +11 -0
- data/lib/cuprum/cli/integrations/thor/arguments_parser.rb +99 -0
- data/lib/cuprum/cli/integrations/thor/registry.rb +42 -0
- data/lib/cuprum/cli/integrations/thor/task.rb +211 -0
- data/lib/cuprum/cli/integrations/thor.rb +14 -0
- data/lib/cuprum/cli/integrations.rb +8 -0
- data/lib/cuprum/cli/metadata.rb +215 -0
- data/lib/cuprum/cli/option.rb +165 -0
- data/lib/cuprum/cli/options/class_methods.rb +232 -0
- data/lib/cuprum/cli/options/quiet.rb +32 -0
- data/lib/cuprum/cli/options/verbose.rb +32 -0
- data/lib/cuprum/cli/options.rb +18 -0
- data/lib/cuprum/cli/registry.rb +141 -0
- data/lib/cuprum/cli/rspec/deferred/arguments_examples.rb +203 -0
- data/lib/cuprum/cli/rspec/deferred/ci/report_examples.rb +450 -0
- data/lib/cuprum/cli/rspec/deferred/ci.rb +8 -0
- data/lib/cuprum/cli/rspec/deferred/dependencies/file_system_examples.rb +1469 -0
- data/lib/cuprum/cli/rspec/deferred/dependencies.rb +8 -0
- data/lib/cuprum/cli/rspec/deferred/metadata_examples.rb +856 -0
- data/lib/cuprum/cli/rspec/deferred/options_examples.rb +234 -0
- data/lib/cuprum/cli/rspec/deferred/registry_examples.rb +451 -0
- data/lib/cuprum/cli/rspec/deferred.rb +8 -0
- data/lib/cuprum/cli/rspec.rb +8 -0
- data/lib/cuprum/cli/version.rb +59 -0
- data/lib/cuprum/cli.rb +47 -0
- 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
|