sleeping_king_studios-tasks 0.1.0.rc.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +26 -0
  3. data/DEVELOPMENT.md +50 -0
  4. data/LICENSE +22 -0
  5. data/README.md +405 -0
  6. data/lib/sleeping_king_studios/tasks.rb +27 -0
  7. data/lib/sleeping_king_studios/tasks/apps.rb +49 -0
  8. data/lib/sleeping_king_studios/tasks/apps/app_configuration.rb +68 -0
  9. data/lib/sleeping_king_studios/tasks/apps/applications_task.rb +28 -0
  10. data/lib/sleeping_king_studios/tasks/apps/bundle.rb +8 -0
  11. data/lib/sleeping_king_studios/tasks/apps/bundle/install_runner.rb +21 -0
  12. data/lib/sleeping_king_studios/tasks/apps/bundle/install_task.rb +39 -0
  13. data/lib/sleeping_king_studios/tasks/apps/bundle/update_runner.rb +21 -0
  14. data/lib/sleeping_king_studios/tasks/apps/bundle/update_task.rb +39 -0
  15. data/lib/sleeping_king_studios/tasks/apps/ci.rb +8 -0
  16. data/lib/sleeping_king_studios/tasks/apps/ci/results_reporter.rb +69 -0
  17. data/lib/sleeping_king_studios/tasks/apps/ci/rspec_task.rb +29 -0
  18. data/lib/sleeping_king_studios/tasks/apps/ci/rspec_wrapper.rb +42 -0
  19. data/lib/sleeping_king_studios/tasks/apps/ci/rubocop_task.rb +29 -0
  20. data/lib/sleeping_king_studios/tasks/apps/ci/rubocop_wrapper.rb +29 -0
  21. data/lib/sleeping_king_studios/tasks/apps/ci/simplecov_task.rb +68 -0
  22. data/lib/sleeping_king_studios/tasks/apps/ci/step_wrapper.rb +49 -0
  23. data/lib/sleeping_king_studios/tasks/apps/ci/steps_runner.rb +37 -0
  24. data/lib/sleeping_king_studios/tasks/apps/ci/steps_task.rb +92 -0
  25. data/lib/sleeping_king_studios/tasks/ci.rb +8 -0
  26. data/lib/sleeping_king_studios/tasks/ci/cucumber_parser.rb +118 -0
  27. data/lib/sleeping_king_studios/tasks/ci/cucumber_results.rb +191 -0
  28. data/lib/sleeping_king_studios/tasks/ci/cucumber_runner.rb +53 -0
  29. data/lib/sleeping_king_studios/tasks/ci/cucumber_task.rb +47 -0
  30. data/lib/sleeping_king_studios/tasks/ci/results_helpers.rb +44 -0
  31. data/lib/sleeping_king_studios/tasks/ci/rspec_each_results.rb +118 -0
  32. data/lib/sleeping_king_studios/tasks/ci/rspec_each_task.rb +156 -0
  33. data/lib/sleeping_king_studios/tasks/ci/rspec_results.rb +126 -0
  34. data/lib/sleeping_king_studios/tasks/ci/rspec_runner.rb +62 -0
  35. data/lib/sleeping_king_studios/tasks/ci/rspec_task.rb +71 -0
  36. data/lib/sleeping_king_studios/tasks/ci/rubocop_results.rb +80 -0
  37. data/lib/sleeping_king_studios/tasks/ci/rubocop_runner.rb +46 -0
  38. data/lib/sleeping_king_studios/tasks/ci/rubocop_task.rb +44 -0
  39. data/lib/sleeping_king_studios/tasks/ci/simplecov_results.rb +62 -0
  40. data/lib/sleeping_king_studios/tasks/ci/simplecov_task.rb +25 -0
  41. data/lib/sleeping_king_studios/tasks/ci/steps_runner.rb +69 -0
  42. data/lib/sleeping_king_studios/tasks/ci/steps_task.rb +93 -0
  43. data/lib/sleeping_king_studios/tasks/configuration.rb +114 -0
  44. data/lib/sleeping_king_studios/tasks/file.rb +8 -0
  45. data/lib/sleeping_king_studios/tasks/file/new_task.rb +238 -0
  46. data/lib/sleeping_king_studios/tasks/process_runner.rb +70 -0
  47. data/lib/sleeping_king_studios/tasks/task.rb +95 -0
  48. data/lib/sleeping_king_studios/tasks/task_group.rb +37 -0
  49. data/lib/sleeping_king_studios/tasks/version.rb +58 -0
  50. metadata +271 -0
