drbqs 0.0.13 → 0.0.14
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +10 -7
- data/README.md +52 -11
- data/Rakefile +32 -10
- data/VERSION +1 -1
- data/bin/drbqs-manage +3 -93
- data/bin/drbqs-node +3 -89
- data/bin/drbqs-server +3 -117
- data/bin/drbqs-ssh +6 -0
- data/drbqs.gemspec +118 -97
- data/example/README.md +2 -2
- data/lib/drbqs/config/config.rb +88 -0
- data/lib/drbqs/config/process_list.rb +194 -0
- data/lib/drbqs/config/ssh_host.rb +41 -0
- data/lib/drbqs/{execute_node.rb → manage/execute_node.rb} +6 -4
- data/lib/drbqs/manage/manage.rb +100 -0
- data/lib/drbqs/manage/send_signal.rb +45 -0
- data/lib/drbqs/manage/ssh_execute.rb +23 -0
- data/lib/drbqs/manage/ssh_shell.rb +143 -0
- data/lib/drbqs/node/connection.rb +69 -0
- data/lib/drbqs/{client.rb → node/node.rb} +48 -18
- data/lib/drbqs/node/task_client.rb +94 -0
- data/lib/drbqs/server/acl_file.rb +15 -0
- data/lib/drbqs/server/check_alive.rb +23 -0
- data/lib/drbqs/server/history.rb +49 -0
- data/lib/drbqs/server/message.rb +142 -0
- data/lib/drbqs/server/node_list.rb +59 -0
- data/lib/drbqs/server/queue.rb +128 -0
- data/lib/drbqs/{server.rb → server/server.rb} +66 -74
- data/lib/drbqs/server/server_hook.rb +72 -0
- data/lib/drbqs/server/transfer_setting.rb +30 -0
- data/lib/drbqs/task/command_task.rb +43 -0
- data/lib/drbqs/{task.rb → task/task.rb} +18 -39
- data/lib/drbqs/{task_generator.rb → task/task_generator.rb} +2 -0
- data/lib/drbqs/utility/argument.rb +27 -0
- data/lib/drbqs/utility/command_line/command_base.rb +27 -0
- data/lib/drbqs/utility/command_line/command_manage.rb +121 -0
- data/lib/drbqs/utility/command_line/command_node.rb +103 -0
- data/lib/drbqs/utility/command_line/command_server.rb +165 -0
- data/lib/drbqs/utility/command_line/command_ssh.rb +126 -0
- data/lib/drbqs/utility/command_line.rb +15 -0
- data/lib/drbqs/utility/misc.rb +72 -0
- data/lib/drbqs/{server_define.rb → utility/server_define.rb} +23 -8
- data/lib/drbqs/utility/temporary.rb +49 -0
- data/lib/drbqs/{ssh/transfer.rb → utility/transfer/file_transfer.rb} +18 -58
- data/lib/drbqs/utility/transfer/transfer_client.rb +90 -0
- data/lib/drbqs.rb +10 -22
- data/spec/config/config_spec.rb +84 -0
- data/spec/config/process_list_spec.rb +149 -0
- data/spec/config/ssh_host_spec.rb +81 -0
- data/spec/integration_test/01_basic_usage_spec.rb +54 -0
- data/spec/integration_test/02_use_generator_spec.rb +53 -0
- data/spec/integration_test/03_use_temporary_file_spec.rb +26 -0
- data/spec/integration_test/04_use_unix_domain_spec.rb +34 -0
- data/spec/integration_test/05_server_exit_signal_spec.rb +23 -0
- data/spec/integration_test/06_node_exit_after_task_spec.rb +42 -0
- data/spec/integration_test/07_command_server_with_node_spec.rb +44 -0
- data/spec/integration_test/definition/server01.rb +20 -0
- data/spec/integration_test/definition/server02.rb +16 -0
- data/spec/integration_test/definition/task_obj_definition.rb +49 -0
- data/spec/manage/manage_spec.rb +33 -0
- data/spec/manage/send_signal_spec.rb +39 -0
- data/spec/{ssh_shell_spec.rb → manage/ssh_shell_spec.rb} +8 -8
- data/spec/node/connection_spec.rb +66 -0
- data/spec/node/task_client_spec.rb +212 -0
- data/spec/server/acl_file_spec.rb +9 -0
- data/spec/{server_check_alive_spec.rb → server/check_alive_spec.rb} +15 -11
- data/spec/{data → server/data}/acl.txt +0 -0
- data/spec/{history_spec.rb → server/history_spec.rb} +9 -5
- data/spec/server/message_spec.rb +195 -0
- data/spec/server/node_list_spec.rb +111 -0
- data/spec/server/queue_spec.rb +239 -0
- data/spec/{server_hook_spec.rb → server/server_hook_spec.rb} +23 -17
- data/spec/server/server_spec.rb +89 -0
- data/spec/server/transfer_setting_spec.rb +37 -0
- data/spec/spec_helper.rb +65 -0
- data/spec/task/file_transfer_spec.rb +107 -0
- data/spec/{task_generator_spec.rb → task/task_generator_spec.rb} +2 -2
- data/spec/{task_spec.rb → task/task_spec.rb} +3 -1
- data/spec/utility/argument_spec.rb +39 -0
- data/spec/utility/command_line/command_base_spec.rb +33 -0
- data/spec/utility/command_line/commands_spec.rb +15 -0
- data/spec/utility/misc_spec.rb +77 -0
- data/spec/utility/server_define_spec.rb +59 -0
- data/spec/utility/temporary_spec.rb +39 -0
- metadata +158 -93
- data/example/drbqs-manage-test.rb +0 -3
- data/example/drbqs-node-test.rb +0 -3
- data/example/drbqs-server-test.rb +0 -3
- data/lib/drbqs/acl_file.rb +0 -13
- data/lib/drbqs/config.rb +0 -98
- data/lib/drbqs/connection.rb +0 -67
- data/lib/drbqs/history.rb +0 -34
- data/lib/drbqs/manage.rb +0 -84
- data/lib/drbqs/message.rb +0 -119
- data/lib/drbqs/node_list.rb +0 -52
- data/lib/drbqs/queue.rb +0 -138
- data/lib/drbqs/server_hook.rb +0 -67
- data/lib/drbqs/ssh/host.rb +0 -26
- data/lib/drbqs/ssh/shell.rb +0 -139
- data/lib/drbqs/task_client.rb +0 -86
- data/lib/drbqs/utils.rb +0 -19
- data/spec/acl_file_spec.rb +0 -9
- data/spec/config_spec.rb +0 -14
- data/spec/connection_spec.rb +0 -49
- data/spec/manage_spec.rb +0 -57
- data/spec/message_spec.rb +0 -81
- data/spec/node_list_spec.rb +0 -68
- data/spec/queue_spec.rb +0 -59
- data/spec/server_define_spec.rb +0 -45
- data/spec/server_spec.rb +0 -56
- data/spec/task_client_spec.rb +0 -53
- data/spec/test/test1.rb +0 -13
- data/spec/test1_spec.rb +0 -80
- data/spec/test2_spec.rb +0 -69
- data/spec/transfer_spec.rb +0 -13
@@ -0,0 +1,107 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
require 'drbqs/utility/transfer/file_transfer'
|
4
|
+
|
5
|
+
describe DRbQS::FileTransfer do
|
6
|
+
def create_file(path, str)
|
7
|
+
path = File.join(@tmp, path)
|
8
|
+
open(path, 'w') do |f|
|
9
|
+
f.puts str
|
10
|
+
end
|
11
|
+
path
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_directory(path)
|
15
|
+
path = File.join(@tmp, path)
|
16
|
+
FileUtils.mkdir_p(path)
|
17
|
+
path
|
18
|
+
end
|
19
|
+
|
20
|
+
before(:all) do
|
21
|
+
@tmp = FileName.create('/tmp/drbqs_test', :directory => :self)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should be empty." do
|
25
|
+
DRbQS::FileTransfer.empty?.should be_true
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should return nil when file queue is empty" do
|
29
|
+
DRbQS::FileTransfer.dequeue_all.should be_nil
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should enqueue path." do
|
33
|
+
path = create_file('file1.txt', 'file1')
|
34
|
+
DRbQS::FileTransfer.enqueue(path)
|
35
|
+
DRbQS::FileTransfer.dequeue.should == path
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should compress and enqueue path." do
|
39
|
+
path = create_file('file2.txt', 'file2')
|
40
|
+
DRbQS::FileTransfer.enqueue(path, :compress => true)
|
41
|
+
DRbQS::FileTransfer.dequeue.should == path + '.gz'
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should rename and enqueue path." do
|
45
|
+
path = create_file('file3.txt', 'file3')
|
46
|
+
rename = 'rename.txt'
|
47
|
+
new_path = File.join(@tmp, rename)
|
48
|
+
DRbQS::FileTransfer.enqueue(path, :rename => rename)
|
49
|
+
DRbQS::FileTransfer.dequeue.should == new_path
|
50
|
+
File.exist?(new_path).should be_true
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should rename to some directory." do
|
54
|
+
path = create_file('file4.txt', 'file4')
|
55
|
+
rename = 'dir/rename.txt'
|
56
|
+
new_path = File.join(@tmp, rename)
|
57
|
+
DRbQS::FileTransfer.enqueue(path, :rename => rename)
|
58
|
+
DRbQS::FileTransfer.dequeue.should == new_path
|
59
|
+
File.exist?(new_path).should be_true
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should rename, compress, and enqueue path." do
|
63
|
+
path = create_file('file5.txt', 'file5')
|
64
|
+
rename = 'dir2/rename.txt'
|
65
|
+
new_path = File.join(@tmp, rename + '.gz')
|
66
|
+
DRbQS::FileTransfer.enqueue(path, :rename => rename, :compress => true)
|
67
|
+
DRbQS::FileTransfer.dequeue.should == new_path
|
68
|
+
File.exist?(new_path).should be_true
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should return array of files" do
|
72
|
+
files = [create_file('file6.txt', 'file6'),
|
73
|
+
create_file('file8.txt', 'file8'),
|
74
|
+
create_file('file8.txt', 'file8')]
|
75
|
+
files.each do |path|
|
76
|
+
DRbQS::FileTransfer.enqueue(path)
|
77
|
+
end
|
78
|
+
DRbQS::FileTransfer.dequeue_all.should == files
|
79
|
+
DRbQS::FileTransfer.empty?.should be_true
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should enqueue path of directory." do
|
83
|
+
path = create_directory('dir/dir1')
|
84
|
+
DRbQS::FileTransfer.enqueue(path)
|
85
|
+
DRbQS::FileTransfer.dequeue.should == path
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should compress and enqueue path of directory." do
|
89
|
+
path = create_directory('dir/dir2')
|
90
|
+
file_path = create_file('dir/dir2/test.txt', 'hello world')
|
91
|
+
DRbQS::FileTransfer.enqueue(path, :compress => true)
|
92
|
+
DRbQS::FileTransfer.dequeue.should == path + '.tar.gz'
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should rename, compress, and enqueue path of directory." do
|
96
|
+
path = create_directory('dir/dir3')
|
97
|
+
file_path = create_file('dir/dir3/test.txt', 'hello world')
|
98
|
+
rename = 'rename_dir'
|
99
|
+
DRbQS::FileTransfer.enqueue(path, :compress => true, :rename => rename)
|
100
|
+
DRbQS::FileTransfer.dequeue.should == File.join(@tmp, 'dir', rename + '.tar.gz')
|
101
|
+
end
|
102
|
+
|
103
|
+
after(:all) do
|
104
|
+
FileUtils.rm_r(@tmp)
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
|
-
require File.expand_path(File.dirname(__FILE__) + '
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
2
|
|
3
|
-
require 'drbqs/
|
3
|
+
require 'drbqs/task/task'
|
4
4
|
|
5
5
|
describe DRbQS::TaskGenerator do
|
6
6
|
def check_task_ary(tasks, num, cl = DRbQS::Task)
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
require 'drbqs/utility/command_line'
|
4
|
+
|
5
|
+
describe DRbQS::CommandLineArgument do
|
6
|
+
|
7
|
+
it "should split arguments" do
|
8
|
+
ary = ['abc', 'def', '--', '123', '45', '6']
|
9
|
+
a1, a2 = DRbQS::CommandLineArgument.split_arguments(ary)
|
10
|
+
a1.should == ['abc', 'def']
|
11
|
+
a2.should == ['123', '45', '6']
|
12
|
+
end
|
13
|
+
|
14
|
+
context "when checking size of array" do
|
15
|
+
it "should return true" do
|
16
|
+
ary = [1, 2, 3]
|
17
|
+
DRbQS::CommandLineArgument.check_argument_size(ary, :>=, 1).should be_true
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should return true" do
|
21
|
+
ary = [1, 2, 3]
|
22
|
+
DRbQS::CommandLineArgument.check_argument_size(ary, :>=, 1, :<=, 4).should be_true
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should raise error" do
|
26
|
+
ary = [1, 2, 3]
|
27
|
+
lambda do
|
28
|
+
DRbQS::CommandLineArgument.check_argument_size(ary, :==, 9)
|
29
|
+
end.should raise_error
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should raise error" do
|
33
|
+
ary = [1, 2, 3]
|
34
|
+
lambda do
|
35
|
+
DRbQS::CommandLineArgument.check_argument_size(ary, :>, 0, :<=, 2)
|
36
|
+
end.should raise_error
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
require 'drbqs/utility/command_line'
|
4
|
+
|
5
|
+
describe DRbQS::CommandBase do
|
6
|
+
it "should define DRbQS::CommandBase.exec." do
|
7
|
+
argv = [1, 2, 3]
|
8
|
+
obj = mock
|
9
|
+
DRbQS::CommandBase.should_receive(:new).and_return(obj)
|
10
|
+
obj.should_receive(:parse_option)
|
11
|
+
obj.should_receive(:exec)
|
12
|
+
DRbQS::CommandBase.exec(argv)
|
13
|
+
end
|
14
|
+
|
15
|
+
subject do
|
16
|
+
DRbQS::CommandBase.new
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should exit with 0." do
|
20
|
+
Kernel.should_receive(:exit).with(0)
|
21
|
+
subject.__send__(:exit_normally)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should exit." do
|
25
|
+
Kernel.should_receive(:exit)
|
26
|
+
subject.__send__(:exit_unusually)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should exit." do
|
30
|
+
Kernel.should_receive(:exit)
|
31
|
+
subject.__send__(:exit_invalid_option)
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
require 'drbqs/utility/command_line'
|
4
|
+
|
5
|
+
[DRbQS::CommandServer, DRbQS::CommandNode, DRbQS::CommandManage, DRbQS::CommandSSH].each do |cls|
|
6
|
+
describe cls do
|
7
|
+
it "should have defined parse_option." do
|
8
|
+
cls.method_defined?(:parse_option).should be_true
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should have defined exec." do
|
12
|
+
cls.method_defined?(:exec).should be_true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
require 'drbqs/utility/temporary'
|
4
|
+
|
5
|
+
describe DRbQS::LoggerDummy do
|
6
|
+
subject do
|
7
|
+
DRbQS::LoggerDummy.new
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should respond to info." do
|
11
|
+
subject.should respond_to(:info)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should respond to warn." do
|
15
|
+
subject.should respond_to(:warn)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should respond to error." do
|
19
|
+
subject.should respond_to(:error)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should respond to debug." do
|
23
|
+
subject.should respond_to(:debug)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe DRbQS::Misc do
|
28
|
+
context "when creating new uri" do
|
29
|
+
it "should return uri of druby." do
|
30
|
+
DRbQS::Misc.create_uri(:port => 10000).should == "druby://:10000"
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should return uri of drbunix." do
|
34
|
+
tmp_path = DRbQS::Temporary.file
|
35
|
+
DRbQS::Misc.create_uri(:unix => tmp_path).should == "drbunix:#{tmp_path}"
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should raise error for non-existing parent directory." do
|
39
|
+
tmp_dir = DRbQS::Temporary.file
|
40
|
+
tmp_path = File.join(tmp_dir, 'tmp_file')
|
41
|
+
lambda do
|
42
|
+
DRbQS::Misc.create_uri(:unix => tmp_path)
|
43
|
+
end.should raise_error
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should raise error for existing path." do
|
47
|
+
tmp_path = DRbQS::Temporary.file
|
48
|
+
open(tmp_path, 'w') do |f|
|
49
|
+
f.puts 'hello world'
|
50
|
+
end
|
51
|
+
lambda do
|
52
|
+
DRbQS::Misc.create_uri(:unix => tmp_path)
|
53
|
+
end.should raise_error
|
54
|
+
end
|
55
|
+
|
56
|
+
after(:all) do
|
57
|
+
DRbQS::Temporary.delete_all
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should create logger." do
|
62
|
+
logger = DRbQS::Misc.create_logger(File.join(HOME_FOR_SPEC, 'tmp.log'), Logger::INFO)
|
63
|
+
logger.should be_an_instance_of(Logger)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should return time string for history." do
|
67
|
+
DRbQS::Misc.time_to_history_string(Time.now).should be_an_instance_of String
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should return ramdom key" do
|
71
|
+
a = DRbQS::Misc.random_key
|
72
|
+
b = DRbQS::Misc.random_key
|
73
|
+
a.should be_an_instance_of String
|
74
|
+
b.should be_an_instance_of String
|
75
|
+
a.should_not == b
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe DRbQS::ServerDefinition do
|
4
|
+
context "when we call class methods" do
|
5
|
+
subject do
|
6
|
+
DRbQS.class_variable_get(:@@server_def)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should define server." do
|
10
|
+
lambda do
|
11
|
+
DRbQS.define_server do |server, argv, opts|
|
12
|
+
server.add_hook(:finish) do |serv|
|
13
|
+
serv.exit
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end.should change { subject.instance_variable_get(:@default_server_opts) }.from(nil).to({})
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should set parser of options." do
|
20
|
+
lambda do
|
21
|
+
DRbQS.option_parser do |opt, hash|
|
22
|
+
opt.on('--test') do |v|
|
23
|
+
hash[:test] = true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end.should change { subject.instance_variable_get(:@option_parse) }.from(nil)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should parse options." do
|
30
|
+
lambda do
|
31
|
+
DRbQS.parse_option(['--test'])
|
32
|
+
end.should change { subject.instance_variable_get(:@argv) }.from(nil)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should start server." do
|
36
|
+
DRbQS::Server.should_receive(:new)
|
37
|
+
begin
|
38
|
+
# After DRbQS::Server.new returns nil, raise error
|
39
|
+
DRbQS.start_server(:uri => 'druby://localhost:13500')
|
40
|
+
rescue
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should return empty help message" do
|
45
|
+
DRbQS.clear_definition
|
46
|
+
DRbQS.option_help_message.should be_nil
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should return help message." do
|
50
|
+
DRbQS.option_parser do |opt, hash|
|
51
|
+
opt.on('--test') do |v|
|
52
|
+
hash[:test] = true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
DRbQS.option_help_message.should be_an_instance_of String
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
require 'drbqs/utility/temporary'
|
4
|
+
|
5
|
+
describe DRbQS::Temporary do
|
6
|
+
before(:all) do
|
7
|
+
DRbQS::Temporary.file
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should create base directory." do
|
11
|
+
File.exist?(DRbQS::Temporary.root).should be_true
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should create empty directory." do
|
15
|
+
dir = DRbQS::Temporary.directory
|
16
|
+
File.directory?(dir).should be_true
|
17
|
+
Dir.entries(dir).should have(2).items
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should return new path of file." do
|
21
|
+
path = DRbQS::Temporary.file
|
22
|
+
File.exist?(path).should_not be_true
|
23
|
+
open(path, 'w') do |f|
|
24
|
+
f.puts 'hello'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should make directory empty." do
|
29
|
+
Dir.entries(DRbQS::Temporary.root).size.should > 2
|
30
|
+
DRbQS::Temporary.delete
|
31
|
+
Dir.entries(DRbQS::Temporary.root).should have(2).items
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should all directories." do
|
35
|
+
root = DRbQS::Temporary.root
|
36
|
+
DRbQS::Temporary.delete_all
|
37
|
+
File.exist?(root).should_not be_true
|
38
|
+
end
|
39
|
+
end
|