beaker 1.7.0 → 1.8.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 +5 -13
- data/README.md +14 -375
- data/lib/beaker.rb +1 -1
- data/lib/beaker/answers.rb +1 -1
- data/lib/beaker/answers/version20.rb +1 -0
- data/lib/beaker/answers/version28.rb +2 -1
- data/lib/beaker/answers/version30.rb +1 -0
- data/lib/beaker/answers/version32.rb +3 -1
- data/lib/beaker/dsl/helpers.rb +24 -3
- data/lib/beaker/dsl/install_utils.rb +6 -4
- data/lib/beaker/host.rb +1 -2
- data/lib/beaker/hypervisor/fusion.rb +10 -2
- data/lib/beaker/hypervisor/vcloud_pooled.rb +3 -0
- data/lib/beaker/options/parser.rb +19 -21
- data/lib/beaker/options/presets.rb +3 -1
- data/lib/beaker/platform.rb +59 -0
- data/lib/beaker/ssh_connection.rb +2 -0
- data/lib/beaker/utils/ntp_control.rb +26 -11
- data/lib/beaker/utils/validator.rb +14 -5
- data/lib/beaker/version.rb +1 -1
- data/spec/beaker/answers_spec.rb +39 -1
- data/spec/beaker/dsl/helpers_spec.rb +30 -5
- data/spec/beaker/options/parser_spec.rb +4 -25
- data/spec/beaker/platform_spec.rb +97 -0
- data/spec/beaker/utils/ntp_control_spec.rb +28 -0
- data/spec/beaker/utils/validator_spec.rb +33 -0
- data/spec/helpers.rb +3 -2
- data/spec/mocks.rb +5 -0
- metadata +18 -15
data/lib/beaker/dsl/helpers.rb
CHANGED
@@ -70,7 +70,10 @@ module Beaker
|
|
70
70
|
# @raise [FailTest] Raises an exception if *command* obviously fails.
|
71
71
|
def on(host, command, opts = {}, &block)
|
72
72
|
unless command.is_a? Command
|
73
|
-
cmd_opts =
|
73
|
+
cmd_opts = {}
|
74
|
+
if opts[:environment]
|
75
|
+
cmd_opts['ENV'] = opts[:environment]
|
76
|
+
end
|
74
77
|
command = Command.new(command.to_s, [], cmd_opts)
|
75
78
|
end
|
76
79
|
if host.is_a? String or host.is_a? Symbol
|
@@ -82,7 +85,16 @@ module Beaker
|
|
82
85
|
@result = host.exec(command, opts)
|
83
86
|
|
84
87
|
# Also, let additional checking be performed by the caller.
|
85
|
-
|
88
|
+
if block_given?
|
89
|
+
case block.arity
|
90
|
+
#block with arity of 0, just hand back yourself
|
91
|
+
when 0
|
92
|
+
yield self
|
93
|
+
#block with arity of 1 or greater, hand back the result object
|
94
|
+
else
|
95
|
+
yield @result
|
96
|
+
end
|
97
|
+
end
|
86
98
|
|
87
99
|
return @result
|
88
100
|
end
|
@@ -453,7 +465,8 @@ module Beaker
|
|
453
465
|
# @api dsl
|
454
466
|
def with_puppet_running_on host, conf_opts, testdir = host.tmpdir(File.basename(@path)), &block
|
455
467
|
raise(ArgumentError, "with_puppet_running_on's conf_opts must be a Hash. You provided a #{conf_opts.class}: '#{conf_opts}'") if !conf_opts.kind_of?(Hash)
|
456
|
-
cmdline_args = conf_opts
|
468
|
+
cmdline_args = conf_opts[:__commandline_args__]
|
469
|
+
conf_opts = conf_opts.reject { |k,v| k == :__commandline_args__ }
|
457
470
|
|
458
471
|
begin
|
459
472
|
backup_file = backup_the_file(host, host['puppetpath'], testdir, 'puppet.conf')
|
@@ -486,6 +499,14 @@ module Beaker
|
|
486
499
|
end
|
487
500
|
|
488
501
|
rescue Exception => teardown_exception
|
502
|
+
begin
|
503
|
+
if !host.is_pe?
|
504
|
+
dump_puppet_log(host)
|
505
|
+
end
|
506
|
+
rescue Exception => dumping_exception
|
507
|
+
logger.error("Raised during attempt to dump puppet logs: #{dumping_exception}")
|
508
|
+
end
|
509
|
+
|
489
510
|
if original_exception
|
490
511
|
logger.error("Raised during attempt to teardown with_puppet_running_on: #{teardown_exception}\n---\n")
|
491
512
|
raise original_exception
|
@@ -453,10 +453,10 @@ module Beaker
|
|
453
453
|
hosts.each do |host|
|
454
454
|
host['pe_dir'] ||= options[:pe_dir]
|
455
455
|
if host['platform'] =~ /windows/
|
456
|
-
host['pe_ver'] = host['pe_ver'] ||
|
456
|
+
host['pe_ver'] = host['pe_ver'] || options['pe_ver'] ||
|
457
457
|
Beaker::Options::PEVersionScraper.load_pe_version(host[:pe_dir], options[:pe_version_file_win])
|
458
458
|
else
|
459
|
-
host['pe_ver'] = host['pe_ver'] ||
|
459
|
+
host['pe_ver'] = host['pe_ver'] || options['pe_ver'] ||
|
460
460
|
Beaker::Options::PEVersionScraper.load_pe_version(host[:pe_dir], options[:pe_version_file])
|
461
461
|
end
|
462
462
|
end
|
@@ -478,9 +478,11 @@ module Beaker
|
|
478
478
|
hosts.each do |host|
|
479
479
|
host['pe_dir'] = host['pe_upgrade_dir'] || path
|
480
480
|
if host['platform'] =~ /windows/
|
481
|
-
host['pe_ver'] = host['pe_upgrade_ver'] ||
|
481
|
+
host['pe_ver'] = host['pe_upgrade_ver'] || options['pe_upgrade_ver'] ||
|
482
|
+
Options::PEVersionScraper.load_pe_version(host['pe_dir'], options[:pe_version_file_win])
|
482
483
|
else
|
483
|
-
host['pe_ver'] = host['pe_upgrade_ver'] ||
|
484
|
+
host['pe_ver'] = host['pe_upgrade_ver'] || options['pe_upgrade_ver'] ||
|
485
|
+
Options::PEVersionScraper.load_pe_version(host['pe_dir'], options[:pe_version_file])
|
484
486
|
end
|
485
487
|
if version_is_less(host['pe_ver'], '3.0')
|
486
488
|
host['pe_installer'] ||= 'puppet-enterprise-upgrader'
|
data/lib/beaker/host.rb
CHANGED
@@ -180,8 +180,7 @@ module Beaker
|
|
180
180
|
# exit codes at the host level and then raising...
|
181
181
|
# is it necessary to break execution??
|
182
182
|
unless result.exit_code_in?(Array(options[:acceptable_exit_codes] || 0))
|
183
|
-
|
184
|
-
raise CommandFailure, "Host '#{self}' exited with #{result.exit_code} running:\n #{cmdline}\nLast #{limit} lines of output were:\n#{result.formatted_output(limit)}"
|
183
|
+
raise CommandFailure, "Host '#{self}' exited with #{result.exit_code} running:\n #{cmdline}\nLast #{@options[:trace_limit]} lines of output were:\n#{result.formatted_output(@options[:trace_limit])}"
|
185
184
|
end
|
186
185
|
end
|
187
186
|
# Danger, so we have to return this result?
|
@@ -11,6 +11,10 @@ module Beaker
|
|
11
11
|
@logger = options[:logger]
|
12
12
|
@options = options
|
13
13
|
@fusion_hosts = fusion_hosts
|
14
|
+
#check preconditions for fusion
|
15
|
+
@fusion_hosts.each do |host|
|
16
|
+
raise "You must specify a snapshot for Fusion instances, no snapshot defined for #{host.name}!" unless host["snapshot"]
|
17
|
+
end
|
14
18
|
@fission = Fission::VM
|
15
19
|
end
|
16
20
|
|
@@ -23,10 +27,14 @@ module Beaker
|
|
23
27
|
vm = @fission.new vm_name
|
24
28
|
raise "Could not find VM '#{vm_name}' for #{host.name}!" unless vm.exists?
|
25
29
|
|
26
|
-
|
30
|
+
vm_snapshots = vm.snapshots.data
|
31
|
+
if vm_snapshots.nil? or vm_snapshots.empty?
|
32
|
+
raise "No snapshots available for VM #{host.name} (vmname: '#{vm_name}')"
|
33
|
+
end
|
34
|
+
|
35
|
+
available_snapshots = vm_snapshots.sort.join(", ")
|
27
36
|
@logger.notify "Available snapshots for #{host.name}: #{available_snapshots}"
|
28
37
|
snap_name = host["snapshot"]
|
29
|
-
raise "No snapshot specified for #{host.name}" unless snap_name
|
30
38
|
raise "Could not find snapshot '#{snap_name}' for host #{host.name}!" unless vm.snapshots.data.include? snap_name
|
31
39
|
|
32
40
|
@logger.notify "Reverting #{host.name} to snapshot '#{snap_name}'"
|
@@ -55,6 +55,9 @@ module Beaker
|
|
55
55
|
start = Time.now
|
56
56
|
try = 1
|
57
57
|
@vcloud_hosts.each_with_index do |h, i|
|
58
|
+
if not h['template']
|
59
|
+
raise ArgumentError, "You must specify a template name for #{h}"
|
60
|
+
end
|
58
61
|
if h['template'] =~ /\//
|
59
62
|
templatefolders = h['template'].split('/')
|
60
63
|
h['template'] = templatefolders.pop
|
@@ -5,14 +5,12 @@ module Beaker
|
|
5
5
|
#An Object that parses, merges and normalizes all supported Beaker options and arguments
|
6
6
|
class Parser
|
7
7
|
GITREPO = 'git://github.com/puppetlabs'
|
8
|
-
#These options can have the form of arg1,arg2 or [arg] or just arg,
|
8
|
+
#These options can have the form of arg1,arg2 or [arg] or just arg,
|
9
9
|
#should default to []
|
10
10
|
LONG_OPTS = [:helper, :load_path, :tests, :pre_suite, :post_suite, :install, :modules]
|
11
11
|
#These options expand out into an array of .rb files
|
12
12
|
RB_FILE_OPTS = [:tests, :pre_suite, :post_suite]
|
13
13
|
|
14
|
-
PLATFORMS = /^(centos|fedora|debian|oracle|redhat|scientific|sles|ubuntu|windows|solaris|aix|el)\-.+\-.+$/
|
15
|
-
|
16
14
|
PARSE_ERROR = if RUBY_VERSION > '1.8.7'; then Psych::SyntaxError; else ArgumentError; end
|
17
15
|
|
18
16
|
#The OptionsHash of all parsed options
|
@@ -41,7 +39,7 @@ module Beaker
|
|
41
39
|
# or can become an array of multiple values by splitting arg over ','. If argument is already an
|
42
40
|
# array that array is returned untouched.
|
43
41
|
# @example
|
44
|
-
# split_arg([1, 2, 3]) == [1, 2, 3]
|
42
|
+
# split_arg([1, 2, 3]) == [1, 2, 3]
|
45
43
|
# split_arg(1) == [1]
|
46
44
|
# split_arg("1,2") == ["1", "2"]
|
47
45
|
# split_arg(nil) == []
|
@@ -71,9 +69,9 @@ module Beaker
|
|
71
69
|
files = []
|
72
70
|
if not paths.empty?
|
73
71
|
paths.each do |root|
|
74
|
-
if File.file?
|
72
|
+
if File.file?(root)
|
75
73
|
files << root
|
76
|
-
|
74
|
+
elsif File.directory?(root) #expand and explore
|
77
75
|
discover_files = Dir.glob(
|
78
76
|
File.join(root, "**/*.rb")
|
79
77
|
).select { |f| File.file?(f) }
|
@@ -81,6 +79,8 @@ module Beaker
|
|
81
79
|
parser_error "empty directory used as an option (#{root})!"
|
82
80
|
end
|
83
81
|
files += discover_files.sort
|
82
|
+
else #not a file, not a directory, not nothin'
|
83
|
+
parser_error "#{root} used as a file option but is not a file or directory!"
|
84
84
|
end
|
85
85
|
end
|
86
86
|
end
|
@@ -93,9 +93,9 @@ module Beaker
|
|
93
93
|
#Converts array of paths into array of fully qualified git repo URLS with expanded keywords
|
94
94
|
#
|
95
95
|
#Supports the following keywords
|
96
|
-
# PUPPET
|
96
|
+
# PUPPET
|
97
97
|
# FACTER
|
98
|
-
# HIERA
|
98
|
+
# HIERA
|
99
99
|
# HIERA-PUPPET
|
100
100
|
#@example
|
101
101
|
# opts = ["PUPPET/3.1"]
|
@@ -142,7 +142,7 @@ module Beaker
|
|
142
142
|
#NOTE on argument precedence:
|
143
143
|
#
|
144
144
|
# Will use env, then hosts/config file, then command line, then file options
|
145
|
-
#
|
145
|
+
#
|
146
146
|
@options = Beaker::Options::Presets.presets
|
147
147
|
cmd_line_options = @command_line_parser.parse!(args)
|
148
148
|
file_options = Beaker::Options::OptionsFileParser.parse_options_file(cmd_line_options[:options_file])
|
@@ -150,7 +150,7 @@ module Beaker
|
|
150
150
|
# overwrite file options with command line options
|
151
151
|
cmd_line_and_file_options = file_options.merge(cmd_line_options)
|
152
152
|
# merge command line and file options with defaults
|
153
|
-
# overwrite defaults with command line and file options
|
153
|
+
# overwrite defaults with command line and file options
|
154
154
|
@options = @options.merge(cmd_line_and_file_options)
|
155
155
|
|
156
156
|
if not @options[:help] and not @options[:version]
|
@@ -206,9 +206,7 @@ module Beaker
|
|
206
206
|
if not @options['HOSTS'][name]['platform']
|
207
207
|
parser_error "Host #{name} does not have a platform specified"
|
208
208
|
else
|
209
|
-
|
210
|
-
parser_error "Host #{name} is on unsupported platform #{@options['HOSTS'][name]['platform']}"
|
211
|
-
end
|
209
|
+
@options['HOSTS'][name]['platform'] = Platform.new(@options['HOSTS'][name]['platform'])
|
212
210
|
end
|
213
211
|
end
|
214
212
|
|
@@ -231,8 +229,8 @@ module Beaker
|
|
231
229
|
else
|
232
230
|
@options[opt] = []
|
233
231
|
end
|
234
|
-
end
|
235
|
-
|
232
|
+
end
|
233
|
+
|
236
234
|
#check for valid type
|
237
235
|
if @options[:type] !~ /(pe)|(git)|(foss)/
|
238
236
|
parser_error "--type must be one of pe, git, or foss, not '#{@options[:type]}'"
|
@@ -240,7 +238,7 @@ module Beaker
|
|
240
238
|
|
241
239
|
#check for valid fail mode
|
242
240
|
if @options[:fail_mode] !~ /stop|fast|slow/
|
243
|
-
parser_error "--fail-mode must be one of fast or slow, not '#{@options[:fail_mode]}'"
|
241
|
+
parser_error "--fail-mode must be one of fast or slow, not '#{@options[:fail_mode]}'"
|
244
242
|
end
|
245
243
|
|
246
244
|
#check for valid preserve_hosts option
|
@@ -249,8 +247,8 @@ module Beaker
|
|
249
247
|
end
|
250
248
|
|
251
249
|
#check for config files necessary for different hypervisors
|
252
|
-
hypervisors = []
|
253
|
-
@options[:HOSTS].each_key do |name|
|
250
|
+
hypervisors = []
|
251
|
+
@options[:HOSTS].each_key do |name|
|
254
252
|
hypervisors << @options[:HOSTS][name][:hypervisor].to_s
|
255
253
|
end
|
256
254
|
hypervisors.uniq!
|
@@ -266,7 +264,7 @@ module Beaker
|
|
266
264
|
#check that roles of hosts make sense
|
267
265
|
# - must be one and only one master
|
268
266
|
roles = []
|
269
|
-
@options[:HOSTS].each_key do |name|
|
267
|
+
@options[:HOSTS].each_key do |name|
|
270
268
|
roles << @options[:HOSTS][name][:roles]
|
271
269
|
end
|
272
270
|
master = 0
|
@@ -299,8 +297,8 @@ module Beaker
|
|
299
297
|
# @api private
|
300
298
|
def test_host_roles(host_name, host_hash)
|
301
299
|
host_roles = host_hash[:roles]
|
302
|
-
if (host_roles
|
303
|
-
parser_error "#{host_hash[:platform].to_s} box '#{host_name}'
|
300
|
+
if !(host_roles & ['master', 'database', 'dashboard']).empty?
|
301
|
+
parser_error "#{host_hash[:platform].to_s} box '#{host_name}' may not have roles 'master', 'dashboard', or 'database'; it has roles #{host_roles.to_s}"
|
304
302
|
end
|
305
303
|
end
|
306
304
|
|
@@ -8,7 +8,7 @@ module Beaker
|
|
8
8
|
#
|
9
9
|
# Currently supports:
|
10
10
|
#
|
11
|
-
# consoleport, IS_PE, pe_dist_dir, pe_version_file, pe_version_file_win
|
11
|
+
# consoleport, IS_PE, pe_dist_dir, pe_version_file, pe_version_file_win, pe_ver
|
12
12
|
#
|
13
13
|
# @return [OptionsHash] The supported environment variables in an OptionsHash,
|
14
14
|
# empty or nil environment variables are removed from the OptionsHash
|
@@ -20,6 +20,7 @@ module Beaker
|
|
20
20
|
:pe_dir => ENV['pe_dist_dir'],
|
21
21
|
:pe_version_file => ENV['pe_version_file'],
|
22
22
|
:pe_version_file_win => ENV['pe_version_file'],
|
23
|
+
:pe_ver => ENV['pe_ver']
|
23
24
|
}.delete_if {|key, value| value.nil? or value.empty? })
|
24
25
|
end
|
25
26
|
|
@@ -30,6 +31,7 @@ module Beaker
|
|
30
31
|
h = Beaker::Options::OptionsHash.new
|
31
32
|
h.merge({
|
32
33
|
:log_level => 'verbose',
|
34
|
+
:trace_limit => 10,
|
33
35
|
:hosts_file => 'sample.cfg',
|
34
36
|
:options_file => nil,
|
35
37
|
:type => 'pe',
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Beaker
|
2
|
+
class Platform < String
|
3
|
+
#supported platforms
|
4
|
+
PLATFORMS = /^(centos|fedora|debian|oracle|redhat|scientific|sles|ubuntu|windows|solaris|aix|el)\-.+\-.+$/
|
5
|
+
|
6
|
+
PLATFORM_VERSION_CODES =
|
7
|
+
{ :debian => { "wheezy" => "7",
|
8
|
+
"squeeze" => "6",
|
9
|
+
},
|
10
|
+
:ubuntu => { "trusty" => "1404",
|
11
|
+
"saucy" => "1310",
|
12
|
+
"raring" => "1304",
|
13
|
+
"quantal" => "1210",
|
14
|
+
"precise" => "1204",
|
15
|
+
},
|
16
|
+
}
|
17
|
+
|
18
|
+
def initialize(name)
|
19
|
+
if name !~ PLATFORMS
|
20
|
+
raise ArgumentError, "Unsupported platform name #{name}"
|
21
|
+
end
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def with_version_codename
|
26
|
+
name, version, extra = self.split('-', 3)
|
27
|
+
PLATFORM_VERSION_CODES.each_key do |platform|
|
28
|
+
if name =~ /#{platform}/
|
29
|
+
PLATFORM_VERSION_CODES[platform].each do |version_codename, version_number|
|
30
|
+
#remove '.' from version number
|
31
|
+
if version.delete('.') =~ /#{version_number}/
|
32
|
+
version = version_codename
|
33
|
+
break
|
34
|
+
end
|
35
|
+
end
|
36
|
+
break
|
37
|
+
end
|
38
|
+
end
|
39
|
+
[name, version, extra].join('-')
|
40
|
+
end
|
41
|
+
|
42
|
+
def with_version_number
|
43
|
+
name, version, extra = self.split('-', 3)
|
44
|
+
PLATFORM_VERSION_CODES.each_key do |platform|
|
45
|
+
if name =~ /#{platform}/
|
46
|
+
PLATFORM_VERSION_CODES[platform].each do |version_codename, version_number|
|
47
|
+
if version =~ /#{version_codename}/
|
48
|
+
version = version_number
|
49
|
+
break
|
50
|
+
end
|
51
|
+
end
|
52
|
+
break
|
53
|
+
end
|
54
|
+
end
|
55
|
+
[name, version, extra].join('-')
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -174,6 +174,7 @@ module Beaker
|
|
174
174
|
# Net::Scp always returns 0, so just set the return code to 0.
|
175
175
|
result.exit_code = 0
|
176
176
|
|
177
|
+
result.finalize!
|
177
178
|
return result
|
178
179
|
end
|
179
180
|
|
@@ -192,6 +193,7 @@ module Beaker
|
|
192
193
|
# Net::Scp always returns 0, so just set the return code to 0.
|
193
194
|
result.exit_code = 0
|
194
195
|
|
196
|
+
result.finalize!
|
195
197
|
result
|
196
198
|
end
|
197
199
|
end
|
@@ -2,6 +2,8 @@ module Beaker
|
|
2
2
|
module Utils
|
3
3
|
class NTPControl
|
4
4
|
NTPSERVER = 'pool.ntp.org'
|
5
|
+
SLEEPWAIT = 5
|
6
|
+
TRIES = 5
|
5
7
|
def initialize(options, hosts)
|
6
8
|
@options = options.dup
|
7
9
|
@hosts = hosts
|
@@ -12,31 +14,44 @@ module Beaker
|
|
12
14
|
@logger.notify "Update system time sync"
|
13
15
|
@logger.notify "run ntpdate against NTP pool systems"
|
14
16
|
@hosts.each do |host|
|
15
|
-
|
16
|
-
if host['platform'].include? 'solaris-10'
|
17
|
-
host.exec(Command.new("sleep 10 && ntpdate -w #{NTPSERVER}"))
|
18
|
-
elsif host['platform'].include? 'windows'
|
17
|
+
if host['platform'].include? 'windows'
|
19
18
|
# The exit code of 5 is for Windows 2008 systems where the w32tm /register command
|
20
19
|
# is not actually necessary.
|
21
20
|
host.exec(Command.new("w32tm /register"), :acceptable_exit_codes => [0,5])
|
22
21
|
host.exec(Command.new("net start w32time"), :acceptable_exit_codes => [0,2])
|
23
22
|
host.exec(Command.new("w32tm /config /manualpeerlist:#{NTPSERVER} /syncfromflags:manual /update"))
|
24
23
|
host.exec(Command.new("w32tm /resync"))
|
24
|
+
@logger.notify "NTP date succeeded on #{host}"
|
25
25
|
else
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
26
|
+
case
|
27
|
+
when host['platform'] =~ /solaris-10/
|
28
|
+
ntp_command = "sleep 10 && ntpdate -w #{NTPSERVER}"
|
29
|
+
when host['platform'] =~ /sles-/
|
30
|
+
ntp_command = "sntp #{NTPSERVER}"
|
31
|
+
else
|
32
|
+
ntp_command = "ntpdate -t 20 #{NTPSERVER}"
|
33
|
+
end
|
34
|
+
success=false
|
35
|
+
try = 0
|
36
|
+
until try >= TRIES do
|
37
|
+
try += 1
|
38
|
+
if host.exec(Command.new(ntp_command), :acceptable_exit_codes => (0..255)).exit_code == 0
|
39
|
+
success=true
|
40
|
+
break
|
32
41
|
end
|
42
|
+
sleep SLEEPWAIT
|
43
|
+
end
|
44
|
+
if success
|
45
|
+
@logger.notify "NTP date succeeded on #{host} after #{try} tries"
|
46
|
+
else
|
47
|
+
raise "NTP date was not successful after #{try} tries"
|
33
48
|
end
|
34
|
-
@logger.notify "NTP date succeeded after #{count} tries"
|
35
49
|
end
|
36
50
|
end
|
37
51
|
rescue => e
|
38
52
|
report_and_raise(@logger, e, "timesync (--ntp)")
|
39
53
|
end
|
54
|
+
|
40
55
|
end
|
41
56
|
end
|
42
57
|
end
|