sshkit 1.7.1 → 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 +4 -4
- data/.travis.yml +2 -2
- data/BREAKING_API_WISHLIST.md +14 -0
- data/CHANGELOG.md +74 -0
- data/CONTRIBUTING.md +43 -0
- data/EXAMPLES.md +265 -169
- data/Gemfile +7 -0
- data/README.md +274 -9
- data/RELEASING.md +16 -8
- data/Rakefile +8 -0
- data/lib/sshkit.rb +0 -9
- data/lib/sshkit/all.rb +6 -4
- data/lib/sshkit/backends/abstract.rb +42 -42
- data/lib/sshkit/backends/connection_pool.rb +57 -8
- data/lib/sshkit/backends/local.rb +21 -50
- data/lib/sshkit/backends/netssh.rb +45 -98
- data/lib/sshkit/backends/printer.rb +3 -23
- data/lib/sshkit/backends/skipper.rb +4 -8
- data/lib/sshkit/color.rb +51 -20
- data/lib/sshkit/command.rb +68 -47
- data/lib/sshkit/configuration.rb +38 -5
- data/lib/sshkit/deprecation_logger.rb +17 -0
- data/lib/sshkit/formatters/abstract.rb +28 -4
- data/lib/sshkit/formatters/black_hole.rb +1 -2
- data/lib/sshkit/formatters/dot.rb +3 -10
- data/lib/sshkit/formatters/pretty.rb +31 -56
- data/lib/sshkit/formatters/simple_text.rb +6 -44
- data/lib/sshkit/host.rb +5 -6
- data/lib/sshkit/logger.rb +0 -1
- data/lib/sshkit/mapping_interaction_handler.rb +47 -0
- data/lib/sshkit/runners/parallel.rb +1 -1
- data/lib/sshkit/runners/sequential.rb +1 -1
- data/lib/sshkit/version.rb +1 -1
- data/sshkit.gemspec +0 -1
- data/test/functional/backends/test_local.rb +14 -1
- data/test/functional/backends/test_netssh.rb +58 -50
- data/test/helper.rb +2 -2
- data/test/unit/backends/test_abstract.rb +145 -0
- data/test/unit/backends/test_connection_pool.rb +27 -2
- data/test/unit/backends/test_printer.rb +47 -47
- data/test/unit/formatters/test_custom.rb +65 -0
- data/test/unit/formatters/test_dot.rb +25 -32
- data/test/unit/formatters/test_pretty.rb +114 -22
- data/test/unit/formatters/test_simple_text.rb +83 -0
- data/test/unit/test_color.rb +69 -5
- data/test/unit/test_command.rb +53 -18
- data/test/unit/test_command_map.rb +0 -4
- data/test/unit/test_configuration.rb +47 -7
- data/test/unit/test_coordinator.rb +45 -52
- data/test/unit/test_deprecation_logger.rb +38 -0
- data/test/unit/test_host.rb +3 -4
- data/test/unit/test_logger.rb +0 -1
- data/test/unit/test_mapping_interaction_handler.rb +101 -0
- metadata +37 -41
- data/lib/sshkit/utils/capture_output_methods.rb +0 -13
- data/test/functional/test_coordinator.rb +0 -17
@@ -6,6 +6,7 @@ module SSHKit
|
|
6
6
|
class TestConnectionPool < UnitTest
|
7
7
|
|
8
8
|
def setup
|
9
|
+
super
|
9
10
|
pool.flush_connections
|
10
11
|
end
|
11
12
|
|
@@ -14,11 +15,11 @@ module SSHKit
|
|
14
15
|
end
|
15
16
|
|
16
17
|
def connect
|
17
|
-
->(*
|
18
|
+
->(*_args) { Object.new }
|
18
19
|
end
|
19
20
|
|
20
21
|
def connect_and_close
|
21
|
-
->(*
|
22
|
+
->(*_args) { OpenStruct.new(:closed? => true) }
|
22
23
|
end
|
23
24
|
|
24
25
|
def echo_args
|
@@ -94,6 +95,18 @@ module SSHKit
|
|
94
95
|
refute_equal conn1, conn2
|
95
96
|
end
|
96
97
|
|
98
|
+
def test_expired_connection_is_closed
|
99
|
+
pool.idle_timeout = 0.1
|
100
|
+
conn1 = mock
|
101
|
+
conn1.expects(:closed?).returns(false)
|
102
|
+
conn1.expects(:close)
|
103
|
+
|
104
|
+
entry1 = pool.checkout("conn1"){|*args| conn1 }
|
105
|
+
pool.checkin entry1
|
106
|
+
sleep(pool.idle_timeout)
|
107
|
+
pool.checkout("conn2"){|*args| Object.new}
|
108
|
+
end
|
109
|
+
|
97
110
|
def test_closed_connection_is_not_reused
|
98
111
|
conn1 = pool.checkout("conn", &connect_and_close)
|
99
112
|
pool.checkin conn1
|
@@ -110,6 +123,18 @@ module SSHKit
|
|
110
123
|
refute_equal conn1, conn2
|
111
124
|
end
|
112
125
|
|
126
|
+
def test_close_connections
|
127
|
+
conn1 = mock
|
128
|
+
conn1.expects(:closed?).returns(false)
|
129
|
+
conn1.expects(:close)
|
130
|
+
entry1 = pool.checkout("conn1"){|*args| conn1 }
|
131
|
+
pool.checkin entry1
|
132
|
+
entry2 = pool.checkout("conn2", &connect)
|
133
|
+
# entry2 isn't closed if close_connections is called
|
134
|
+
|
135
|
+
pool.close_connections
|
136
|
+
end
|
137
|
+
|
113
138
|
end
|
114
139
|
end
|
115
140
|
end
|
@@ -1,73 +1,73 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
module SSHKit
|
4
|
-
|
5
4
|
module Backend
|
6
|
-
|
7
5
|
class TestPrinter < UnitTest
|
8
6
|
|
9
|
-
def
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
def backend
|
16
|
-
@backend ||= Printer
|
7
|
+
def setup
|
8
|
+
super
|
9
|
+
SSHKit.config.output = SSHKit::Formatter::Pretty.new(output)
|
10
|
+
SSHKit.config.output_verbosity = Logger::DEBUG
|
11
|
+
Command.any_instance.stubs(:uuid).returns('aaaaaa')
|
17
12
|
end
|
18
13
|
|
19
|
-
def
|
20
|
-
@
|
14
|
+
def output
|
15
|
+
@output ||= String.new
|
21
16
|
end
|
22
17
|
|
23
18
|
def printer
|
24
|
-
Printer.new(Host.new(
|
19
|
+
@printer ||= Printer.new(Host.new('example.com'))
|
25
20
|
end
|
26
21
|
|
27
|
-
def
|
28
|
-
|
22
|
+
def test_execute
|
23
|
+
printer.execute 'uname -a'
|
24
|
+
assert_output_lines(
|
25
|
+
' INFO [aaaaaa] Running /usr/bin/env uname -a on example.com',
|
26
|
+
' DEBUG [aaaaaa] Command: uname -a'
|
27
|
+
)
|
29
28
|
end
|
30
29
|
|
31
|
-
def
|
32
|
-
|
33
|
-
SSHKit.capture_output(result) do
|
34
|
-
printer.run
|
35
|
-
end
|
36
|
-
result.rewind
|
37
|
-
assert_equal "/usr/bin/env ls -l /some/directory\n", result.read
|
38
|
-
end
|
30
|
+
def test_test_method
|
31
|
+
printer.test '[ -d /some/file ]'
|
39
32
|
|
40
|
-
|
41
|
-
|
33
|
+
assert_output_lines(
|
34
|
+
' DEBUG [aaaaaa] Running /usr/bin/env [ -d /some/file ] on example.com',
|
35
|
+
' DEBUG [aaaaaa] Command: [ -d /some/file ]'
|
36
|
+
)
|
42
37
|
end
|
43
38
|
|
44
|
-
def
|
45
|
-
|
46
|
-
ssh.pty = true
|
47
|
-
ssh.connection_timeout = 30
|
48
|
-
ssh.ssh_options = {
|
49
|
-
keys: %w(/home/user/.ssh/id_rsa),
|
50
|
-
forward_agent: false,
|
51
|
-
auth_methods: %w(publickey password)
|
52
|
-
}
|
53
|
-
end
|
39
|
+
def test_capture
|
40
|
+
result = printer.capture 'ls -l'
|
54
41
|
|
55
|
-
assert_equal
|
56
|
-
assert_equal true, backend.config.pty
|
42
|
+
assert_equal '', result
|
57
43
|
|
58
|
-
|
59
|
-
|
60
|
-
|
44
|
+
assert_output_lines(
|
45
|
+
' DEBUG [aaaaaa] Running /usr/bin/env ls -l on example.com',
|
46
|
+
' DEBUG [aaaaaa] Command: ls -l'
|
47
|
+
)
|
61
48
|
end
|
62
49
|
|
63
|
-
def
|
64
|
-
|
65
|
-
|
66
|
-
|
50
|
+
def test_upload
|
51
|
+
printer.upload! '/some/file', '/remote'
|
52
|
+
assert_output_lines(
|
53
|
+
' INFO [aaaaaa] Running /usr/bin/env /some/file /remote on example.com',
|
54
|
+
' DEBUG [aaaaaa] Command: /usr/bin/env /some/file /remote'
|
55
|
+
)
|
67
56
|
end
|
68
57
|
|
69
|
-
|
58
|
+
def test_download
|
59
|
+
printer.download! 'remote/file', '/local/path'
|
60
|
+
assert_output_lines(
|
61
|
+
' INFO [aaaaaa] Running /usr/bin/env remote/file /local/path on example.com',
|
62
|
+
' DEBUG [aaaaaa] Command: /usr/bin/env remote/file /local/path'
|
63
|
+
)
|
64
|
+
end
|
70
65
|
|
71
|
-
|
66
|
+
private
|
72
67
|
|
73
|
-
|
68
|
+
def assert_output_lines(*expected_lines)
|
69
|
+
assert_equal(expected_lines, output.split("\n"))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
module SSHKit
|
4
|
+
# Try to maintain backwards compatibility with Custom formatters defined by other people
|
5
|
+
class TestCustom < UnitTest
|
6
|
+
|
7
|
+
def setup
|
8
|
+
super
|
9
|
+
SSHKit.config.output_verbosity = Logger::DEBUG
|
10
|
+
end
|
11
|
+
|
12
|
+
def output
|
13
|
+
@output ||= String.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def custom
|
17
|
+
@custom ||= CustomFormatter.new(output)
|
18
|
+
end
|
19
|
+
|
20
|
+
{
|
21
|
+
log: 'LM 1 Test',
|
22
|
+
fatal: 'LM 4 Test',
|
23
|
+
error: 'LM 3 Test',
|
24
|
+
warn: 'LM 2 Test',
|
25
|
+
info: 'LM 1 Test',
|
26
|
+
debug: 'LM 0 Test'
|
27
|
+
}.each do |level, expected_output|
|
28
|
+
define_method("test_#{level}_logging") do
|
29
|
+
custom.send(level, 'Test')
|
30
|
+
assert_log_output expected_output
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_write_logs_commands
|
35
|
+
custom.write(Command.new(:ls))
|
36
|
+
|
37
|
+
assert_log_output 'C 1 /usr/bin/env ls'
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_double_chevron_logs_commands
|
41
|
+
custom << Command.new(:ls)
|
42
|
+
|
43
|
+
assert_log_output 'C 1 /usr/bin/env ls'
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def assert_log_output(expected_output)
|
49
|
+
assert_equal expected_output, output
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
class CustomFormatter < SSHKit::Formatter::Abstract
|
55
|
+
def write(obj)
|
56
|
+
original_output << case obj
|
57
|
+
when SSHKit::Command then "C #{obj.verbosity} #{obj}"
|
58
|
+
when SSHKit::LogMessage then "LM #{obj.verbosity} #{obj}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
alias :<< :write
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -1,65 +1,58 @@
|
|
1
1
|
require 'helper'
|
2
|
-
require 'sshkit'
|
3
2
|
|
4
3
|
module SSHKit
|
5
4
|
class TestDot < UnitTest
|
6
5
|
|
7
6
|
def setup
|
7
|
+
super
|
8
8
|
SSHKit.config.output_verbosity = Logger::DEBUG
|
9
9
|
end
|
10
10
|
|
11
11
|
def output
|
12
|
-
@
|
12
|
+
@output ||= String.new
|
13
13
|
end
|
14
14
|
|
15
15
|
def dot
|
16
|
-
@
|
16
|
+
@dot ||= SSHKit::Formatter::Dot.new(output)
|
17
17
|
end
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
%w(fatal error warn info debug).each do |level|
|
20
|
+
define_method("test_#{level}_output") do
|
21
|
+
dot.send(level, 'Test')
|
22
|
+
assert_log_output('')
|
23
|
+
end
|
23
24
|
end
|
24
25
|
|
25
|
-
def
|
26
|
-
dot
|
27
|
-
|
26
|
+
def test_log_command_start
|
27
|
+
dot.log_command_start(SSHKit::Command.new(:ls))
|
28
|
+
assert_log_output('')
|
28
29
|
end
|
29
30
|
|
30
|
-
def
|
31
|
-
dot
|
32
|
-
|
33
|
-
end
|
34
|
-
|
35
|
-
def test_logging_warn
|
36
|
-
dot << SSHKit::LogMessage.new(Logger::WARN, "Test")
|
37
|
-
assert_equal "", output.strip
|
38
|
-
end
|
39
|
-
|
40
|
-
def test_logging_info
|
41
|
-
dot << SSHKit::LogMessage.new(Logger::INFO, "Test")
|
42
|
-
assert_equal "", output.strip
|
43
|
-
end
|
44
|
-
|
45
|
-
def test_logging_debug
|
46
|
-
dot << SSHKit::LogMessage.new(Logger::DEBUG, "Test")
|
47
|
-
assert_equal "", output.strip
|
31
|
+
def test_log_command_data
|
32
|
+
dot.log_command_data(SSHKit::Command.new(:ls), :stdout, 'Some output')
|
33
|
+
assert_log_output('')
|
48
34
|
end
|
49
35
|
|
50
36
|
def test_command_success
|
37
|
+
output.stubs(:tty?).returns(true)
|
51
38
|
command = SSHKit::Command.new(:ls)
|
52
39
|
command.exit_status = 0
|
53
|
-
dot
|
54
|
-
|
40
|
+
dot.log_command_exit(command)
|
41
|
+
assert_log_output("\e[0;32;49m.\e[0m")
|
55
42
|
end
|
56
43
|
|
57
44
|
def test_command_failure
|
45
|
+
output.stubs(:tty?).returns(true)
|
58
46
|
command = SSHKit::Command.new(:ls, {raise_on_non_zero_exit: false})
|
59
47
|
command.exit_status = 1
|
60
|
-
dot
|
61
|
-
|
48
|
+
dot.log_command_exit(command)
|
49
|
+
assert_log_output("\e[0;31;49m.\e[0m")
|
62
50
|
end
|
63
51
|
|
52
|
+
private
|
53
|
+
|
54
|
+
def assert_log_output(expected_output)
|
55
|
+
assert_equal expected_output, output
|
56
|
+
end
|
64
57
|
end
|
65
58
|
end
|
@@ -1,50 +1,142 @@
|
|
1
1
|
require 'helper'
|
2
|
-
require 'sshkit'
|
3
2
|
|
4
3
|
module SSHKit
|
5
4
|
class TestPretty < UnitTest
|
6
5
|
|
7
6
|
def setup
|
7
|
+
super
|
8
8
|
SSHKit.config.output_verbosity = Logger::DEBUG
|
9
|
+
Command.any_instance.stubs(:uuid).returns('aaaaaa')
|
9
10
|
end
|
10
11
|
|
11
12
|
def output
|
12
|
-
@
|
13
|
+
@output ||= String.new
|
13
14
|
end
|
14
15
|
|
15
16
|
def pretty
|
16
|
-
@
|
17
|
+
@pretty ||= SSHKit::Formatter::Pretty.new(output)
|
17
18
|
end
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
{
|
21
|
+
log: "\e[0;34;49mINFO\e[0m Test\n",
|
22
|
+
fatal: "\e[0;31;49mFATAL\e[0m Test\n",
|
23
|
+
error: "\e[0;31;49mERROR\e[0m Test\n",
|
24
|
+
warn: "\e[0;33;49mWARN\e[0m Test\n",
|
25
|
+
info: "\e[0;34;49mINFO\e[0m Test\n",
|
26
|
+
debug: "\e[0;30;49mDEBUG\e[0m Test\n"
|
27
|
+
}.each do |level, expected_output|
|
28
|
+
define_method("test_#{level}_output_with_color") do
|
29
|
+
output.stubs(:tty?).returns(true)
|
30
|
+
pretty.send(level, 'Test')
|
31
|
+
assert_log_output(expected_output)
|
32
|
+
end
|
23
33
|
end
|
24
34
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
35
|
+
def test_command_lifecycle_logging_with_color
|
36
|
+
output.stubs(:tty?).returns(true)
|
37
|
+
simulate_command_lifecycle(pretty)
|
38
|
+
|
39
|
+
expected_log_lines = [
|
40
|
+
"\e[0;34;49mINFO\e[0m [\e[0;32;49maaaaaa\e[0m] Running \e[1;33;49m/usr/bin/env a_cmd some args\e[0m as \e[0;34;49muser\e[0m@\e[0;34;49mlocalhost\e[0m",
|
41
|
+
"\e[0;30;49mDEBUG\e[0m [\e[0;32;49maaaaaa\e[0m] Command: \e[0;34;49m/usr/bin/env a_cmd some args\e[0m",
|
42
|
+
"\e[0;30;49mDEBUG\e[0m [\e[0;32;49maaaaaa\e[0m] \e[0;32;49m\tstdout message\e[0m",
|
43
|
+
"\e[0;30;49mDEBUG\e[0m [\e[0;32;49maaaaaa\e[0m] \e[0;31;49m\tstderr message\e[0m",
|
44
|
+
"\e[0;34;49mINFO\e[0m [\e[0;32;49maaaaaa\e[0m] Finished in 1.000 seconds with exit status 0 (\e[1;32;49msuccessful\e[0m)."
|
45
|
+
]
|
46
|
+
assert_equal expected_log_lines, output.split("\n")
|
47
|
+
end
|
48
|
+
|
49
|
+
{
|
50
|
+
log: " INFO Test\n",
|
51
|
+
fatal: " FATAL Test\n",
|
52
|
+
error: " ERROR Test\n",
|
53
|
+
warn: " WARN Test\n",
|
54
|
+
info: " INFO Test\n",
|
55
|
+
debug: " DEBUG Test\n"
|
56
|
+
}.each do |level, expected_output|
|
57
|
+
define_method("test_#{level}_output_without_color") do
|
58
|
+
pretty.send(level, "Test")
|
59
|
+
assert_log_output expected_output
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_logging_message_with_leading_and_trailing_space
|
64
|
+
pretty.log(" some spaces\n\n \t")
|
65
|
+
assert_log_output " INFO some spaces\n"
|
28
66
|
end
|
29
67
|
|
30
|
-
def
|
31
|
-
pretty
|
32
|
-
|
68
|
+
def test_can_log_non_strings
|
69
|
+
pretty.log(Pathname.new('/var/log/my.log'))
|
70
|
+
assert_log_output " INFO /var/log/my.log\n"
|
33
71
|
end
|
34
72
|
|
35
|
-
def
|
36
|
-
pretty
|
37
|
-
|
73
|
+
def test_command_lifecycle_logging_without_color
|
74
|
+
simulate_command_lifecycle(pretty)
|
75
|
+
|
76
|
+
expected_log_lines = [
|
77
|
+
' INFO [aaaaaa] Running /usr/bin/env a_cmd some args as user@localhost',
|
78
|
+
' DEBUG [aaaaaa] Command: /usr/bin/env a_cmd some args',
|
79
|
+
" DEBUG [aaaaaa] \tstdout message",
|
80
|
+
" DEBUG [aaaaaa] \tstderr message",
|
81
|
+
' INFO [aaaaaa] Finished in 1.000 seconds with exit status 0 (successful).'
|
82
|
+
]
|
83
|
+
|
84
|
+
assert_equal expected_log_lines, output.split("\n")
|
38
85
|
end
|
39
86
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
87
|
+
def test_unsupported_class
|
88
|
+
raised_error = assert_raises RuntimeError do
|
89
|
+
pretty << Pathname.new('/tmp')
|
90
|
+
end
|
91
|
+
assert_equal('write only supports formatting SSHKit::LogMessage, called with Pathname: #<Pathname:/tmp>', raised_error.message)
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_does_not_log_message_when_verbosity_is_too_low
|
95
|
+
SSHKit.config.output_verbosity = Logger::WARN
|
96
|
+
pretty.info('Some info')
|
97
|
+
assert_log_output('')
|
98
|
+
|
99
|
+
SSHKit.config.output_verbosity = Logger::INFO
|
100
|
+
pretty.info('Some other info')
|
101
|
+
assert_log_output(" INFO Some other info\n")
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_does_not_log_command_when_verbosity_is_too_low
|
105
|
+
SSHKit.config.output_verbosity = Logger::WARN
|
106
|
+
command = Command.new(:ls, host: Host.new('user@localhost'), verbosity: Logger::INFO)
|
107
|
+
pretty.log_command_start(command)
|
108
|
+
assert_log_output('')
|
109
|
+
|
110
|
+
SSHKit.config.output_verbosity = Logger::INFO
|
111
|
+
pretty.log_command_start(command)
|
112
|
+
assert_log_output(" INFO [aaaaaa] Running /usr/bin/env ls as user@localhost\n")
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
def test_can_write_to_output_which_just_supports_append
|
117
|
+
# Note output doesn't have to be an IO, it only needs to support <<
|
118
|
+
output = stub(:<<)
|
119
|
+
pretty = SSHKit::Formatter::Pretty.new(output)
|
120
|
+
simulate_command_lifecycle(pretty)
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def simulate_command_lifecycle(pretty)
|
126
|
+
command = SSHKit::Command.new(:a_cmd, 'some args', host: Host.new('user@localhost'))
|
127
|
+
command.stubs(:runtime).returns(1)
|
128
|
+
pretty.log_command_start(command)
|
129
|
+
command.started = true
|
130
|
+
command.on_stdout(nil, 'stdout message')
|
131
|
+
pretty.log_command_data(command, :stdout, 'stdout message')
|
132
|
+
command.on_stderr(nil, 'stderr message')
|
133
|
+
pretty.log_command_data(command, :stderr, 'stderr message')
|
134
|
+
command.exit_status = 0
|
135
|
+
pretty.log_command_exit(command)
|
43
136
|
end
|
44
137
|
|
45
|
-
def
|
46
|
-
|
47
|
-
assert_equal output.strip, "\e[0;30;49mDEBUG\e[0m Test".strip
|
138
|
+
def assert_log_output(expected_output)
|
139
|
+
assert_equal expected_output, output
|
48
140
|
end
|
49
141
|
|
50
142
|
end
|