expect-behaviors 0.1.1
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 +7 -0
- data/.document +5 -0
- data/.ruby-version +1 -0
- data/.travis.yml +20 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +99 -0
- data/LICENSE +22 -0
- data/README.md +16 -0
- data/Rakefile +17 -0
- data/VERSION +1 -0
- data/examples/example_ssh_localhost.rb +36 -0
- data/expect-behaviors.gemspec +95 -0
- data/lib/expect/behavior.rb +134 -0
- data/lib/expect/match.rb +39 -0
- data/lib/expect/ssh.rb +161 -0
- data/lib/expect/timeout_error.rb +4 -0
- data/tasks/doc.rake +10 -0
- data/tasks/jeweler.rake +14 -0
- data/tasks/test.rake +32 -0
- data/test/acceptance/sshd/erb/ssh_config.erb +8 -0
- data/test/acceptance/sshd/erb/sshd_config.erb +65 -0
- data/test/acceptance/test_expect_behaviors_slow_tests.rb +109 -0
- data/test/acceptance/test_expect_ssh.rb +97 -0
- data/test/class_including_expect_behavior.rb +33 -0
- data/test/helper.rb +22 -0
- data/test/sshd.rb +139 -0
- data/test/unit/expect/test_expect_behaviors.rb +178 -0
- data/test/unit/expect/test_expect_match.rb +64 -0
- data/test/unit/expect/test_includer_class.rb +34 -0
- metadata +250 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'sshd'
|
3
|
+
|
4
|
+
require 'expect/ssh'
|
5
|
+
|
6
|
+
class TestExpectSSH < Test::Unit::TestCase
|
7
|
+
|
8
|
+
def self.startup
|
9
|
+
@@hostname = '127.0.0.1'
|
10
|
+
@@username = ENV['USER']
|
11
|
+
@@sshd = SSHD.new
|
12
|
+
@@sshd.start
|
13
|
+
@@port = @@sshd.port
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
def self.shutdown
|
18
|
+
@@sshd.teardown
|
19
|
+
end
|
20
|
+
|
21
|
+
context :init_and_start do
|
22
|
+
|
23
|
+
setup do
|
24
|
+
@ssh = Expect::SSH.new(@@hostname, @@username, port: @@port, key_file: @@sshd.client_key_path, ignore_known_hosts: true)
|
25
|
+
end
|
26
|
+
|
27
|
+
should "return an options hash when calling #options" do
|
28
|
+
expected = {
|
29
|
+
:auth_methods => ["none", "publickey", "password"],
|
30
|
+
:keys => [@@sshd.client_key_path],
|
31
|
+
:logger => nil,
|
32
|
+
:port => @@port,
|
33
|
+
:user_known_hosts_file => "/dev/null"
|
34
|
+
}
|
35
|
+
result = @ssh.send(:options)
|
36
|
+
result[:logger] = nil
|
37
|
+
assert_equal(expected, result)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context :startup do
|
42
|
+
|
43
|
+
setup do
|
44
|
+
@ssh = Expect::SSH.new(@@hostname, @@username, port: @@port, key_file: @@sshd.client_key_path, ignore_known_hosts: true)
|
45
|
+
end
|
46
|
+
|
47
|
+
should "be able to authenticate" do
|
48
|
+
@ssh.start
|
49
|
+
@ssh.stop
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
context :expect do
|
55
|
+
|
56
|
+
setup do
|
57
|
+
@ssh = Expect::SSH.new(@@hostname, @@username, port: @@port, key_file: @@sshd.client_key_path, ignore_known_hosts: true)
|
58
|
+
@ssh.start
|
59
|
+
end
|
60
|
+
|
61
|
+
teardown do
|
62
|
+
@ssh.stop
|
63
|
+
end
|
64
|
+
|
65
|
+
should "be able to send a few commands" do
|
66
|
+
result = @ssh.expect do
|
67
|
+
when_matching(/.*\$.*/) { @exp_match }
|
68
|
+
end
|
69
|
+
assert_match(/Last login:/, result.to_s)
|
70
|
+
@ssh.send_data("cat /etc/resolv.conf")
|
71
|
+
result = @ssh.expect do
|
72
|
+
when_matching(/.*nameserver.*/) { @exp_match }
|
73
|
+
end
|
74
|
+
assert_match(/nameserver [\d\.]+.*/, result.exact_match_string)
|
75
|
+
@ssh.send_data("date")
|
76
|
+
result = @ssh.expect do
|
77
|
+
when_matching(/.*20.*/) { @exp_match }
|
78
|
+
end
|
79
|
+
assert_match(/20/, result.exact_match_string)
|
80
|
+
end
|
81
|
+
|
82
|
+
should "timeout as expected" do
|
83
|
+
result = @ssh.expect do
|
84
|
+
when_matching(/.*\$.*/) { @exp_match }
|
85
|
+
end
|
86
|
+
assert_match(/Last login:/, result.to_s)
|
87
|
+
@ssh.send_data("sleep 2")
|
88
|
+
result = @ssh.expect do
|
89
|
+
when_matching(/.*\$.*/) { @exp_match }
|
90
|
+
when_timeout(1) { "timeout" }
|
91
|
+
end
|
92
|
+
assert_match(/timeout/, result)
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'expect/behavior'
|
2
|
+
|
3
|
+
class ClassIncludingExpectBehavior
|
4
|
+
include Expect::Behavior
|
5
|
+
# :Required methods to be created by the class mixing Expect::Behaviors :
|
6
|
+
# #exp_process - should do one iteration of handle input and append buffer
|
7
|
+
# #exp_buffer - provide the current buffer contents and empty it
|
8
|
+
|
9
|
+
attr_accessor :exp_buffer_values, :wait_sec
|
10
|
+
|
11
|
+
def initialize(wait: nil, values: [])
|
12
|
+
@exp_buffer = ''
|
13
|
+
@exp_buffer_values = values
|
14
|
+
@wait_sec = wait
|
15
|
+
end
|
16
|
+
|
17
|
+
##
|
18
|
+
# exp_buffer - provide the current buffer contents and empty it
|
19
|
+
def exp_buffer
|
20
|
+
result = @exp_buffer
|
21
|
+
@exp_buffer = ''
|
22
|
+
result
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# exp_process - should do one iteration of handle input and append buffer
|
27
|
+
def exp_process
|
28
|
+
sleep(@wait_sec.to_f)
|
29
|
+
# handle input
|
30
|
+
@exp_buffer << @exp_buffer_values.shift.to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "codeclimate-test-reporter"
|
2
|
+
CodeClimate::TestReporter.start
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'bundler'
|
6
|
+
gem 'mocha'
|
7
|
+
require 'mocha/test_unit'
|
8
|
+
begin
|
9
|
+
Bundler.setup(:default, :development)
|
10
|
+
rescue Bundler::BundlerError => e
|
11
|
+
$stderr.puts e.message
|
12
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
13
|
+
exit e.status_code
|
14
|
+
end
|
15
|
+
require 'test/unit'
|
16
|
+
require 'shoulda'
|
17
|
+
|
18
|
+
# add lib folder from parent
|
19
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
20
|
+
# ...and the test folder
|
21
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
22
|
+
require 'expect/behavior'
|
data/test/sshd.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
|
6
|
+
##
|
7
|
+
# A Helper to start a second instance of SSHD on an unprivilege port which allows for a custom client key
|
8
|
+
# All keys for this server are created just in time.
|
9
|
+
class SSHD
|
10
|
+
|
11
|
+
TEST_ROOT = File.dirname(__FILE__)
|
12
|
+
SSHD_CFG_ROOT = File.join(TEST_ROOT, 'acceptance', 'sshd')
|
13
|
+
SSHD_TMP_ROOT = File.join(TEST_ROOT, 'tmp', 'sshd')
|
14
|
+
PIDFILE_PATH = File.join(SSHD_TMP_ROOT, 'sshd.pid')
|
15
|
+
ERB_ROOT = File.join(SSHD_CFG_ROOT, 'erb')
|
16
|
+
SSHD_CONFIG_ERB_PATH = File.join(ERB_ROOT, 'sshd_config.erb')
|
17
|
+
SSH_CONFIG_ERB_PATH = File.join(ERB_ROOT, 'ssh_config.erb')
|
18
|
+
SSHD_CONFIG_PATH = File.join(SSHD_TMP_ROOT, 'sshd_config')
|
19
|
+
SSH_CONFIG_PATH = File.join(SSHD_TMP_ROOT, 'ssh_config')
|
20
|
+
SSHD_LOG_PATH = File.join(SSHD_TMP_ROOT, 'sshd.log')
|
21
|
+
|
22
|
+
KEY_ROOT = SSHD_TMP_ROOT
|
23
|
+
SSHD_CLIENT_KEY_PATH = File.join(KEY_ROOT, 'id_rsa')
|
24
|
+
SSHD_CLIENT_PUBKEY_PATH = File.join(KEY_ROOT, 'id_rsa.pub')
|
25
|
+
SSHD_RSA_HOST_KEY_PATH = File.join(KEY_ROOT, 'ssh_host_key_rsa')
|
26
|
+
SSHD_DSA_HOST_KEY_PATH = File.join(KEY_ROOT, 'ssh_host_key_dsa')
|
27
|
+
SSHD_AUTHORIZED_KEYS_PATH = SSHD_CLIENT_PUBKEY_PATH
|
28
|
+
|
29
|
+
attr_reader :port
|
30
|
+
|
31
|
+
def initialize(address = '127.0.0.1')
|
32
|
+
@tcpserver = nil
|
33
|
+
@address = address
|
34
|
+
@port = reserve_unprivileged_tcp_port
|
35
|
+
@sshd_filepath = %x(which sshd).chomp
|
36
|
+
@ssh_keygen_filepath = %x(which ssh-keygen).chomp
|
37
|
+
teardown
|
38
|
+
end
|
39
|
+
|
40
|
+
def client_key_path
|
41
|
+
SSHD_CLIENT_KEY_PATH
|
42
|
+
end
|
43
|
+
|
44
|
+
def start
|
45
|
+
unless openssh_files_found?
|
46
|
+
raise(RuntimeError, "[SSHD] Error: Unable to locate sshd or ssh-keygen.")
|
47
|
+
end
|
48
|
+
create_sshd_config
|
49
|
+
create_ssh_config
|
50
|
+
generate_keys
|
51
|
+
release_port
|
52
|
+
start_ssh_server
|
53
|
+
end
|
54
|
+
|
55
|
+
def stop
|
56
|
+
this_pid = pid
|
57
|
+
unless this_pid.nil?
|
58
|
+
$stdout.puts("[SSHD] [#{__method__}]: Killing SSHD, [pid=#{this_pid}]")
|
59
|
+
begin
|
60
|
+
Process.kill(0, this_pid)
|
61
|
+
rescue Errno::ESRCH
|
62
|
+
$stderr.puts("[SSHD] [#{__method__}]: No Action: Process not found, [pid=#{this_pid}]")
|
63
|
+
ensure
|
64
|
+
File.delete(PIDFILE_PATH) if File.exists?(PIDFILE_PATH)
|
65
|
+
end
|
66
|
+
else
|
67
|
+
$stderr.puts("[SSHD] [#{__method__}]: No Action: PIDFILE Doesnt exist: #{PIDFILE_PATH}")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def teardown
|
72
|
+
stop
|
73
|
+
clean_tmp_root
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
#####################
|
78
|
+
private
|
79
|
+
#####################
|
80
|
+
|
81
|
+
def clean_tmp_root
|
82
|
+
FileUtils.rmtree(SSHD_TMP_ROOT)
|
83
|
+
FileUtils.mkpath(SSHD_TMP_ROOT)
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
|
87
|
+
def create_ssh_config
|
88
|
+
erb = ERB.new(IO.read(SSH_CONFIG_ERB_PATH))
|
89
|
+
result = erb.result(binding)
|
90
|
+
File.open(SSH_CONFIG_PATH, 'w') {|f| f.write(result)}
|
91
|
+
result
|
92
|
+
end
|
93
|
+
|
94
|
+
def create_sshd_config
|
95
|
+
erb = ERB.new(IO.read(SSHD_CONFIG_ERB_PATH))
|
96
|
+
result = erb.result(binding)
|
97
|
+
File.open(SSHD_CONFIG_PATH, 'w') {|f| f.write(result)}
|
98
|
+
result
|
99
|
+
end
|
100
|
+
|
101
|
+
def generate_keys
|
102
|
+
[SSHD_CLIENT_KEY_PATH, SSHD_RSA_HOST_KEY_PATH, SSHD_DSA_HOST_KEY_PATH].each do |key_path|
|
103
|
+
%x(#{@ssh_keygen_filepath} -t rsa -b 4096 -C user@localhost -f #{key_path} -N '')
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def openssh_files_found?
|
108
|
+
not @ssh_keygen_filepath.empty? and not @sshd_filepath.empty?
|
109
|
+
end
|
110
|
+
|
111
|
+
def pid
|
112
|
+
pid = nil
|
113
|
+
if File.exists?(PIDFILE_PATH)
|
114
|
+
pid = IO.read(PIDFILE_PATH).chomp.to_i
|
115
|
+
end
|
116
|
+
pid
|
117
|
+
end
|
118
|
+
|
119
|
+
def reserve_unprivileged_tcp_port
|
120
|
+
@tcpserver ||= TCPServer.new(@host, 0)
|
121
|
+
@port = @tcpserver.addr[1]
|
122
|
+
end
|
123
|
+
|
124
|
+
def release_port
|
125
|
+
@tcpserver.close
|
126
|
+
@tcpserver = nil
|
127
|
+
@port
|
128
|
+
end
|
129
|
+
|
130
|
+
def start_ssh_server
|
131
|
+
%x(#{@sshd_filepath} -4 -f #{SSHD_CONFIG_PATH})
|
132
|
+
# -E #{SSHD_LOG_PATH}
|
133
|
+
$stdout.puts("[SSHD] [#{__method__}]: Starting on [port=#{@port}]")
|
134
|
+
sleep(0.5)
|
135
|
+
$stdout.puts("[SSHD] [#{__method__}]: Started on [port=#{@port}] [pid=#{pid}]")
|
136
|
+
pid
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
@@ -0,0 +1,178 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'expect/behavior'
|
3
|
+
require 'class_including_expect_behavior'
|
4
|
+
gem 'mocha'
|
5
|
+
require 'mocha/test_unit'
|
6
|
+
|
7
|
+
class TestExpectBehaviors < Test::Unit::TestCase
|
8
|
+
|
9
|
+
####################################
|
10
|
+
context "expect" do
|
11
|
+
setup do
|
12
|
+
@values = []
|
13
|
+
@values << "the sun is a mass"
|
14
|
+
@values << "\nof incandescent gas"
|
15
|
+
@values << "\nswitch-prompt#"
|
16
|
+
@values << "\nblip blip"
|
17
|
+
@values << "\nblah blah"
|
18
|
+
@values << "\nswitch-prompt2#"
|
19
|
+
@includer = ClassIncludingExpectBehavior.new(values: @values)
|
20
|
+
@wait_sec = 0.1
|
21
|
+
end
|
22
|
+
|
23
|
+
should "match up to first switch-prompt" do
|
24
|
+
result = @includer.expect do
|
25
|
+
when_matching(/switch-prompt#/) do
|
26
|
+
@exp_match
|
27
|
+
end
|
28
|
+
end
|
29
|
+
expected = "the sun is a mass\nof incandescent gas\nswitch-prompt#"
|
30
|
+
assert_equal(expected, result.to_s)
|
31
|
+
end
|
32
|
+
|
33
|
+
should "timeout for switch-prompt2#" do
|
34
|
+
@includer.wait_sec = @wait_sec
|
35
|
+
@includer.exp_timeout_sec = @wait_sec * 4
|
36
|
+
result = @includer.expect do
|
37
|
+
when_matching(/switch-prompt2#/) do
|
38
|
+
@exp_match
|
39
|
+
end
|
40
|
+
when_timeout(@wait_sec * 3) do
|
41
|
+
"timed out"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
assert_equal('timed out', result.to_s)
|
45
|
+
end
|
46
|
+
|
47
|
+
should "not timeout for switch-prompt2# if expect_continue for blip" do
|
48
|
+
omit("See acceptance version of this test... doesn't work for such a short interval: (#{@wait_sec}")
|
49
|
+
end
|
50
|
+
|
51
|
+
should "timeout for switch-prompt2# if expect_continue for incandescent" do
|
52
|
+
omit("See acceptance version of this test... doesn't work for such a short interval: (#{@wait_sec}")
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
should "handle subsequent expect statements" do
|
57
|
+
@includer.wait_sec = @wait_sec
|
58
|
+
@includer.expect do
|
59
|
+
when_matching(/incandescent/) do
|
60
|
+
@exp_match
|
61
|
+
end
|
62
|
+
when_timeout(@wait_sec * 3) do
|
63
|
+
"timed out"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
result = @includer.expect do
|
67
|
+
when_matching(/switch-prompt#/) do
|
68
|
+
@exp_match
|
69
|
+
end
|
70
|
+
when_timeout(@wait_sec * 3) do
|
71
|
+
"timed out"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
expected = "\nswitch-prompt#"
|
75
|
+
assert_equal(expected, result.to_s)
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
####################################
|
82
|
+
context "initialize_expect" do
|
83
|
+
setup do
|
84
|
+
@includer = ClassIncludingExpectBehavior.new(values: ["bobert"])
|
85
|
+
end
|
86
|
+
|
87
|
+
should "be run when calling expect" do
|
88
|
+
@includer.expects(:initialize_expect)
|
89
|
+
@includer.expects(:execute_expect_loop)
|
90
|
+
@includer.expect do
|
91
|
+
when_matching(/bob/) do
|
92
|
+
return_value
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
should "key instance variables undefined before first expect" do
|
98
|
+
# undefined/nil before first expect
|
99
|
+
assert_equal(nil, @includer.instance_variable_get(:@exp_match_registry))
|
100
|
+
assert_equal(nil, @includer.instance_variable_get(:@exp_timeout_sec))
|
101
|
+
assert_equal(nil, @includer.instance_variable_get(:@exp_match))
|
102
|
+
assert_equal(nil, @includer.instance_variable_get(:@exp_timeout_block))
|
103
|
+
assert_equal(nil, @includer.instance_variable_get(:@__exp_buffer))
|
104
|
+
end
|
105
|
+
|
106
|
+
should "setup instance variables prior to expect" do
|
107
|
+
@includer.expects(:execute_expect_loop) #stub out expect loop
|
108
|
+
@includer.expect do
|
109
|
+
"bob"
|
110
|
+
end
|
111
|
+
assert_equal({}, @includer.instance_variable_get(:@exp_match_registry))
|
112
|
+
assert_equal(10, @includer.instance_variable_get(:@exp_timeout_sec))
|
113
|
+
assert_equal(nil, @includer.instance_variable_get(:@exp_match))
|
114
|
+
assert_equal(nil, @includer.instance_variable_get(:@exp_timeout_block))
|
115
|
+
assert_equal('', @includer.instance_variable_get(:@__exp_buffer))
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
####################################
|
121
|
+
context "match registry" do
|
122
|
+
setup do
|
123
|
+
@includer = ClassIncludingExpectBehavior.new
|
124
|
+
end
|
125
|
+
|
126
|
+
should "be populated by when_matching statements" do
|
127
|
+
@includer.stubs(:execute_expect_loop)
|
128
|
+
return_value = "Matched BOB"
|
129
|
+
@includer.expect do
|
130
|
+
when_matching(/bob/) do
|
131
|
+
return_value
|
132
|
+
end
|
133
|
+
when_matching(/joe/) do
|
134
|
+
"Matched JOE"
|
135
|
+
end
|
136
|
+
when_timeout do
|
137
|
+
"TIMEOUT"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
assert_equal(/bob/, @includer.instance_variable_get(:@exp_match_registry).keys.first)
|
141
|
+
assert_equal(2, @includer.instance_variable_get(:@exp_match_registry).length)
|
142
|
+
assert_equal(return_value, @includer.instance_variable_get(:@exp_match_registry).values.first.call)
|
143
|
+
assert_equal("TIMEOUT", @includer.instance_variable_get(:@exp_timeout_block).call)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
####################################
|
149
|
+
context "timeout" do
|
150
|
+
setup do
|
151
|
+
@includer = ClassIncludingExpectBehavior.new(wait: 0.2)
|
152
|
+
end
|
153
|
+
|
154
|
+
should "raise TimeoutError when timeout is reached before match is found" do
|
155
|
+
@includer.exp_timeout_sec = 0.1
|
156
|
+
assert_raises(Expect::TimeoutError) do
|
157
|
+
@includer.expect do
|
158
|
+
when_matching(/bob/) do
|
159
|
+
return_value
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
should "execute arbitrary block on timeout with override" do
|
166
|
+
result = @includer.expect do
|
167
|
+
when_matching(/bob/) do
|
168
|
+
return_value
|
169
|
+
end
|
170
|
+
when_timeout(0.1) do
|
171
|
+
"timeout"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
assert_equal('timeout', result)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|