sleeping_king_studios-tasks 0.1.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +21 -0
- data/DEVELOPMENT.md +4 -0
- data/README.md +6 -6
- data/lib/sleeping_king_studios/tasks/apps.rb +1 -1
- data/lib/sleeping_king_studios/tasks/apps/app_configuration.rb +1 -1
- data/lib/sleeping_king_studios/tasks/apps/ci/results_reporter.rb +8 -6
- data/lib/sleeping_king_studios/tasks/apps/ci/rspec_task.rb +1 -1
- data/lib/sleeping_king_studios/tasks/apps/ci/rubocop_task.rb +1 -1
- data/lib/sleeping_king_studios/tasks/ci/cucumber_parser.rb +4 -3
- data/lib/sleeping_king_studios/tasks/ci/cucumber_results.rb +3 -3
- data/lib/sleeping_king_studios/tasks/ci/cucumber_task.rb +1 -1
- data/lib/sleeping_king_studios/tasks/ci/eslint_results.rb +112 -0
- data/lib/sleeping_king_studios/tasks/ci/eslint_runner.rb +51 -0
- data/lib/sleeping_king_studios/tasks/ci/eslint_task.rb +32 -0
- data/lib/sleeping_king_studios/tasks/ci/jest_results.rb +106 -0
- data/lib/sleeping_king_studios/tasks/ci/jest_runner.rb +51 -0
- data/lib/sleeping_king_studios/tasks/ci/jest_task.rb +43 -0
- data/lib/sleeping_king_studios/tasks/ci/rspec_each_results.rb +1 -1
- data/lib/sleeping_king_studios/tasks/ci/rspec_each_task.rb +18 -2
- data/lib/sleeping_king_studios/tasks/ci/rspec_results.rb +1 -1
- data/lib/sleeping_king_studios/tasks/ci/rspec_task.rb +12 -2
- data/lib/sleeping_king_studios/tasks/ci/rubocop_task.rb +1 -1
- data/lib/sleeping_king_studios/tasks/ci/simplecov_results.rb +4 -5
- data/lib/sleeping_king_studios/tasks/ci/steps_runner.rb +1 -1
- data/lib/sleeping_king_studios/tasks/ci/steps_task.rb +1 -1
- data/lib/sleeping_king_studios/tasks/ci/tasks.thor +4 -0
- data/lib/sleeping_king_studios/tasks/configuration.rb +25 -4
- data/lib/sleeping_king_studios/tasks/file/new_task.rb +16 -14
- data/lib/sleeping_king_studios/tasks/file/templates/rspec.erb +13 -0
- data/lib/sleeping_king_studios/tasks/file/templates/ruby.erb +28 -0
- data/lib/sleeping_king_studios/tasks/process_runner.rb +15 -5
- data/lib/sleeping_king_studios/tasks/task.rb +2 -2
- data/lib/sleeping_king_studios/tasks/version.rb +2 -2
- data/lib/sleeping_king_studios/tools/toolbox/configuration.rb +280 -0
- metadata +31 -37
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ddfbfc40ee6294af3eecd6e729ea837d4744f193e516320348de5410885db19e
|
4
|
+
data.tar.gz: ebd8165cbfeb62882af9d327d01632cd3eb56c8c8c89e9ade902281198858846
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 943e07b91a67a3dcef63beb1115c1c36ff4d0f302c700d10811bbd50770229104cf79918872fc26fa4d1959345f7b20dd84250bfb7faced58cbeff0a8b64a8f6
|
7
|
+
data.tar.gz: b820d7614f276d330d109422704e26dd0a6b694a86d949fd0607ab6c4730b439b58b2250e81cc9ca104d7abdcf53bd86dae9805460d0608cb656a509dfd93c8d
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,26 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 0.4.1
|
4
|
+
|
5
|
+
- Add gem compatibility with SleepingKingStudios::Tools 1.0 release.
|
6
|
+
|
7
|
+
## 0.4.0
|
8
|
+
|
9
|
+
- Fix deprecations from SleepingKingStudios::Tools.
|
10
|
+
- Add support for Ruby 3.0.
|
11
|
+
|
12
|
+
## 0.3.0
|
13
|
+
|
14
|
+
- Fix deprecation warnings in Ruby 2.7.
|
15
|
+
- Add support for Thor 1.0.
|
16
|
+
|
17
|
+
## 0.2.0
|
18
|
+
|
19
|
+
### Ci
|
20
|
+
|
21
|
+
- Implement tasks for Jest.js, ESLint.
|
22
|
+
- Add --format option, configuration value for RSpec, RSpec (Each) tasks.
|
23
|
+
|
3
24
|
## 0.1.0
|
4
25
|
|
5
26
|
Initial commit.
|
data/DEVELOPMENT.md
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# SleepingKingStudios::Tasks [![Build Status](https://travis-ci.org/sleepingkingstudios/sleeping_king_studios-tasks.svg?branch=master)](https://travis-ci.org/sleepingkingstudios/sleeping_king_studios-tasks)
|
2
2
|
|
3
|
+
**Note:** This project is deprecated, and will not receive further feature updates.
|
4
|
+
|
3
5
|
A toolkit providing an encapsulation layer around the Thor CLI library, with predefined tasks for development and continuous integration.
|
4
6
|
|
5
7
|
See also [https://github.com/erikhuda/thor](https://github.com/erikhuda/thor).
|
@@ -10,10 +12,6 @@ See also [https://github.com/erikhuda/thor](https://github.com/erikhuda/thor).
|
|
10
12
|
|
11
13
|
The canonical repository for this gem is on [GitHub](https://github.com/sleepingkingstudios/sleeping_king_studios-tasks).
|
12
14
|
|
13
|
-
### A Note From The Developer
|
14
|
-
|
15
|
-
Hi, I'm Rob Smith, a Ruby Engineer and the developer of this library. I use these tools every day, but they're not just written for me. If you find this project helpful in your own work, or if you have any questions, suggestions or critiques, please feel free to get in touch! I can be reached on GitHub (see above, and feel encouraged to submit bug reports or merge requests there) or via email at `merlin@sleepingkingstudios.com`. I look forward to hearing from you!
|
16
|
-
|
17
15
|
## Task Classes
|
18
16
|
|
19
17
|
SleepingKingStudios::Tasks defines a wrapper around the Thor CLI.
|
@@ -169,9 +167,11 @@ In addition, the cucumber step has the additional option:
|
|
169
167
|
|
170
168
|
- `default_files [Array]`: Files that are always loaded when running Cucumber, such as step definitions or support files. By default, this includes 'step_definitions.rb' and the 'step_definitions' directory inside 'features'.
|
171
169
|
|
172
|
-
`config.ci.rspec [Hash]`: Step configuration for the RSpec step. Has the same options as `config.ci.cucumber`, above, except for the aforementioned `default_files` option.
|
170
|
+
`config.ci.rspec [Hash]`: Step configuration for the RSpec step. Has the same options as `config.ci.cucumber`, above, except for the aforementioned `default_files` option. In addition, the RSpec task has the following option:
|
171
|
+
|
172
|
+
- `format [String]`: The RSpec formatter used to format the spec results. Defaults to 'documentation'.
|
173
173
|
|
174
|
-
`config.ci.rspec_each [Hash]`: Step configuration for the RSpec Each step.
|
174
|
+
`config.ci.rspec_each [Hash]`: Step configuration for the RSpec Each step. Has the same configuration options as the RSpec step.
|
175
175
|
|
176
176
|
`config.ci.rubocop [Hash]`: Step configuration for the RuboCop step.
|
177
177
|
|
@@ -49,7 +49,7 @@ module SleepingKingStudios::Tasks::Apps
|
|
49
49
|
|
50
50
|
define_method :short_name do
|
51
51
|
# rubocop:disable Style/RedundantSelf
|
52
|
-
tools.
|
52
|
+
tools.str.underscore(self.name.gsub(/\s+/, '_'))
|
53
53
|
# rubocop:enable Style/RedundantSelf
|
54
54
|
end # define_method
|
55
55
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# lib/sleeping_king_studios/tasks/apps/ci/results_reporter.rb
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'forwardable'
|
4
4
|
|
5
5
|
require 'sleeping_king_studios/tasks/apps/ci'
|
6
6
|
require 'sleeping_king_studios/tasks/ci/results_helpers'
|
@@ -9,21 +9,23 @@ module SleepingKingStudios::Tasks::Apps::Ci
|
|
9
9
|
# Reports on the results of a multi-application continuous integration
|
10
10
|
# process, printing the step results grouped by application.
|
11
11
|
class ResultsReporter
|
12
|
-
extend
|
12
|
+
extend Forwardable
|
13
13
|
|
14
14
|
include SleepingKingStudios::Tasks::Ci::ResultsHelpers
|
15
15
|
|
16
|
-
|
17
|
-
:applications,
|
16
|
+
def_delegators :@context,
|
18
17
|
:print_table,
|
19
18
|
:say,
|
20
|
-
:set_color
|
21
|
-
:to => :@context
|
19
|
+
:set_color
|
22
20
|
|
23
21
|
def initialize context
|
24
22
|
@context = context
|
25
23
|
end # method initialize
|
26
24
|
|
25
|
+
def applications
|
26
|
+
@context.send(:applications)
|
27
|
+
end
|
28
|
+
|
27
29
|
def call results
|
28
30
|
width = 1 + heading_width(results)
|
29
31
|
|
@@ -22,7 +22,7 @@ module SleepingKingStudios::Tasks::Apps::Ci
|
|
22
22
|
|
23
23
|
def call *applications
|
24
24
|
SleepingKingStudios::Tasks::Apps::Ci::StepsTask.
|
25
|
-
new(options.merge('only' => %w
|
25
|
+
new(options.merge('only' => %w[rspec])).
|
26
26
|
call(*applications)
|
27
27
|
end # method call
|
28
28
|
end # class
|
@@ -22,7 +22,7 @@ module SleepingKingStudios::Tasks::Apps::Ci
|
|
22
22
|
|
23
23
|
def call *applications
|
24
24
|
SleepingKingStudios::Tasks::Apps::Ci::StepsTask.
|
25
|
-
new(options.merge('only' => %w
|
25
|
+
new(options.merge('only' => %w[rubocop])).
|
26
26
|
call(*applications)
|
27
27
|
end # method call
|
28
28
|
end # class
|
@@ -65,11 +65,12 @@ module SleepingKingStudios::Tasks::Ci
|
|
65
65
|
|
66
66
|
status = step_status(step) || 'failed'
|
67
67
|
|
68
|
-
|
68
|
+
case status
|
69
|
+
when 'failed'
|
69
70
|
report['failing_step_count'] += 1
|
70
|
-
|
71
|
+
when 'pending', 'skipped'
|
71
72
|
report['pending_step_count'] += 1
|
72
|
-
end
|
73
|
+
end
|
73
74
|
|
74
75
|
status
|
75
76
|
end # method parse_step
|
@@ -167,7 +167,7 @@ module SleepingKingStudios::Tasks::Ci
|
|
167
167
|
end # method build_summary_details
|
168
168
|
|
169
169
|
def keys
|
170
|
-
%w
|
170
|
+
%w[
|
171
171
|
duration
|
172
172
|
step_count
|
173
173
|
pending_step_count
|
@@ -175,11 +175,11 @@ module SleepingKingStudios::Tasks::Ci
|
|
175
175
|
scenario_count
|
176
176
|
pending_scenarios
|
177
177
|
failing_scenarios
|
178
|
-
|
178
|
+
] # end keys
|
179
179
|
end # method keys
|
180
180
|
|
181
181
|
def pluralize count, singular, plural = nil
|
182
|
-
"#{count} #{tools.
|
182
|
+
"#{count} #{tools.int.pluralize count, singular, plural}"
|
183
183
|
end # method pluralize
|
184
184
|
|
185
185
|
def tools
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sleeping_king_studios/tasks/ci'
|
4
|
+
|
5
|
+
module SleepingKingStudios::Tasks::Ci
|
6
|
+
# Encapsulates the results of an Eslint call.
|
7
|
+
class EslintResults
|
8
|
+
# @param results [Hash] The raw results of the Eslint call.
|
9
|
+
def initialize results
|
10
|
+
@results = results
|
11
|
+
end
|
12
|
+
|
13
|
+
# @param other [EslintResults] The other results object to compare.
|
14
|
+
#
|
15
|
+
# @return [Boolean] True if the results are equal, otherwise false.
|
16
|
+
def == other
|
17
|
+
if other.is_a?(Array)
|
18
|
+
empty? ? other.empty? : @results == other
|
19
|
+
elsif other.is_a?(EslintResults)
|
20
|
+
to_h == other.to_h
|
21
|
+
else
|
22
|
+
false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Boolean] True if there are no inspected files, otherwise false.
|
27
|
+
def empty?
|
28
|
+
inspected_file_count.zero?
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Integer] The total number of error results across all files.
|
32
|
+
def error_count
|
33
|
+
@error_count ||= @results.map { |hsh| hsh['errorCount'] }.sum
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Boolean] True if there are no errors or warnings, otherwise
|
37
|
+
# false.
|
38
|
+
def failing?
|
39
|
+
!(error_count.zero? && warning_count.zero?)
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Integer] The number of inspected files.
|
43
|
+
def inspected_file_count
|
44
|
+
@results.size
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Boolean] Always false. Both warnings and errors trigger a failure
|
48
|
+
# state.
|
49
|
+
def pending?
|
50
|
+
false
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Hash] The hash representation of the results.
|
54
|
+
def to_h
|
55
|
+
{
|
56
|
+
'inspected_file_count' => inspected_file_count,
|
57
|
+
'error_count' => error_count,
|
58
|
+
'warning_count' => warning_count
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [String] The string representation of the results.
|
63
|
+
def to_s # rubocop:disable Metrics/AbcSize
|
64
|
+
str = +"#{pluralize inspected_file_count, 'file'} inspected"
|
65
|
+
|
66
|
+
str << ", #{pluralize error_count, 'error'}"
|
67
|
+
|
68
|
+
str << ", #{pluralize warning_count, 'warning'}"
|
69
|
+
|
70
|
+
str << "\n" unless non_empty_results.empty?
|
71
|
+
|
72
|
+
non_empty_results.each do |hsh|
|
73
|
+
str << "\n #{format_result_item(hsh)}"
|
74
|
+
end
|
75
|
+
|
76
|
+
str
|
77
|
+
end
|
78
|
+
|
79
|
+
# @return [Integer] The total number of warning results across all files.
|
80
|
+
def warning_count
|
81
|
+
@warning_count ||= @results.map { |hsh| hsh['warningCount'] }.sum
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def format_result_item hsh
|
87
|
+
str = +relative_path(hsh['filePath'])
|
88
|
+
|
89
|
+
str << ": #{pluralize hsh['errorCount'], 'error'}"
|
90
|
+
|
91
|
+
str << ", #{pluralize hsh['warningCount'], 'warning'}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def non_empty_results
|
95
|
+
@results.reject do |hsh|
|
96
|
+
hsh['errorCount'].zero? && hsh['warningCount'].zero?
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def pluralize count, singular, plural = nil
|
101
|
+
"#{count} #{tools.int.pluralize count, singular, plural}"
|
102
|
+
end
|
103
|
+
|
104
|
+
def relative_path path
|
105
|
+
path.sub(/\A#{Dir.getwd}#{File::SEPARATOR}?/, '')
|
106
|
+
end
|
107
|
+
|
108
|
+
def tools
|
109
|
+
SleepingKingStudios::Tools::Toolbelt.instance
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sleeping_king_studios/tasks/ci'
|
4
|
+
require 'sleeping_king_studios/tasks/process_runner'
|
5
|
+
|
6
|
+
module SleepingKingStudios::Tasks::Ci
|
7
|
+
# Service object to run Eslint as an external process with the specified
|
8
|
+
# parameters.
|
9
|
+
class EslintRunner < SleepingKingStudios::Tasks::ProcessRunner
|
10
|
+
def call env: {}, files: [], options: [], report: true
|
11
|
+
report = 'tmp/ci/eslint.json' if report && !report.is_a?(String)
|
12
|
+
command =
|
13
|
+
build_command(
|
14
|
+
:env => env,
|
15
|
+
:files => files,
|
16
|
+
:options => options,
|
17
|
+
:report => report
|
18
|
+
)
|
19
|
+
|
20
|
+
stream_process(command)
|
21
|
+
|
22
|
+
report ? load_report(:report => report) : []
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def base_command
|
28
|
+
'yarn eslint'
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_options files:, options:, report:, **_kwargs
|
32
|
+
files = default_files if files.empty?
|
33
|
+
options += ['--format=json', "--output-file=#{report}"] if report
|
34
|
+
|
35
|
+
super :files => files, :options => options
|
36
|
+
end
|
37
|
+
|
38
|
+
def default_files
|
39
|
+
SleepingKingStudios::Tasks.configuration.ci.eslint.
|
40
|
+
fetch(:default_files, '"**/*.js"')
|
41
|
+
end
|
42
|
+
|
43
|
+
def load_report report:
|
44
|
+
raw = File.read report
|
45
|
+
|
46
|
+
JSON.parse raw
|
47
|
+
rescue
|
48
|
+
[]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sleeping_king_studios/tasks/ci'
|
4
|
+
require 'sleeping_king_studios/tasks/ci/eslint_results'
|
5
|
+
require 'sleeping_king_studios/tasks/ci/eslint_runner'
|
6
|
+
|
7
|
+
module SleepingKingStudios::Tasks::Ci
|
8
|
+
# Defines a Thor task for running the Eslint linter.
|
9
|
+
class EslintTask < SleepingKingStudios::Tasks::Task
|
10
|
+
def self.description
|
11
|
+
'Runs the ESLint linter.'
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.task_name
|
15
|
+
'eslint'
|
16
|
+
end
|
17
|
+
|
18
|
+
def call *files
|
19
|
+
results = eslint_runner.call(:files => files)
|
20
|
+
|
21
|
+
EslintResults.new(results)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def eslint_runner
|
27
|
+
opts = %w[--color]
|
28
|
+
|
29
|
+
EslintRunner.new(:options => opts)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sleeping_king_studios/tasks/ci'
|
4
|
+
|
5
|
+
module SleepingKingStudios::Tasks::Ci
|
6
|
+
# Encapsulates the results of a Jest call.
|
7
|
+
class JestResults
|
8
|
+
# @param results [Hash] The raw results of the Jest call.
|
9
|
+
def initialize results
|
10
|
+
@results = results
|
11
|
+
end
|
12
|
+
|
13
|
+
# @param other [JestResults] The other results object to compare.
|
14
|
+
#
|
15
|
+
# @return [Boolean] True if the results are equal, otherwise false.
|
16
|
+
def == other
|
17
|
+
if other.is_a?(Hash)
|
18
|
+
empty? ? other.empty? : @results == other
|
19
|
+
elsif other.is_a?(JestResults)
|
20
|
+
to_h == other.to_h
|
21
|
+
else
|
22
|
+
false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Float] The duration value, in seconds.
|
27
|
+
def duration
|
28
|
+
(end_time - start_time).to_f / 1000
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Boolean] True if there are no tests, otherwise false.
|
32
|
+
def empty?
|
33
|
+
test_count.zero?
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Boolean] True if there are any failing tests, otherwise false.
|
37
|
+
def failing?
|
38
|
+
!failure_count.zero?
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Integer] The number of failing tests.
|
42
|
+
def failure_count
|
43
|
+
@results.fetch('numFailedTests', 0)
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Boolean] True if there are any pending tests, otherwise false.
|
47
|
+
def pending?
|
48
|
+
!pending_count.zero?
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [Intger] The number of pending tests.
|
52
|
+
def pending_count
|
53
|
+
@results.fetch('numPendingTests', 0)
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Integer] The total number of tests.
|
57
|
+
def test_count
|
58
|
+
@results.fetch('numTotalTests', 0)
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [Hash] The hash representation of the results.
|
62
|
+
def to_h
|
63
|
+
{
|
64
|
+
'duration' => duration,
|
65
|
+
'failure_count' => failure_count,
|
66
|
+
'pending_count' => pending_count,
|
67
|
+
'test_count' => test_count
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [String] The string representation of the results.
|
72
|
+
def to_s # rubocop:disable Metrics/AbcSize
|
73
|
+
str = +pluralize(test_count, 'test')
|
74
|
+
|
75
|
+
str << ', ' << pluralize(failure_count, 'failure')
|
76
|
+
|
77
|
+
str << ', ' << pending_count.to_s << ' pending' if pending?
|
78
|
+
|
79
|
+
str << " in #{duration.round(2)} seconds"
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def end_time
|
85
|
+
return 0 unless @results['testResults']
|
86
|
+
|
87
|
+
@results['testResults'].
|
88
|
+
map { |test_result| test_result['endTime'] }.
|
89
|
+
reduce do |memo, time|
|
90
|
+
[memo, time].max
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def pluralize count, singular, plural = nil
|
95
|
+
"#{count} #{tools.int.pluralize count, singular, plural}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def start_time
|
99
|
+
@results['startTime'] || 0
|
100
|
+
end
|
101
|
+
|
102
|
+
def tools
|
103
|
+
SleepingKingStudios::Tools::Toolbelt.instance
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|