pact_broker-client 1.32.0 → 1.37.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/.github/workflows/release_gem.yml +1 -0
- data/.github/workflows/test.yml +23 -0
- data/CHANGELOG.md +48 -0
- data/README.md +15 -3
- data/Rakefile +2 -0
- data/doc/pacts/markdown/Pact Broker Client - Pact Broker.md +331 -8
- data/lib/pact_broker/client.rb +1 -1
- data/lib/pact_broker/client/backports.rb +13 -0
- data/lib/pact_broker/client/cli/broker.rb +73 -28
- data/lib/pact_broker/client/cli/can_i_deploy_long_desc.txt +18 -9
- data/lib/pact_broker/client/cli/custom_thor.rb +9 -12
- data/lib/pact_broker/client/git.rb +43 -22
- data/lib/pact_broker/client/hal/entity.rb +27 -3
- data/lib/pact_broker/client/hal/http_client.rb +4 -0
- data/lib/pact_broker/client/hal/links.rb +39 -0
- data/lib/pact_broker/client/hal_client_methods.rb +11 -0
- data/lib/pact_broker/client/hash_refinements.rb +19 -0
- data/lib/pact_broker/client/matrix.rb +2 -1
- data/lib/pact_broker/client/matrix/text_formatter.rb +46 -11
- data/lib/pact_broker/client/publish_pacts.rb +93 -14
- data/lib/pact_broker/client/tasks/publication_task.rb +37 -6
- data/lib/pact_broker/client/version.rb +1 -1
- data/lib/pact_broker/client/versions/record_deployment.rb +109 -0
- data/pact-broker-client.gemspec +2 -0
- data/script/publish-pact.sh +7 -1
- data/script/trigger-release.sh +1 -1
- data/spec/lib/pact_broker/client/cli/broker_can_i_deploy_spec.rb +13 -2
- data/spec/lib/pact_broker/client/cli/broker_publish_spec.rb +108 -12
- data/spec/lib/pact_broker/client/git_spec.rb +39 -2
- data/spec/lib/pact_broker/client/hal/entity_spec.rb +4 -3
- data/spec/lib/pact_broker/client/matrix/text_formatter_spec.rb +29 -4
- data/spec/lib/pact_broker/client/publish_pacts_spec.rb +119 -7
- data/spec/lib/pact_broker/client/tasks/publication_task_spec.rb +88 -10
- data/spec/lib/pact_broker/client/versions/describe_spec.rb +0 -1
- data/spec/lib/pact_broker/client/versions/record_deployment_spec.rb +82 -0
- data/spec/pacts/pact_broker_client-pact_broker.json +335 -8
- data/spec/service_providers/pact_broker_client_create_version_spec.rb +89 -0
- data/spec/service_providers/pact_broker_client_matrix_spec.rb +4 -0
- data/spec/service_providers/pact_broker_client_versions_spec.rb +1 -2
- data/spec/service_providers/record_deployment_spec.rb +219 -0
- data/spec/support/matrix.json +6 -1
- data/spec/support/matrix.txt +3 -3
- data/spec/support/matrix_error.txt +3 -3
- data/spec/support/matrix_with_results.txt +10 -0
- data/tasks/pact.rake +2 -0
- metadata +44 -4
- data/.travis.yml +0 -11
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'delegate'
|
3
|
+
|
4
|
+
module PactBroker
|
5
|
+
module Client
|
6
|
+
module Hal
|
7
|
+
class Links
|
8
|
+
def initialize(href, key, links)
|
9
|
+
@href = href
|
10
|
+
@key = key
|
11
|
+
@links = links
|
12
|
+
end
|
13
|
+
|
14
|
+
def names
|
15
|
+
@names ||= links.collect(&:name).compact.uniq
|
16
|
+
end
|
17
|
+
|
18
|
+
def find!(name, not_found_message = nil)
|
19
|
+
link = find(name)
|
20
|
+
if link
|
21
|
+
link
|
22
|
+
else
|
23
|
+
message = not_found_message || "Could not find relation '#{key}' with name '#{name}' in resource at #{href}."
|
24
|
+
available_options = names.any? ? names.join(", ") : "<none found>"
|
25
|
+
raise RelationNotFoundError.new(message.chomp(".") + ". Available options: #{available_options}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def find(name)
|
30
|
+
links.find{ | link | link.name == name }
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
attr_reader :links, :key, :href
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'pact_broker/client/hal'
|
2
|
+
require 'pact_broker/client/retry'
|
2
3
|
|
3
4
|
module PactBroker
|
4
5
|
module Client
|
@@ -10,6 +11,16 @@ module PactBroker
|
|
10
11
|
def create_http_client(pact_broker_client_options)
|
11
12
|
PactBroker::Client::Hal::HttpClient.new(pact_broker_client_options.merge(pact_broker_client_options[:basic_auth] || {}))
|
12
13
|
end
|
14
|
+
|
15
|
+
def index_entry_point
|
16
|
+
@index_entry_point ||= create_index_entry_point(pact_broker_base_url, pact_broker_client_options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def index_resource
|
20
|
+
@index_resource ||= Retry.while_error do
|
21
|
+
index_entry_point.get!
|
22
|
+
end
|
23
|
+
end
|
13
24
|
end
|
14
25
|
end
|
15
26
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module PactBroker
|
2
|
+
module Client
|
3
|
+
module HashRefinements
|
4
|
+
refine Hash do
|
5
|
+
def compact
|
6
|
+
h = {}
|
7
|
+
each do |key, value|
|
8
|
+
h[key] = value unless value == nil
|
9
|
+
end
|
10
|
+
h
|
11
|
+
end unless Hash.method_defined? :compact
|
12
|
+
|
13
|
+
def compact!
|
14
|
+
reject! {|_key, value| value == nil}
|
15
|
+
end unless Hash.method_defined? :compact!
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -53,10 +53,11 @@ module PactBroker
|
|
53
53
|
opts[:success] = [*options[:success]]
|
54
54
|
end
|
55
55
|
opts[:limit] = options[:limit] if options[:limit]
|
56
|
+
opts[:environment] = options[:to_environment] if options[:to_environment]
|
56
57
|
if options[:to_tag]
|
57
58
|
opts[:latest] = 'true'
|
58
59
|
opts[:tag] = options[:to_tag]
|
59
|
-
elsif selectors.size == 1
|
60
|
+
elsif selectors.size == 1 && !options[:to_environment]
|
60
61
|
opts[:latest] = 'true'
|
61
62
|
end
|
62
63
|
opts
|
@@ -1,41 +1,76 @@
|
|
1
1
|
require 'table_print'
|
2
|
+
require 'dig_rb'
|
3
|
+
require 'pact_broker/client/hash_refinements'
|
2
4
|
|
3
5
|
module PactBroker
|
4
6
|
module Client
|
5
7
|
class Matrix
|
6
8
|
class TextFormatter
|
9
|
+
using PactBroker::Client::HashRefinements
|
7
10
|
|
8
|
-
Line = Struct.new(:consumer, :consumer_version, :provider, :provider_version, :success)
|
11
|
+
Line = Struct.new(:consumer, :consumer_version, :provider, :provider_version, :success, :ref)
|
9
12
|
|
10
13
|
OPTIONS = [
|
11
14
|
{ consumer: {} },
|
12
15
|
{ consumer_version: {display_name: 'C.VERSION'} },
|
13
16
|
{ provider: {} },
|
14
17
|
{ provider_version: {display_name: 'P.VERSION'} },
|
15
|
-
{ success: {display_name: 'SUCCESS?'} }
|
18
|
+
{ success: {display_name: 'SUCCESS?'} },
|
19
|
+
{ ref: { display_name: 'RESULT#' }}
|
16
20
|
]
|
17
21
|
|
18
22
|
def self.call(matrix)
|
19
23
|
matrix_rows = matrix[:matrix]
|
20
24
|
return "" if matrix_rows.size == 0
|
21
|
-
|
25
|
+
verification_result_number = 0
|
26
|
+
data = matrix_rows.each_with_index.collect do | line |
|
27
|
+
has_verification_result_url = lookup(line, nil, :verificationResult, :_links, :self, :href)
|
28
|
+
if has_verification_result_url
|
29
|
+
verification_result_number += 1
|
30
|
+
end
|
22
31
|
Line.new(
|
23
|
-
lookup(line, :consumer, :name),
|
24
|
-
lookup(line, :consumer, :version, :number),
|
25
|
-
lookup(line, :provider, :name),
|
26
|
-
lookup(line, :provider, :version, :number),
|
27
|
-
lookup(line, :verificationResult, :success).to_s
|
32
|
+
lookup(line, "???", :consumer, :name),
|
33
|
+
lookup(line, "???", :consumer, :version, :number),
|
34
|
+
lookup(line, "???", :provider, :name) ,
|
35
|
+
lookup(line, "???", :provider, :version, :number),
|
36
|
+
(lookup(line, "???", :verificationResult, :success)).to_s,
|
37
|
+
has_verification_result_url ? verification_result_number : ""
|
28
38
|
)
|
29
39
|
end
|
30
40
|
|
31
41
|
printer = TablePrint::Printer.new(data, OPTIONS)
|
32
|
-
printer.table_print
|
42
|
+
printer.table_print + verification_result_urls_text(matrix)
|
33
43
|
end
|
34
44
|
|
35
|
-
def self.lookup line, *keys
|
45
|
+
def self.lookup line, default, *keys
|
36
46
|
keys.reduce(line) { | line, key | line[key] }
|
37
47
|
rescue NoMethodError
|
38
|
-
|
48
|
+
default
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.verification_results_urls_and_successes(matrix)
|
52
|
+
(matrix[:matrix] || []).collect do | row |
|
53
|
+
url = row.dig(:verificationResult, :_links, :self, :href)
|
54
|
+
if url
|
55
|
+
success = row.dig(:verificationResult, :success)
|
56
|
+
[url, success]
|
57
|
+
else
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
end.compact
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.verification_result_urls_text(matrix)
|
64
|
+
text = self.verification_results_urls_and_successes(matrix).each_with_index.collect do |(url, success), i|
|
65
|
+
status = success ? 'success' : 'failure'
|
66
|
+
"#{i+1}. #{url} (#{status})"
|
67
|
+
end.join("\n")
|
68
|
+
|
69
|
+
if text.size > 0
|
70
|
+
"\n\nVERIFICATION RESULTS\n--------------------\n#{text}"
|
71
|
+
else
|
72
|
+
text
|
73
|
+
end
|
39
74
|
end
|
40
75
|
end
|
41
76
|
end
|
@@ -4,39 +4,64 @@ require 'pact_broker/client/retry'
|
|
4
4
|
require 'pact_broker/client/pact_file'
|
5
5
|
require 'pact_broker/client/pact_hash'
|
6
6
|
require 'pact_broker/client/merge_pacts'
|
7
|
+
require 'pact_broker/client/hal_client_methods'
|
8
|
+
require 'pact_broker/client/hash_refinements'
|
7
9
|
|
8
10
|
module PactBroker
|
9
11
|
module Client
|
10
12
|
class PublishPacts
|
13
|
+
using PactBroker::Client::HashRefinements
|
14
|
+
include HalClientMethods
|
11
15
|
|
12
|
-
def self.call(pact_broker_base_url, pact_file_paths,
|
13
|
-
new(pact_broker_base_url, pact_file_paths,
|
16
|
+
def self.call(pact_broker_base_url, pact_file_paths, consumer_version_params, pact_broker_client_options={})
|
17
|
+
new(pact_broker_base_url, pact_file_paths, consumer_version_params, pact_broker_client_options).call
|
14
18
|
end
|
15
19
|
|
16
|
-
def initialize pact_broker_base_url, pact_file_paths,
|
20
|
+
def initialize pact_broker_base_url, pact_file_paths, consumer_version_params, pact_broker_client_options={}
|
17
21
|
@pact_broker_base_url = pact_broker_base_url
|
18
22
|
@pact_file_paths = pact_file_paths
|
19
|
-
@
|
20
|
-
@
|
23
|
+
@consumer_version_number = consumer_version_params[:number].respond_to?(:strip) ? consumer_version_params[:number].strip : consumer_version_params[:number]
|
24
|
+
@branch = consumer_version_params[:branch]
|
25
|
+
@build_url = consumer_version_params[:build_url]
|
26
|
+
@tags = consumer_version_params[:tags] ? consumer_version_params[:tags].collect{ |tag| tag.respond_to?(:strip) ? tag.strip : tag } : []
|
27
|
+
@version_required = consumer_version_params[:version_required]
|
21
28
|
@pact_broker_client_options = pact_broker_client_options
|
22
29
|
end
|
23
30
|
|
24
31
|
def call
|
25
32
|
validate
|
26
33
|
$stdout.puts("")
|
27
|
-
result = apply_tags && publish_pacts
|
34
|
+
result = create_consumer_versions && apply_tags && publish_pacts
|
28
35
|
$stdout.puts("")
|
29
36
|
result
|
30
37
|
end
|
31
38
|
|
32
39
|
private
|
33
40
|
|
34
|
-
attr_reader :pact_broker_base_url, :pact_file_paths, :
|
41
|
+
attr_reader :pact_broker_base_url, :pact_file_paths, :consumer_version_number, :branch, :tags, :build_url, :pact_broker_client_options, :version_required
|
35
42
|
|
36
43
|
def pact_broker_client
|
37
44
|
@pact_broker_client ||= PactBroker::Client::PactBrokerClient.new(base_url: pact_broker_base_url, client_options: pact_broker_client_options)
|
38
45
|
end
|
39
46
|
|
47
|
+
def index_entry_point
|
48
|
+
@index_entry_point ||= create_index_entry_point(pact_broker_base_url, pact_broker_client_options)
|
49
|
+
end
|
50
|
+
|
51
|
+
def index_resource
|
52
|
+
@index_resource ||= Retry.while_error do
|
53
|
+
index_entry_point.get!
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def can_create_version_with_branch?
|
58
|
+
@can_create_version_with_branch ||= index_resource.can?('pb:pacticipant-version')
|
59
|
+
end
|
60
|
+
|
61
|
+
def merge_on_server?
|
62
|
+
pact_broker_client_options[:write] == :merge
|
63
|
+
end
|
64
|
+
|
40
65
|
def publish_pacts
|
41
66
|
pact_files.group_by(&:pact_name).collect do | pact_name, pact_files |
|
42
67
|
$stdout.puts "Merging #{pact_files.collect(&:path).join(", ")}" if pact_files.size > 1
|
@@ -66,8 +91,58 @@ module PactBroker
|
|
66
91
|
end
|
67
92
|
end
|
68
93
|
|
94
|
+
def create_consumer_versions
|
95
|
+
if create_versions?
|
96
|
+
consumer_names.collect do | consumer_name |
|
97
|
+
create_version(index_resource, consumer_name)
|
98
|
+
end
|
99
|
+
true
|
100
|
+
else
|
101
|
+
true
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def create_versions?
|
106
|
+
if version_required
|
107
|
+
if can_create_version_with_branch?
|
108
|
+
true
|
109
|
+
else
|
110
|
+
raise PactBroker::Client::Error.new("This version of the Pact Broker does not support versions with branches or build URLs. Please upgrade your broker to 2.76.2 or later.")
|
111
|
+
end
|
112
|
+
elsif (branch || build_url) && can_create_version_with_branch?
|
113
|
+
true
|
114
|
+
else
|
115
|
+
false
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def create_version(index_resource, consumer_name)
|
120
|
+
Retry.while_error do
|
121
|
+
version_resource = index_resource._link('pb:pacticipant-version').expand(version: consumer_version_number, pacticipant: consumer_name).put(version_body).assert_success!
|
122
|
+
message = if version_resource.response.status == 200
|
123
|
+
"Replaced version #{consumer_version_number} of #{consumer_name}"
|
124
|
+
else
|
125
|
+
"Created version #{consumer_version_number} of #{consumer_name}"
|
126
|
+
end
|
127
|
+
|
128
|
+
message = message + " (branch #{branch})" if branch
|
129
|
+
$stdout.puts message
|
130
|
+
if version_resource.response.status == 200
|
131
|
+
$stdout.puts ::Term::ANSIColor.yellow("Replacing the version resource is not recommended under normal circumstances and may indicate that you have not configured your Pact pipeline correctly (unless you are just re-running a build for a particular commit). For more information see https://docs.pact.io/versioning")
|
132
|
+
end
|
133
|
+
true
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def version_body
|
138
|
+
{
|
139
|
+
branch: branch,
|
140
|
+
buildUrl: build_url
|
141
|
+
}.compact
|
142
|
+
end
|
143
|
+
|
69
144
|
def apply_tags
|
70
|
-
return true if tags.
|
145
|
+
return true if tags.empty?
|
71
146
|
tags.all? do | tag |
|
72
147
|
tag_consumer_version tag
|
73
148
|
end
|
@@ -77,8 +152,8 @@ module PactBroker
|
|
77
152
|
versions = pact_broker_client.pacticipants.versions
|
78
153
|
Retry.while_error do
|
79
154
|
consumer_names.collect do | consumer_name |
|
80
|
-
versions.tag(pacticipant: consumer_name, version:
|
81
|
-
$stdout.puts "Tagged version #{
|
155
|
+
versions.tag(pacticipant: consumer_name, version: consumer_version_number, tag: tag)
|
156
|
+
$stdout.puts "Tagged version #{consumer_version_number} of #{consumer_name} as #{tag.inspect}"
|
82
157
|
true
|
83
158
|
end
|
84
159
|
end
|
@@ -90,18 +165,22 @@ module PactBroker
|
|
90
165
|
def publish_pact_contents(pact)
|
91
166
|
Retry.while_error do
|
92
167
|
pacts = pact_broker_client.pacticipants.versions.pacts
|
93
|
-
if pacts.version_published?(consumer: pact.consumer_name, provider: pact.provider_name, consumer_version:
|
94
|
-
|
168
|
+
if pacts.version_published?(consumer: pact.consumer_name, provider: pact.provider_name, consumer_version: consumer_version_number)
|
169
|
+
if merge_on_server?
|
170
|
+
$stdout.puts "A pact for this consumer version is already published. Merging contents."
|
171
|
+
else
|
172
|
+
$stdout.puts ::Term::ANSIColor.yellow("A pact for this consumer version is already published. Overwriting. (Note: Overwriting pacts is not recommended as it can lead to race conditions. Best practice is to provide a unique consumer version number for each publication. For more information, see https://docs.pact.io/versioning)")
|
173
|
+
end
|
95
174
|
end
|
96
175
|
|
97
|
-
latest_pact_url = pacts.publish(pact_hash: pact, consumer_version:
|
176
|
+
latest_pact_url = pacts.publish(pact_hash: pact, consumer_version: consumer_version_number)
|
98
177
|
$stdout.puts "The latest version of this pact can be accessed at the following URL:\n#{latest_pact_url}"
|
99
178
|
true
|
100
179
|
end
|
101
180
|
end
|
102
181
|
|
103
182
|
def validate
|
104
|
-
raise PactBroker::Client::Error.new("Please specify the
|
183
|
+
raise PactBroker::Client::Error.new("Please specify the consumer_version_number") unless (consumer_version_number && consumer_version_number.to_s.strip.size > 0)
|
105
184
|
raise PactBroker::Client::Error.new("Please specify the pact_broker_base_url") unless (pact_broker_base_url && pact_broker_base_url.to_s.strip.size > 0)
|
106
185
|
raise PactBroker::Client::Error.new("No pact files found") unless (pact_file_paths && pact_file_paths.any?)
|
107
186
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'rake/tasklib'
|
2
2
|
require 'pact_broker/client/git'
|
3
|
+
require 'pact_broker/client/hash_refinements'
|
3
4
|
|
4
5
|
=begin
|
5
6
|
require pact_broker/client/tasks
|
@@ -17,31 +18,52 @@ end
|
|
17
18
|
module PactBroker
|
18
19
|
module Client
|
19
20
|
class PublicationTask < ::Rake::TaskLib
|
21
|
+
using PactBroker::Client::HashRefinements
|
20
22
|
|
21
23
|
attr_accessor :pattern, :pact_broker_base_url, :consumer_version, :tag, :write_method, :tag_with_git_branch, :pact_broker_basic_auth, :pact_broker_token
|
24
|
+
attr_reader :auto_detect_version_properties, :branch, :build_url
|
22
25
|
alias_method :tags=, :tag=
|
23
26
|
alias_method :tags, :tag
|
24
27
|
|
25
28
|
def initialize name = nil, &block
|
26
29
|
@name = name
|
30
|
+
@auto_detect_version_properties = nil
|
31
|
+
@version_required = false
|
27
32
|
@pattern = 'spec/pacts/*.json'
|
28
33
|
@pact_broker_base_url = 'http://pact-broker'
|
29
34
|
rake_task &block
|
30
35
|
end
|
31
36
|
|
37
|
+
def auto_detect_version_properties= auto_detect_version_properties
|
38
|
+
@version_required = version_required || auto_detect_version_properties
|
39
|
+
@auto_detect_version_properties = auto_detect_version_properties
|
40
|
+
end
|
41
|
+
|
42
|
+
def branch= branch
|
43
|
+
@version_required = version_required || !!branch
|
44
|
+
@branch = branch
|
45
|
+
end
|
46
|
+
|
47
|
+
def build_url= build_url
|
48
|
+
@version_required = version_required || !!build_url
|
49
|
+
@build_url = build_url
|
50
|
+
end
|
51
|
+
|
32
52
|
private
|
33
53
|
|
54
|
+
attr_reader :version_required
|
55
|
+
|
34
56
|
def rake_task &block
|
35
57
|
namespace :pact do
|
36
58
|
desc "Publish pacts to pact broker"
|
37
59
|
task task_name do
|
38
60
|
block.call(self)
|
39
61
|
require 'pact_broker/client/publish_pacts'
|
40
|
-
pact_broker_client_options =
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
success = PactBroker::Client::PublishPacts.new(pact_broker_base_url, FileList[pattern],
|
62
|
+
pact_broker_client_options = { write: write_method, token: pact_broker_token }
|
63
|
+
pact_broker_client_options[:basic_auth] = pact_broker_basic_auth if pact_broker_basic_auth && pact_broker_basic_auth.any?
|
64
|
+
pact_broker_client_options.compact!
|
65
|
+
consumer_version_params = { number: consumer_version, branch: the_branch, build_url: build_url, tags: all_tags, version_required: version_required }.compact
|
66
|
+
success = PactBroker::Client::PublishPacts.new(pact_broker_base_url, FileList[pattern], consumer_version_params, pact_broker_client_options).call
|
45
67
|
raise "One or more pacts failed to be published" unless success
|
46
68
|
end
|
47
69
|
end
|
@@ -53,9 +75,18 @@ module PactBroker
|
|
53
75
|
|
54
76
|
def all_tags
|
55
77
|
t = [*tags]
|
56
|
-
t << PactBroker::Client::Git.branch if tag_with_git_branch
|
78
|
+
t << PactBroker::Client::Git.branch(raise_error: true) if tag_with_git_branch
|
57
79
|
t.compact.uniq
|
58
80
|
end
|
81
|
+
|
82
|
+
def the_branch
|
83
|
+
if branch.nil? && auto_detect_version_properties != false
|
84
|
+
PactBroker::Client::Git.branch(raise_error: auto_detect_version_properties == true)
|
85
|
+
else
|
86
|
+
branch
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
59
90
|
end
|
60
91
|
end
|
61
92
|
end
|