@@ -0,0 +1,29 @@
1
+ # lib/sleeping_king_studios/tasks/apps/ci/rubocop_task.rb
2
+
3
+ require 'sleeping_king_studios/tasks/apps/ci'
4
+ require 'sleeping_king_studios/tasks/apps/ci/steps_task'
5
+
6
+ module SleepingKingStudios::Tasks::Apps::Ci
7
+ # Defines a Thor task for running the RuboCop linter for each application.
8
+ class RuboCopTask < SleepingKingStudios::Tasks::Task
9
+ def self.description
10
+ 'Runs the RuboCop linter for each application.'
11
+ end # class method description
12
+
13
+ def self.task_name
14
+ 'rubocop'
15
+ end # class method task_name
16
+
17
+ option :quiet,
18
+ :aliases => '-q',
19
+ :type => :boolean,
20
+ :default => false,
21
+ :desc => 'Do not write lint results to STDOUT.'
22
+
23
+ def call *applications
24
+ SleepingKingStudios::Tasks::Apps::Ci::StepsTask.
25
+ new(options.merge('only' => %w(rubocop))).
26
+ call(*applications)
27
+ end # method call
28
+ end # class
29
+ end # module
@@ -0,0 +1,29 @@
1
+ # lib/sleeping_king_studios/tasks/apps/ci/rubocop_wrapper.rb
2
+
3
+ require 'sleeping_king_studios/tasks/apps/ci'
4
+ require 'sleeping_king_studios/tasks/apps/ci/step_wrapper'
5
+ require 'sleeping_king_studios/tasks/ci/rubocop_results'
6
+
7
+ module SleepingKingStudios::Tasks::Apps::Ci
8
+ # Wrapper class for calling a RuboCop Ci task for a specific application.
9
+ class RuboCopWrapper < SleepingKingStudios::Tasks::Apps::Ci::StepWrapper
10
+ def call application
11
+ super
12
+
13
+ run_step(*source_files)
14
+ end # method call
15
+
16
+ private
17
+
18
+ def source_files
19
+ config =
20
+ SleepingKingStudios::Tasks::Apps.configuration[current_application]
21
+
22
+ config.source_files + config.spec_files
23
+ end # method source_files
24
+
25
+ def step_key
26
+ :rubocop
27
+ end # method step_key
28
+ end # module
29
+ end # module
@@ -0,0 +1,68 @@
1
+ # lib/sleeping_king_studios/tasks/apps/ci/simplecov_task.rb
2
+
3
+ require 'sleeping_king_studios/tasks/apps/ci'
4
+ require 'sleeping_king_studios/tasks/ci/simplecov_results'
5
+ require 'sleeping_king_studios/tasks/task'
6
+
7
+ module SleepingKingStudios::Tasks::Apps::Ci
8
+ # Defines a Thor task for aggregating SimpleCov coverage results across
9
+ # applications.
10
+ class SimpleCovTask < SleepingKingStudios::Tasks::Task
11
+ RESULTS_STRUCT =
12
+ Struct.new(
13
+ :covered_lines,
14
+ :covered_percent,
15
+ :missed_lines,
16
+ :total_lines
17
+ ) # end struct
18
+
19
+ def self.configure_simplecov!
20
+ require 'simplecov'
21
+ require 'simplecov-json'
22
+
23
+ ::SimpleCov.configure do
24
+ command_name "#{command_name}:#{ENV['APP_NAME']}" if ENV['APP_NAME']
25
+
26
+ self.formatter =
27
+ ::SimpleCov::Formatter::MultiFormatter.new(
28
+ [formatter, ::SimpleCov::Formatter::JSONFormatter]
29
+ ) # end formatter
30
+ end # configure
31
+ end # class method configure_simplecov!
32
+
33
+ def self.description
34
+ 'Aggregates the SimpleCov results for all applications.'
35
+ end # class method description
36
+
37
+ def self.task_name
38
+ 'simplecov'
39
+ end # class method task_name
40
+
41
+ def call _application = nil
42
+ results = load_report :report => File.join('coverage', 'coverage.json')
43
+ results = convert_results_to_object(results)
44
+
45
+ SleepingKingStudios::Tasks::Ci::SimpleCovResults.new(results)
46
+ end # method call
47
+
48
+ private
49
+
50
+ def load_report report:
51
+ raw = File.read report
52
+ json = JSON.parse raw
53
+
54
+ json['metrics']
55
+ rescue
56
+ {}
57
+ end # method load_report
58
+
59
+ def convert_results_to_object hsh
60
+ RESULTS_STRUCT.new(
61
+ hsh.fetch('covered_lines', 0),
62
+ hsh.fetch('covered_percent', 0.0),
63
+ hsh.fetch('total_lines', 0) - hsh.fetch('covered_lines', 0),
64
+ hsh.fetch('total_lines', 0)
65
+ ) # end object
66
+ end # method convert_results_to_object
67
+ end # module
68
+ end # module
@@ -0,0 +1,49 @@
1
+ # lib/sleeping_king_studios/tasks/apps/ci/step_wrapper.rb
2
+
3
+ require 'sleeping_king_studios/tasks/apps/applications_task'
4
+ require 'sleeping_king_studios/tasks/apps/ci'
5
+
6
+ module SleepingKingStudios::Tasks::Apps::Ci
7
+ # Wrapper class for calling a configured task for a specific application.
8
+ class StepWrapper < SleepingKingStudios::Tasks::Task
9
+ include SleepingKingStudios::Tasks::Apps::ApplicationsTask
10
+
11
+ def call application, *_rest
12
+ @current_application = application
13
+ @step_config = nil
14
+ end # method call
15
+
16
+ private
17
+
18
+ attr_reader :current_application
19
+
20
+ def build_step
21
+ require step_config.fetch(:require) if step_config.key?(:require)
22
+
23
+ step_class = Object.const_get(step_config.fetch :class)
24
+
25
+ step_class.new(step_options)
26
+ end # method
27
+
28
+ def run_step *args
29
+ return if skip_step?
30
+
31
+ build_step.call(*args)
32
+ end # method run_step
33
+
34
+ def skip_step?
35
+ step_config == false
36
+ end # method skip_step?
37
+
38
+ def step_config
39
+ config = SleepingKingStudios::Tasks.configuration
40
+ steps = config.ci.steps_with_options
41
+
42
+ steps.fetch(step_key, false)
43
+ end # method step_config
44
+
45
+ def step_options
46
+ options
47
+ end # method step_options
48
+ end # class
49
+ end # module
@@ -0,0 +1,37 @@
1
+ # lib/sleeping_king_studios/tasks/apps/ci/steps_runner.rb
2
+
3
+ require 'sleeping_king_studios/tasks/apps/applications_task'
4
+ require 'sleeping_king_studios/tasks/ci/steps_runner'
5
+
6
+ module SleepingKingStudios::Tasks::Apps::Ci
7
+ # Abstract base class for running a sequence of tasks for a specific
8
+ # application from a configured list.
9
+ class StepsRunner < SleepingKingStudios::Tasks::Ci::StepsRunner
10
+ include SleepingKingStudios::Tasks::Apps::ApplicationsTask
11
+
12
+ def call application
13
+ @current_application = application
14
+
15
+ super application
16
+ end # method call
17
+
18
+ private
19
+
20
+ attr_reader :current_application
21
+
22
+ def ci_steps
23
+ SleepingKingStudios::Tasks::Apps.configuration[current_application].
24
+ ci.steps_with_options
25
+ end # method ci_steps
26
+
27
+ def skip_step? _name, config
28
+ return true if config == false
29
+
30
+ if options.fetch('global', false)
31
+ !config.fetch(:global, false)
32
+ else
33
+ config.fetch(:global, false)
34
+ end # if-else
35
+ end # method skip_step?
36
+ end # class
37
+ end # module
@@ -0,0 +1,92 @@
1
+ # lib/sleeping_king_studios/tasks/apps/ci/steps_task.rb
2
+
3
+ require 'sleeping_king_studios/tasks/apps/applications_task'
4
+ require 'sleeping_king_studios/tasks/apps/ci'
5
+ require 'sleeping_king_studios/tasks/apps/ci/results_reporter'
6
+ require 'sleeping_king_studios/tasks/apps/ci/steps_runner'
7
+
8
+ module SleepingKingStudios::Tasks::Apps::Ci
9
+ # Thor task for running each step in the CI suite for each application and
10
+ # generating a report.
11
+ class StepsTask < SleepingKingStudios::Tasks::Task
12
+ include SleepingKingStudios::Tasks::Apps::ApplicationsTask
13
+
14
+ def self.description
15
+ 'Runs the configured steps for each application.'
16
+ end # class method description
17
+
18
+ option :except,
19
+ :type => :array,
20
+ :default => [],
21
+ :desc => 'Exclude steps from the CI process.'
22
+ option :only,
23
+ :type => :array,
24
+ :default => [],
25
+ :desc => 'Run only the specified steps from the CI process.'
26
+ option :quiet,
27
+ :aliases => '-q',
28
+ :type => :boolean,
29
+ :default => false,
30
+ :desc => 'Do not write intermediate results to STDOUT.'
31
+
32
+ def call *applications
33
+ filtered = filter_applications :only => applications
34
+ results = run_steps(filtered)
35
+ globals = run_global_steps
36
+
37
+ aggregate_results(results) if filtered.count > 1
38
+
39
+ (results['Totals'] ||= {}).update(globals)
40
+
41
+ say "\n" unless quiet?
42
+
43
+ reporter = ResultsReporter.new(self)
44
+ reporter.call(results)
45
+
46
+ results
47
+ end # method call
48
+
49
+ private
50
+
51
+ def aggregate_results results
52
+ totals = Hash.new { |hsh, key| hsh[key] = 0 }
53
+
54
+ results.each do |_, app_results|
55
+ app_results.each do |step, step_results|
56
+ next if step_results.nil?
57
+
58
+ next totals[step] = step_results unless totals.key?(step)
59
+
60
+ totals[step] = totals[step].merge(step_results)
61
+ end # each
62
+ end # each
63
+
64
+ results['Totals'] = totals
65
+ end # method aggregate_results
66
+
67
+ def run_global_steps
68
+ opts = options.merge 'global' => true
69
+ runner = SleepingKingStudios::Tasks::Apps::Ci::StepsRunner.new(opts)
70
+
71
+ runner.call(nil)
72
+ end # method run_global_steps
73
+
74
+ def run_steps applications
75
+ results = Hash.new { |hsh, key| hsh[key] = {} }
76
+
77
+ applications.each do |name, _|
78
+ results[name] = run_steps_for_application(name)
79
+ end # each
80
+
81
+ results
82
+ end # method run_steps
83
+
84
+ def run_steps_for_application name
85
+ steps_runner.call(name)
86
+ end # method run_steps_for_application
87
+
88
+ def steps_runner
89
+ SleepingKingStudios::Tasks::Apps::Ci::StepsRunner.new(options)
90
+ end # method steps_runner
91
+ end # class
92
+ end # module
@@ -0,0 +1,8 @@
1
+ # lib/sleeping_king_studios/tasks/ci.rb
2
+
3
+ require 'sleeping_king_studios/tasks'
4
+
5
+ module SleepingKingStudios::Tasks
6
+ # Thor tasks for continuous integration.
7
+ module Ci; end
8
+ end # module
@@ -0,0 +1,118 @@
1
+ # lib/sleeping_king_studios/tasks/ci/cucumber_parser.rb
2
+
3
+ require 'sleeping_king_studios/tasks/ci'
4
+
5
+ module SleepingKingStudios::Tasks::Ci
6
+ # Parses the output from cucumber --format=json into a summary report.
7
+ module CucumberParser
8
+ class << self
9
+ def parse results
10
+ @report = build_report
11
+
12
+ results.each { |feature| parse_feature(feature) }
13
+
14
+ report['duration'] = report['duration'] / (1.0 * 10**9)
15
+
16
+ report
17
+ end # class method parse
18
+
19
+ private
20
+
21
+ attr_reader :report
22
+
23
+ def build_report
24
+ {
25
+ 'scenario_count' => 0,
26
+ 'failing_scenarios' => [],
27
+ 'pending_scenarios' => [],
28
+ 'step_count' => 0,
29
+ 'failing_step_count' => 0,
30
+ 'pending_step_count' => 0,
31
+ 'duration' => 0
32
+ } # end report
33
+ end # method build report
34
+
35
+ def parse_feature feature
36
+ feature['elements'].each do |scenario|
37
+ parse_scenario(scenario, feature)
38
+ end # each
39
+ end # method parse_feature
40
+
41
+ # rubocop:disable Metrics/MethodLength
42
+ def parse_scenario scenario, feature
43
+ failing = false
44
+ pending = false
45
+
46
+ scenario['steps'].each do |step|
47
+ status = parse_step(step)
48
+
49
+ failing ||= status == 'failed'
50
+ pending ||= status == 'pending' || status == 'skipped'
51
+ end # each
52
+
53
+ report_scenario(
54
+ scenario,
55
+ feature,
56
+ :failing => failing,
57
+ :pending => pending
58
+ ) # end report scenario
59
+ end # method parse_scenario
60
+ # rubocop:enable Metrics/MethodLength
61
+
62
+ def parse_step step
63
+ report['step_count'] += 1
64
+ report['duration'] += step_duration(step)
65
+
66
+ status = step_status(step) || 'failed'
67
+
68
+ if status == 'failed'
69
+ report['failing_step_count'] += 1
70
+ elsif status == 'pending' || status == 'skipped'
71
+ report['pending_step_count'] += 1
72
+ end # if
73
+
74
+ status
75
+ end # method parse_step
76
+
77
+ # rubocop:disable Metrics/MethodLength
78
+ def report_scenario scenario, feature, failing:, pending:
79
+ report['scenario_count'] += 1
80
+
81
+ return unless failing || pending
82
+
83
+ hsh =
84
+ {
85
+ 'location' => "#{feature['uri']}:#{scenario['line']}",
86
+ 'description' => "#{scenario['keyword']}: #{scenario['name']}"
87
+ }
88
+
89
+ if failing
90
+ report['failing_scenarios'] << hsh
91
+ else
92
+ report['pending_scenarios'] << hsh
93
+ end # if-else
94
+ end # method report_scenario
95
+ # rubocop:enable Metrics/MethodLength
96
+
97
+ def step_duration hsh
98
+ return 0 if hsh.nil? || hsh.empty?
99
+
100
+ result = hsh['result']
101
+
102
+ return 0 if result.nil? || result.empty?
103
+
104
+ result['duration'] || 0
105
+ end # method step_duration
106
+
107
+ def step_status hsh
108
+ return nil if hsh.nil? || hsh.empty?
109
+
110
+ result = hsh['result']
111
+
112
+ return nil if result.nil? || result.empty?
113
+
114
+ result['status']
115
+ end # method step_status
116
+ end # class
117
+ end # module
118
+ end # module
@@ -0,0 +1,191 @@
1
+ # lib/sleeping_king_studios/tasks/ci/cucumber_results.rb
2
+
3
+ require 'sleeping_king_studios/tasks/ci'
4
+
5
+ module SleepingKingStudios::Tasks::Ci
6
+ # rubocop:disable Metrics/ClassLength
7
+
8
+ # Encapsulates the results of a Cucumber call.
9
+ class CucumberResults
10
+ # @param results [Hash] The raw results of the RSpec call.
11
+ def initialize results
12
+ @results = results
13
+ end # constructor
14
+
15
+ # @param other [RSpecResults] The other results object to compare.
16
+ #
17
+ # @return [Boolean] True if the results are equal, otherwise false.
18
+ def == other
19
+ if other.is_a?(Hash)
20
+ empty? ? other.empty? : to_h == other
21
+ elsif other.is_a?(CucumberResults)
22
+ to_h == other.to_h
23
+ else
24
+ false
25
+ end # if-elsif-else
26
+ end # method ==
27
+
28
+ # @return [Float] The duration value.
29
+ def duration
30
+ @results.fetch('duration', 0.0)
31
+ end # method duration
32
+
33
+ # @return [Boolean] True if there are no scenarios, otherwise false.
34
+ def empty?
35
+ scenario_count.zero?
36
+ end # method empty?
37
+
38
+ # @return [Boolean] True if there are any failing scenarios, otherwise
39
+ # false.
40
+ def failing?
41
+ !failing_scenario_count.zero?
42
+ end # method failing?
43
+
44
+ # @return [Integer] The list of failing scenarios.
45
+ def failing_scenarios
46
+ @results.fetch('failing_scenarios', [])
47
+ end # method failing_scenarios
48
+
49
+ # @return [Integer] The number of failing scenarios.
50
+ def failing_scenario_count
51
+ failing_scenarios.count
52
+ end # method failing_scenario_count
53
+
54
+ # @return [Integer] The number of failing steps.
55
+ def failing_step_count
56
+ @results.fetch('failing_step_count', 0)
57
+ end # method failing_step_count
58
+
59
+ # Adds the given result values and returns a new results object with the
60
+ # sums.
61
+ #
62
+ # @param other [RSpecResults] The results to add.
63
+ #
64
+ # @return [RSpecResults] The total results.
65
+ def merge other
66
+ merged = {}
67
+
68
+ keys.each do |key|
69
+ merged[key] = public_send(key) + other.public_send(key)
70
+ end # each
71
+
72
+ self.class.new(merged)
73
+ end # method merge
74
+
75
+ # @return [Boolean] True if there are any pending scenarios, otherwise
76
+ # false.
77
+ def pending?
78
+ !pending_scenario_count.zero?
79
+ end # method pending?
80
+
81
+ # @return [Integer] The number of pending scenarios.
82
+ def pending_scenario_count
83
+ pending_scenarios.count
84
+ end # method pending_scenario_count
85
+
86
+ # @return [Integer] The list of pending scenarios.
87
+ def pending_scenarios
88
+ @results.fetch('pending_scenarios', [])
89
+ end # method pending_scenarios
90
+
91
+ # @return [Integer] The number of pending steps.
92
+ def pending_step_count
93
+ @results.fetch('pending_step_count', 0)
94
+ end # method pending_step_count
95
+
96
+ # @return [Integer] The total number of scenarios.
97
+ def scenario_count
98
+ @results.fetch('scenario_count', 0)
99
+ end # method scenario_count
100
+
101
+ # @return [String] A brief summary of the scenario results.
102
+ def scenarios_summary
103
+ build_summary(
104
+ 'scenario',
105
+ scenario_count,
106
+ failing_scenario_count,
107
+ pending_scenario_count
108
+ ) # end build_summary
109
+ end # method scenarios_summary
110
+
111
+ # @return [Integer] The total number of steps.
112
+ def step_count
113
+ @results.fetch('step_count', 0)
114
+ end # method step_count
115
+
116
+ # @return [String] A brief summary of the scenario results.
117
+ def steps_summary
118
+ build_summary(
119
+ 'step',
120
+ step_count,
121
+ failing_step_count,
122
+ pending_step_count
123
+ ) # end build_summary
124
+ end # method steps_summary
125
+
126
+ # @return [String] A brief summary of the results.
127
+ def summary
128
+ str = scenarios_summary
129
+
130
+ str << ', '
131
+
132
+ str << steps_summary
133
+
134
+ str << " in #{duration.round(2)} seconds"
135
+ end # method summary
136
+ alias_method :to_s, :summary
137
+
138
+ # @return [Hash] The hash representation of the results.
139
+ def to_h
140
+ hsh = {}
141
+
142
+ keys.each { |key| hsh[key] = public_send(key) }
143
+
144
+ hsh
145
+ end # method to_h
146
+
147
+ private
148
+
149
+ def build_summary name, count, failing_count, pending_count
150
+ str = pluralize(count, name)
151
+
152
+ return str if failing_count.zero? && pending_count.zero?
153
+
154
+ str << build_summary_details(failing_count, pending_count)
155
+ end # method build_summary
156
+
157
+ def build_summary_details failing_count, pending_count
158
+ str = ' ('
159
+
160
+ str << pluralize(failing_count, 'failure') unless failing_count.zero?
161
+
162
+ str << ', ' if !failing_count.zero? && !pending_count.zero?
163
+
164
+ str << "#{pending_count} pending" unless pending_count.zero?
165
+
166
+ str << ')'
167
+ end # method build_summary_details
168
+
169
+ def keys
170
+ %w(
171
+ duration
172
+ step_count
173
+ pending_step_count
174
+ failing_step_count
175
+ scenario_count
176
+ pending_scenarios
177
+ failing_scenarios
178
+ ) # end keys
179
+ end # method keys
180
+
181
+ def pluralize count, singular, plural = nil
182
+ "#{count} #{tools.integer.pluralize count, singular, plural}"
183
+ end # method pluralize
184
+
185
+ def tools
186
+ SleepingKingStudios::Tools::Toolbelt.instance
187
+ end # method tools
188
+ end # class
189
+
190
+ # rubocop:enable Metrics/ClassLength
191
+ end # module