knapsack_pro 0.0.1 → 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 +4 -4
- data/.gitignore +3 -3
- data/CHANGELOG.md +4 -0
- data/README.md +368 -4
- data/bin/knapsack_pro +20 -0
- data/circle.yml +2 -0
- data/knapsack_pro.gemspec +6 -1
- data/lib/knapsack_pro.rb +74 -0
- data/lib/knapsack_pro/adapters/base_adapter.rb +30 -0
- data/lib/knapsack_pro/adapters/cucumber_adapter.rb +40 -0
- data/lib/knapsack_pro/adapters/minitest_adapter.rb +52 -0
- data/lib/knapsack_pro/adapters/rspec_adapter.rb +48 -0
- data/lib/knapsack_pro/allocator.rb +40 -0
- data/lib/knapsack_pro/allocator_builder.rb +40 -0
- data/lib/knapsack_pro/client/api/action.rb +17 -0
- data/lib/knapsack_pro/client/api/v1/base.rb +15 -0
- data/lib/knapsack_pro/client/api/v1/build_distributions.rb +29 -0
- data/lib/knapsack_pro/client/api/v1/build_subsets.rb +29 -0
- data/lib/knapsack_pro/client/connection.rb +94 -0
- data/lib/knapsack_pro/config/ci/base.rb +22 -0
- data/lib/knapsack_pro/config/ci/buildkite.rb +27 -0
- data/lib/knapsack_pro/config/ci/circle.rb +29 -0
- data/lib/knapsack_pro/config/ci/semaphore.rb +28 -0
- data/lib/knapsack_pro/config/env.rb +111 -0
- data/lib/knapsack_pro/logger_wrapper.rb +15 -0
- data/lib/knapsack_pro/presenter.rb +25 -0
- data/lib/knapsack_pro/report.rb +20 -0
- data/lib/knapsack_pro/repository_adapter_initiator.rb +12 -0
- data/lib/knapsack_pro/repository_adapters/base_adapter.rb +14 -0
- data/lib/knapsack_pro/repository_adapters/env_adapter.rb +13 -0
- data/lib/knapsack_pro/repository_adapters/git_adapter.rb +19 -0
- data/lib/knapsack_pro/runners/base_runner.rb +31 -0
- data/lib/knapsack_pro/runners/cucumber_runner.rb +16 -0
- data/lib/knapsack_pro/runners/minitest_runner.rb +26 -0
- data/lib/knapsack_pro/runners/rspec_runner.rb +16 -0
- data/lib/knapsack_pro/task_loader.rb +11 -0
- data/lib/knapsack_pro/test_file_cleaner.rb +7 -0
- data/lib/knapsack_pro/test_file_finder.rb +33 -0
- data/lib/knapsack_pro/test_file_pattern.rb +7 -0
- data/lib/knapsack_pro/test_file_presenter.rb +11 -0
- data/lib/knapsack_pro/test_flat_distributor.rb +84 -0
- data/lib/knapsack_pro/tracker.rb +64 -0
- data/lib/knapsack_pro/version.rb +1 -1
- data/lib/tasks/cucumber.rake +7 -0
- data/lib/tasks/minitest.rake +7 -0
- data/lib/tasks/rspec.rake +7 -0
- data/spec/fixtures/vcr_cassettes/api/v1/build_distributions/subset/invalid_test_suite_token.yml +50 -0
- data/spec/fixtures/vcr_cassettes/api/v1/build_distributions/subset/success.yml +52 -0
- data/spec/fixtures/vcr_cassettes/api/v1/build_subsets/create/invalid_test_suite_token.yml +50 -0
- data/spec/fixtures/vcr_cassettes/api/v1/build_subsets/create/success.yml +50 -0
- data/spec/integration/api/build_distributions_subset_spec.rb +74 -0
- data/spec/integration/api/build_subsets_create_spec.rb +76 -0
- data/spec/knapsack_pro/adapters/base_adapter_spec.rb +63 -0
- data/spec/knapsack_pro/adapters/cucumber_adapter_spec.rb +71 -0
- data/spec/knapsack_pro/adapters/minitest_adapter_spec.rb +107 -0
- data/spec/knapsack_pro/adapters/rspec_adapter_spec.rb +94 -0
- data/spec/knapsack_pro/allocator_builder_spec.rb +45 -0
- data/spec/knapsack_pro/allocator_spec.rb +80 -0
- data/spec/knapsack_pro/client/api/action_spec.rb +17 -0
- data/spec/knapsack_pro/client/api/v1/base_spec.rb +2 -0
- data/spec/knapsack_pro/client/api/v1/build_distributions_spec.rb +35 -0
- data/spec/knapsack_pro/client/api/v1/build_subsets_spec.rb +35 -0
- data/spec/knapsack_pro/client/connection_spec.rb +138 -0
- data/spec/knapsack_pro/config/ci/base_spec.rb +7 -0
- data/spec/knapsack_pro/config/ci/buildkite_spec.rb +74 -0
- data/spec/knapsack_pro/config/ci/circle_spec.rb +74 -0
- data/spec/knapsack_pro/config/ci/semaphore_spec.rb +74 -0
- data/spec/knapsack_pro/config/env_spec.rb +368 -0
- data/spec/knapsack_pro/logger_wrapper_spec.rb +15 -0
- data/spec/knapsack_pro/presenter_spec.rb +57 -0
- data/spec/knapsack_pro/report_spec.rb +68 -0
- data/spec/knapsack_pro/repository_adapter_initiator_spec.rb +21 -0
- data/spec/knapsack_pro/repository_adapters/base_adapter_spec.rb +13 -0
- data/spec/knapsack_pro/repository_adapters/env_adapter_spec.rb +27 -0
- data/spec/knapsack_pro/repository_adapters/git_adapter_spec.rb +27 -0
- data/spec/knapsack_pro/runners/base_runner_spec.rb +61 -0
- data/spec/knapsack_pro/runners/cucumber_runner_spec.rb +47 -0
- data/spec/knapsack_pro/runners/minitest_runner_spec.rb +34 -0
- data/spec/knapsack_pro/runners/rspec_runner_spec.rb +49 -0
- data/spec/knapsack_pro/task_loader_spec.rb +14 -0
- data/spec/knapsack_pro/test_file_cleaner_spec.rb +11 -0
- data/spec/knapsack_pro/test_file_finder_spec.rb +35 -0
- data/spec/knapsack_pro/test_file_pattern_spec.rb +23 -0
- data/spec/knapsack_pro/test_file_presenter_spec.rb +22 -0
- data/spec/knapsack_pro/test_flat_distributor_spec.rb +60 -0
- data/spec/knapsack_pro/tracker_spec.rb +102 -0
- data/spec/knapsack_pro_spec.rb +56 -0
- data/spec/spec_helper.rb +11 -1
- data/spec/support/fakes/cucumber.rb +12 -0
- data/spec/support/fakes/minitest.rb +12 -0
- data/spec/support/shared_examples/adapter.rb +17 -0
- data/spec_fake/controllers/users_controller_spec.rb +0 -0
- data/spec_fake/models/admin_spec.rb +0 -0
- data/spec_fake/models/user_spec.rb +0 -0
- data/spec_fake/spec_helper.rb +0 -0
- data/test_fake/a_test.rb +0 -0
- data/test_fake/b_test.rb +0 -0
- metadata +212 -12
- data/Gemfile.lock +0 -69
- data/spec/knapsack_spec.rb +0 -3
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
module KnapsackPro
|
|
2
|
+
module Client
|
|
3
|
+
class Connection
|
|
4
|
+
TIMEOUT = 15
|
|
5
|
+
|
|
6
|
+
def initialize(action)
|
|
7
|
+
@action = action
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
send(action.http_method)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def success?
|
|
15
|
+
!!response
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def errors?
|
|
19
|
+
!!(response && (response['errors'] || response['error']))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :action, :response
|
|
25
|
+
|
|
26
|
+
def logger
|
|
27
|
+
KnapsackPro.logger
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def endpoint
|
|
31
|
+
KnapsackPro::Config::Env.endpoint
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def endpoint_url
|
|
35
|
+
endpoint + action.endpoint_path
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def request_hash
|
|
39
|
+
action
|
|
40
|
+
.request_hash
|
|
41
|
+
.merge({
|
|
42
|
+
test_suite_token: test_suite_token
|
|
43
|
+
})
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def request_body
|
|
47
|
+
request_hash.to_json
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def test_suite_token
|
|
51
|
+
KnapsackPro::Config::Env.test_suite_token
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def json_headers
|
|
55
|
+
{
|
|
56
|
+
'Content-Type' => 'application/json',
|
|
57
|
+
'Accept' => 'application/json'
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def parse_response(body)
|
|
62
|
+
return '' if body == '' || body.nil?
|
|
63
|
+
JSON.parse(body)
|
|
64
|
+
rescue JSON::ParserError
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def post
|
|
69
|
+
uri = URI.parse(endpoint_url)
|
|
70
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
71
|
+
http.use_ssl = (uri.scheme == 'https')
|
|
72
|
+
http.open_timeout = TIMEOUT
|
|
73
|
+
http.read_timeout = TIMEOUT
|
|
74
|
+
|
|
75
|
+
http_response = http.post(uri.path, request_body, json_headers)
|
|
76
|
+
@response = parse_response(http_response.body)
|
|
77
|
+
|
|
78
|
+
request_uuid = http_response.header['X-Request-Id']
|
|
79
|
+
|
|
80
|
+
logger.info("API request UUID: #{request_uuid}")
|
|
81
|
+
logger.info('API response:')
|
|
82
|
+
if errors?
|
|
83
|
+
logger.error(response)
|
|
84
|
+
else
|
|
85
|
+
logger.info(response)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
response
|
|
89
|
+
rescue Errno::ECONNREFUSED, EOFError, Net::OpenTimeout, Net::ReadTimeout => e
|
|
90
|
+
logger.warn(e.inspect)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module KnapsackPro
|
|
2
|
+
module Config
|
|
3
|
+
module CI
|
|
4
|
+
class Buildkite < Base
|
|
5
|
+
def node_total
|
|
6
|
+
ENV['BUILDKITE_PARALLEL_JOB_COUNT']
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def node_index
|
|
10
|
+
ENV['BUILDKITE_PARALLEL_JOB']
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def commit_hash
|
|
14
|
+
ENV['BUILDKITE_COMMIT']
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def branch
|
|
18
|
+
ENV['BUILDKITE_BRANCH']
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def project_dir
|
|
22
|
+
ENV['BUILDKITE_BUILD_CHECKOUT_PATH']
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module KnapsackPro
|
|
2
|
+
module Config
|
|
3
|
+
module CI
|
|
4
|
+
class Circle < Base
|
|
5
|
+
def node_total
|
|
6
|
+
ENV['CIRCLE_NODE_TOTAL']
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def node_index
|
|
10
|
+
ENV['CIRCLE_NODE_INDEX']
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def commit_hash
|
|
14
|
+
ENV['CIRCLE_SHA1']
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def branch
|
|
18
|
+
ENV['CIRCLE_BRANCH']
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def project_dir
|
|
22
|
+
project_repo_name = ENV['CIRCLE_PROJECT_REPONAME']
|
|
23
|
+
"/home/ubuntu/#{project_repo_name}" if project_repo_name
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module KnapsackPro
|
|
2
|
+
module Config
|
|
3
|
+
module CI
|
|
4
|
+
class Semaphore < Base
|
|
5
|
+
def node_total
|
|
6
|
+
ENV['SEMAPHORE_THREAD_COUNT']
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def node_index
|
|
10
|
+
index = ENV['SEMAPHORE_CURRENT_THREAD']
|
|
11
|
+
index.to_i - 1 if index
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def commit_hash
|
|
15
|
+
ENV['REVISION']
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def branch
|
|
19
|
+
ENV['BRANCH_NAME']
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def project_dir
|
|
23
|
+
ENV['SEMAPHORE_PROJECT_DIR']
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
module KnapsackPro
|
|
2
|
+
module Config
|
|
3
|
+
class Env
|
|
4
|
+
class << self
|
|
5
|
+
def ci_node_total
|
|
6
|
+
(ENV['KNAPSACK_PRO_CI_NODE_TOTAL'] ||
|
|
7
|
+
ci_env_for(:node_total) ||
|
|
8
|
+
1).to_i
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def ci_node_index
|
|
12
|
+
(ENV['KNAPSACK_PRO_CI_NODE_INDEX'] ||
|
|
13
|
+
ci_env_for(:node_index) ||
|
|
14
|
+
0).to_i
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def commit_hash
|
|
18
|
+
ENV['KNAPSACK_PRO_COMMIT_HASH'] ||
|
|
19
|
+
ci_env_for(:commit_hash)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def branch
|
|
23
|
+
ENV['KNAPSACK_PRO_BRANCH'] ||
|
|
24
|
+
ci_env_for(:branch)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def project_dir
|
|
28
|
+
ENV['KNAPSACK_PRO_PROJECT_DIR'] ||
|
|
29
|
+
ci_env_for(:project_dir)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_file_pattern
|
|
33
|
+
ENV['KNAPSACK_PRO_TEST_FILE_PATTERN']
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def repository_adapter
|
|
37
|
+
ENV['KNAPSACK_PRO_REPOSITORY_ADAPTER']
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def recording_enabled
|
|
41
|
+
ENV['KNAPSACK_PRO_RECORDING_ENABLED']
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def recording_enabled?
|
|
45
|
+
recording_enabled == 'true'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def endpoint
|
|
49
|
+
env_name = 'KNAPSACK_PRO_ENDPOINT'
|
|
50
|
+
return ENV[env_name] if ENV[env_name]
|
|
51
|
+
|
|
52
|
+
case mode
|
|
53
|
+
when :development
|
|
54
|
+
'http://api.knapsackpro.dev:3000'
|
|
55
|
+
when :test
|
|
56
|
+
'http://api-staging.knapsackpro.com'
|
|
57
|
+
when :production
|
|
58
|
+
'http://api.knapsackpro.com'
|
|
59
|
+
else
|
|
60
|
+
required_env(env_name)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_suite_token
|
|
65
|
+
required_env('KNAPSACK_PRO_TEST_SUITE_TOKEN')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_suite_token_rspec
|
|
69
|
+
ENV['KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC']
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_suite_token_minitest
|
|
73
|
+
ENV['KNAPSACK_PRO_TEST_SUITE_TOKEN_MINITEST']
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_suite_token_cucumber
|
|
77
|
+
ENV['KNAPSACK_PRO_TEST_SUITE_TOKEN_CUCUMBER']
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def mode
|
|
81
|
+
mode = ENV['KNAPSACK_PRO_MODE']
|
|
82
|
+
return :production if mode.nil?
|
|
83
|
+
mode = mode.to_sym
|
|
84
|
+
if [:development, :test, :production].include?(mode)
|
|
85
|
+
mode
|
|
86
|
+
else
|
|
87
|
+
raise ArgumentError.new('Wrong mode name')
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def ci_env_for(env_name)
|
|
92
|
+
value = nil
|
|
93
|
+
ci_list = KnapsackPro::Config::CI.constants - [:Base]
|
|
94
|
+
ci_list.each do |ci_name|
|
|
95
|
+
ci_class = Object.const_get("KnapsackPro::Config::CI::#{ci_name}")
|
|
96
|
+
ci = ci_class.new
|
|
97
|
+
value = ci.send(env_name)
|
|
98
|
+
break unless value.nil?
|
|
99
|
+
end
|
|
100
|
+
value
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def required_env(env_name)
|
|
106
|
+
ENV[env_name] || raise("Missing environment variable #{env_name}")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module KnapsackPro
|
|
2
|
+
class LoggerWrapper
|
|
3
|
+
def initialize(logger)
|
|
4
|
+
@logger = ::ActiveSupport::TaggedLogging.new(logger)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
attr_reader :logger
|
|
10
|
+
|
|
11
|
+
def method_missing(m, *args, &block)
|
|
12
|
+
logger.tagged('knapsack_pro') { logger.send(m, *args, &block) }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module KnapsackPro
|
|
2
|
+
class Presenter
|
|
3
|
+
class << self
|
|
4
|
+
def global_time
|
|
5
|
+
global_time = pretty_seconds(KnapsackPro.tracker.global_time)
|
|
6
|
+
"Global time execution for tests: #{global_time}"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def pretty_seconds(seconds)
|
|
10
|
+
sign = ''
|
|
11
|
+
|
|
12
|
+
if seconds < 0
|
|
13
|
+
seconds = seconds*-1
|
|
14
|
+
sign = '-'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
return "#{sign}#{seconds}s" if seconds.abs < 1
|
|
18
|
+
|
|
19
|
+
time = Time.at(seconds).gmtime.strftime('%Hh %Mm %Ss')
|
|
20
|
+
time_without_zeros = time.gsub(/00(h|m|s)/, '').strip
|
|
21
|
+
sign + time_without_zeros
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module KnapsackPro
|
|
2
|
+
class Report
|
|
3
|
+
def self.save
|
|
4
|
+
repository_adapter = KnapsackPro::RepositoryAdapterInitiator.call
|
|
5
|
+
action = KnapsackPro::Client::API::V1::BuildSubsets.create(
|
|
6
|
+
commit_hash: repository_adapter.commit_hash,
|
|
7
|
+
branch: repository_adapter.branch,
|
|
8
|
+
node_total: KnapsackPro::Config::Env.ci_node_total,
|
|
9
|
+
node_index: KnapsackPro::Config::Env.ci_node_index,
|
|
10
|
+
test_files: KnapsackPro.tracker.to_a,
|
|
11
|
+
)
|
|
12
|
+
connection = KnapsackPro::Client::Connection.new(action)
|
|
13
|
+
response = connection.call
|
|
14
|
+
if connection.success?
|
|
15
|
+
raise ArgumentError.new(response) if connection.errors?
|
|
16
|
+
KnapsackPro.logger.info('Saved time execution report on API server.')
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module KnapsackPro
|
|
2
|
+
class RepositoryAdapterInitiator
|
|
3
|
+
def self.call
|
|
4
|
+
case KnapsackPro::Config::Env.repository_adapter
|
|
5
|
+
when 'git'
|
|
6
|
+
KnapsackPro::RepositoryAdapters::GitAdapter.new
|
|
7
|
+
else
|
|
8
|
+
KnapsackPro::RepositoryAdapters::EnvAdapter.new
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module KnapsackPro
|
|
2
|
+
module RepositoryAdapters
|
|
3
|
+
class GitAdapter < BaseAdapter
|
|
4
|
+
def commit_hash
|
|
5
|
+
`git -C "#{working_dir}" rev-parse HEAD`.strip
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def branch
|
|
9
|
+
`git -C "#{working_dir}" rev-parse --abbrev-ref HEAD`.strip
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def working_dir
|
|
15
|
+
KnapsackPro::Config::Env.project_dir
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|