sphero_pwn 0.0.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 +7 -0
- data/.document +5 -0
- data/.travis.yml +5 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +76 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +25 -0
- data/Rakefile +39 -0
- data/VERSION +1 -0
- data/lib/sphero_pwn.rb +20 -0
- data/lib/sphero_pwn/async.rb +10 -0
- data/lib/sphero_pwn/asyncs.rb +34 -0
- data/lib/sphero_pwn/asyncs/l1_diagnostics.rb +21 -0
- data/lib/sphero_pwn/channel.rb +65 -0
- data/lib/sphero_pwn/channel_recorder.rb +60 -0
- data/lib/sphero_pwn/command.rb +54 -0
- data/lib/sphero_pwn/commands.rb +3 -0
- data/lib/sphero_pwn/commands/get_versions.rb +44 -0
- data/lib/sphero_pwn/commands/l1_diagnostics.rb +16 -0
- data/lib/sphero_pwn/commands/ping.rb +15 -0
- data/lib/sphero_pwn/replay_channel.rb +81 -0
- data/lib/sphero_pwn/response.rb +47 -0
- data/lib/sphero_pwn/session.rb +160 -0
- data/lib/sphero_pwn/test_channel.rb +18 -0
- data/test/channel_recorder_test.rb +102 -0
- data/test/command_test.rb +35 -0
- data/test/commands/get_versions_test.rb +34 -0
- data/test/commands/l1_diagnostics_test.rb +40 -0
- data/test/commands/ping_test.rb +25 -0
- data/test/data/get_version.txt +2 -0
- data/test/data/l1_diagnostics.txt +2 -0
- data/test/data/ping.txt +2 -0
- data/test/helper.rb +37 -0
- data/test/replay_channel_test.rb +147 -0
- data/test/response_test.rb +15 -0
- data/test/session_test.rb +15 -0
- data/test/sphero_pwn_test.rb +4 -0
- metadata +181 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
# Asks the robot about the versions of its software stack components.
|
2
|
+
class SpheroPwn::Commands::GetVersions < SpheroPwn::Command
|
3
|
+
def initialize
|
4
|
+
super 0x00, 0x02, nil
|
5
|
+
end
|
6
|
+
|
7
|
+
# @see {SpheroPwn::Command#response_class}
|
8
|
+
def response_class
|
9
|
+
SpheroPwn::Commands::GetVersions::Response
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# The versions of a robot's software stack.
|
14
|
+
class SpheroPwn::Commands::GetVersions::Response < SpheroPwn::Response
|
15
|
+
# @return {Hash<Symbol, Number>} the software versions of the components in
|
16
|
+
# the robot's software stack
|
17
|
+
attr_reader :versions
|
18
|
+
|
19
|
+
# @see {SpheroPwn::Response#initialize}
|
20
|
+
def initialize(code_byte, sequence_byte, data_bytes)
|
21
|
+
super
|
22
|
+
|
23
|
+
@versions = {}
|
24
|
+
response_version = data_bytes[0]
|
25
|
+
if response_version >= 1
|
26
|
+
@versions.merge! model: data_bytes[1], hardware: data_bytes[2],
|
27
|
+
sphero_app: { version: data_bytes[3], revision: data_bytes[4] },
|
28
|
+
bootloader: self.class.parse_packed_nibble(data_bytes[5]),
|
29
|
+
basic: self.class.parse_packed_nibble(data_bytes[6]),
|
30
|
+
macros: self.class.parse_packed_nibble(data_bytes[7])
|
31
|
+
end
|
32
|
+
if response_version >= 2
|
33
|
+
@versions.merge! api: { major: data_bytes[8], minor: data_bytes[9] }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Decodes a version from packed nibble format.
|
38
|
+
#
|
39
|
+
# @param {Number} byte the byte value packing the version
|
40
|
+
# @return {Hash<Symbol, Number>} maps :major and :minor to version numbers
|
41
|
+
def self.parse_packed_nibble(byte)
|
42
|
+
{ major: (byte >> 4), minor: (byte & 0x0F) }
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Asks the robot to send ASCII diagnostic data.
|
2
|
+
class SpheroPwn::Commands::L1Diagnostics < SpheroPwn::Command
|
3
|
+
def initialize
|
4
|
+
super 0x00, 0x40, nil
|
5
|
+
end
|
6
|
+
|
7
|
+
# @see {SpheroPwn::Command#response_class}
|
8
|
+
def response_class
|
9
|
+
SpheroPwn::Commands::L1Diagnostics::Response
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# The response to an L1 diagnostics command.
|
14
|
+
class SpheroPwn::Commands::L1Diagnostics::Response < SpheroPwn::Response
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Asks the robot to echo this message.
|
2
|
+
class SpheroPwn::Commands::Ping < SpheroPwn::Command
|
3
|
+
def initialize
|
4
|
+
super 0x00, 0x01, nil
|
5
|
+
end
|
6
|
+
|
7
|
+
# @see {SpheroPwn::Command#response_class}
|
8
|
+
def response_class
|
9
|
+
SpheroPwn::Commands::Ping::Response
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# The response to an echo command.
|
14
|
+
class SpheroPwn::Commands::Ping::Response < SpheroPwn::Response
|
15
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# Implements the Channel API using data from a file.
|
2
|
+
class SpheroPwn::ReplayChannel
|
3
|
+
#
|
4
|
+
#
|
5
|
+
# @param {String} recording_path the file storing the recording
|
6
|
+
def initialize(recording_path)
|
7
|
+
@file = File.open recording_path, 'rt'
|
8
|
+
@lines = @file.read.split("\n").map do |line|
|
9
|
+
tokens = line.split ' '
|
10
|
+
|
11
|
+
tokens.map! do |token|
|
12
|
+
case token
|
13
|
+
when '<'
|
14
|
+
:recv
|
15
|
+
when '>'
|
16
|
+
:send
|
17
|
+
else
|
18
|
+
token.to_i 16
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
@line_index = 0
|
24
|
+
@byte_index = 0
|
25
|
+
end
|
26
|
+
|
27
|
+
# @see {Channel#recv_bytes}
|
28
|
+
def recv_bytes(count)
|
29
|
+
if @lines[@line_index].nil? || @lines[@line_index].first != :recv
|
30
|
+
raise RuntimeError, "Received data at an unexpected time!"
|
31
|
+
end
|
32
|
+
|
33
|
+
line_bytes = @lines[@line_index].length - 1
|
34
|
+
if @byte_index + count > line_bytes
|
35
|
+
raise ArgumentError, "Attempted to receive #{count} bytes, but only " +
|
36
|
+
"#{line_bytes - @byte_index} are available"
|
37
|
+
end
|
38
|
+
|
39
|
+
data = @lines[@line_index][@byte_index + 1, count].pack('C*')
|
40
|
+
@byte_index += count
|
41
|
+
|
42
|
+
if @byte_index == line_bytes
|
43
|
+
@byte_index = 0
|
44
|
+
@line_index += 1
|
45
|
+
end
|
46
|
+
|
47
|
+
data
|
48
|
+
end
|
49
|
+
|
50
|
+
# @see {Channel#send_bytes}
|
51
|
+
def send_bytes(bytes)
|
52
|
+
if @lines[@line_index] && @lines[@line_index].first == :recv
|
53
|
+
line_bytes = @lines[@line_index].length - 1
|
54
|
+
raise RuntimeError, "Sent data before receiving " +
|
55
|
+
"#{line_bytes - @byte_index} of #{line_bytes} bytes!"
|
56
|
+
end
|
57
|
+
|
58
|
+
if @lines[@line_index].nil? || @lines[@line_index].first != :send
|
59
|
+
raise RuntimeError, "Sent data at an unexpected time!"
|
60
|
+
end
|
61
|
+
|
62
|
+
expected = @lines[@line_index][1..-1]
|
63
|
+
data = bytes.unpack 'C*'
|
64
|
+
if data != expected
|
65
|
+
raise ArgumentError, "Incorrect bytes sent! " +
|
66
|
+
"Expected: #{expected.inspect} Got: #{data.inspect}"
|
67
|
+
end
|
68
|
+
@line_index += 1
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
# @see {Channel#close}
|
73
|
+
def close
|
74
|
+
if @line_index != @lines.length
|
75
|
+
ops_left = @lines.length - @line_index
|
76
|
+
raise RuntimeError, "Closed before performing #{ops_left} operations!"
|
77
|
+
end
|
78
|
+
|
79
|
+
self
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# Superclass for all messages sent from the robot in response to commands.
|
2
|
+
class SpheroPwn::Response
|
3
|
+
# @return {Symbol} the command's response
|
4
|
+
attr_reader :code
|
5
|
+
|
6
|
+
# @return {Number} the sequence number matching the response to its command
|
7
|
+
attr_reader :sequence
|
8
|
+
|
9
|
+
# @return {Array<Number>} the additional payload bytes in the response; this
|
10
|
+
# array is frozen
|
11
|
+
attr_reader :data_bytes
|
12
|
+
|
13
|
+
# Parses a response to a command.
|
14
|
+
#
|
15
|
+
# @param {Number} code_byte the response code number
|
16
|
+
# @param {Number} sequence_byte the sequence number matching the response to
|
17
|
+
# its command
|
18
|
+
# @param {Array<Number>} data_bytes the additional response payload; can be
|
19
|
+
# empty, cannot be nil; the constructor takes ownership of the array and
|
20
|
+
# freezes it
|
21
|
+
def initialize(code_byte, sequence_byte, data_bytes)
|
22
|
+
@code = RESPONSE_CODES[code_byte] || :unknown
|
23
|
+
@sequence = sequence_byte
|
24
|
+
@data_bytes = data_bytes.freeze
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return {Hash<Integer, Symbol>} maps error codes to symbols
|
28
|
+
RESPONSE_CODES = {
|
29
|
+
0x00 => :ok, # Command succeeded.
|
30
|
+
0x01 => :generic_error, # General, non-specific error.
|
31
|
+
0x02 => :bad_checksum, # Received checksum failure.
|
32
|
+
0x03 => :got_fragment, # Received command fragment.
|
33
|
+
0x04 => :bad_command, # Unknown command ID.
|
34
|
+
0x05 => :unsupported, # Command currently unsupported.
|
35
|
+
0x06 => :bad_message, # Bad message format.
|
36
|
+
0x07 => :invalid_param, # Parameter value(s) invalid.
|
37
|
+
0x08 => :exec_failure, # Failed to execute command.
|
38
|
+
0x09 => :bad_device, # Unknown Device ID.
|
39
|
+
0x0A => :ram_busy, # Generic RAM access needed but it is busy.
|
40
|
+
0x0B => :bad_password, # Supplied password incorrect.
|
41
|
+
0x31 => :low_battery, # Voltage too low for reflash operation.
|
42
|
+
0x32 => :bad_page, # Illegal page number provided.
|
43
|
+
0x33 => :flash_fail, # Page did not reprogram correctly.
|
44
|
+
0x34 => :main_app_corrupt, # Main Application corrupt.
|
45
|
+
0x35 => :timed_out, # Msg state machine timed out.
|
46
|
+
}.freeze
|
47
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
# A communication session with a robot.
|
4
|
+
class SpheroPwn::Session
|
5
|
+
# @param {Channel} channel the byte-level communication channel with the
|
6
|
+
# robot
|
7
|
+
def initialize(channel)
|
8
|
+
@channel = channel
|
9
|
+
|
10
|
+
|
11
|
+
# Must be acquired to change any of the values below.
|
12
|
+
@sequence_lock = Mutex.new
|
13
|
+
# Maps sequence numbers to responses expected from the server.
|
14
|
+
@pending_responses = {}
|
15
|
+
# Sweeps the space of valid sequence numbers.
|
16
|
+
@last_sequence = 0
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
# Terminates this session and closes the underlying communication channel.
|
21
|
+
#
|
22
|
+
# @return {Session} self
|
23
|
+
def close
|
24
|
+
@channel.close
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param {Command} command the command to be sent
|
29
|
+
# @return {Session} self
|
30
|
+
def send_command(command)
|
31
|
+
sequence = 0
|
32
|
+
if command.expects_response?
|
33
|
+
@sequence_lock.synchronize do
|
34
|
+
sequence = alloc_sequence
|
35
|
+
@pending_responses[sequence] = command.response_class
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
bytes = command.to_bytes sequence
|
40
|
+
@channel.send_bytes bytes
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
# Reads messages from the robot until a response is received.
|
45
|
+
#
|
46
|
+
# @return {Response} the response received
|
47
|
+
def recv_until_response
|
48
|
+
loop do
|
49
|
+
message = recv_message
|
50
|
+
if message && message.kind_of?(SpheroPwn::Response)
|
51
|
+
return message
|
52
|
+
end
|
53
|
+
|
54
|
+
# TODO(pwnall): customizable sleep interval
|
55
|
+
sleep 0.05
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Reads a message from the robot.
|
60
|
+
#
|
61
|
+
# This method blocks until a message is available. The method can be called
|
62
|
+
# in a loop on a dedicated thread, and will synchronize correctly with
|
63
|
+
# {Session#send_command}.
|
64
|
+
#
|
65
|
+
# @return {Response, Async} the response read from the channel; can be nil if
|
66
|
+
# no message was received or if the checksum verification failed
|
67
|
+
def recv_message
|
68
|
+
return nil if @channel.recv_bytes(1).ord != 0xFF
|
69
|
+
|
70
|
+
packet_type = @channel.recv_bytes 1
|
71
|
+
case packet_type.ord
|
72
|
+
when 0xFF
|
73
|
+
read_response
|
74
|
+
when 0xFE
|
75
|
+
read_async_message
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Finds an unused sequence number.
|
80
|
+
#
|
81
|
+
# Sending a command that requires a response allocates a sequence number.
|
82
|
+
# Receiving the required response frees up the sequence number.
|
83
|
+
#
|
84
|
+
# The caller should own the sequence_lock mutex.
|
85
|
+
#
|
86
|
+
# @return {Number} a sequence number that can be used for a command; the
|
87
|
+
# sequence number is not considered to be allocated until the caller
|
88
|
+
# inserts it as a key in @pending_commands
|
89
|
+
def alloc_sequence
|
90
|
+
begin
|
91
|
+
@last_sequence = (@last_sequence + 1) & 0xFF
|
92
|
+
end while @pending_responses.has_key? @last_sequence
|
93
|
+
@last_sequence
|
94
|
+
end
|
95
|
+
private :alloc_sequence
|
96
|
+
|
97
|
+
# Reads the response to a command from the channel.
|
98
|
+
#
|
99
|
+
# This assumes that the start-of-packet was already read and indicates a
|
100
|
+
# command response.
|
101
|
+
#
|
102
|
+
# @return {Response} the parsed response
|
103
|
+
def read_response
|
104
|
+
header_bytes = @channel.recv_bytes(3).unpack('C*')
|
105
|
+
response_code, sequence, data_length = *header_bytes
|
106
|
+
|
107
|
+
# It may seem that it'd be better to look up the sequence number and bail
|
108
|
+
# early if we don't find it. However, in order to avoid misleading error
|
109
|
+
# messages, we don't want to touch anything in the message until we know
|
110
|
+
# that the checksum is valid.
|
111
|
+
data_bytes = @channel.recv_bytes(data_length).unpack('C*')
|
112
|
+
checksum = data_bytes.pop
|
113
|
+
unless self.class.valid_checksum?(header_bytes, data_bytes, checksum)
|
114
|
+
return nil
|
115
|
+
end
|
116
|
+
|
117
|
+
klass = @sequence_lock.synchronize { @pending_responses.delete sequence }
|
118
|
+
return nil if klass.nil?
|
119
|
+
|
120
|
+
klass.new response_code, sequence, data_bytes
|
121
|
+
end
|
122
|
+
private :read_response
|
123
|
+
|
124
|
+
# Reads an asynchronous message from the channel.
|
125
|
+
#
|
126
|
+
# This assumes that the start-of-packet was already read and indicates an
|
127
|
+
# asynchronous message.
|
128
|
+
#
|
129
|
+
# @return {Response} the parsed response
|
130
|
+
def read_async_message
|
131
|
+
header_bytes = @channel.recv_bytes(3).unpack('C*')
|
132
|
+
class_id, length_msb, length_lsb = *header_bytes
|
133
|
+
data_length = (length_msb << 8) | length_lsb
|
134
|
+
|
135
|
+
# It may seem that it'd be better to look up the sequence number and bail
|
136
|
+
# early if we don't find it. However, in order to avoid misleading error
|
137
|
+
# messages, we don't want to touch anything in the message until we know
|
138
|
+
# that the checksum is valid.
|
139
|
+
data_bytes = @channel.recv_bytes(data_length).unpack('C*')
|
140
|
+
checksum = data_bytes.pop
|
141
|
+
unless self.class.valid_checksum?(header_bytes, data_bytes, checksum)
|
142
|
+
return nil
|
143
|
+
end
|
144
|
+
|
145
|
+
SpheroPwn::Asyncs.create class_id, data_bytes
|
146
|
+
end
|
147
|
+
private :read_async_message
|
148
|
+
|
149
|
+
# Checks if a message's checksum matches its contents.
|
150
|
+
#
|
151
|
+
# @param {Array<Number>} header_bytes the header semantics differ between
|
152
|
+
# command responses and async messages, but both have 3 bytes
|
153
|
+
# @param {Array<Number>} data_bytes
|
154
|
+
def self.valid_checksum?(header_bytes, data_bytes, checksum)
|
155
|
+
sum = 0
|
156
|
+
header_bytes.each { |byte| sum += byte }
|
157
|
+
data_bytes.each { |byte| sum += byte }
|
158
|
+
checksum == ((sum & 0xFF) ^ 0xFF)
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module SpheroPwn
|
2
|
+
# Intelligently toggles between recording and replaying test data.
|
3
|
+
#
|
4
|
+
# @param {String} rfconn_path the path to the device file connecting to the
|
5
|
+
# robot's Bluetooth RFCONN service
|
6
|
+
# @param {String} recording_path the file storing the recording
|
7
|
+
# @return {Channel} if the recording file at the given path exists, the
|
8
|
+
# return value is a {ReplayChannel} that uses the file; otherwise, the
|
9
|
+
# return value is a {ChannelRecorder} that creates the recording
|
10
|
+
def self.new_test_channel(rfconn_path, recording_path)
|
11
|
+
if File.exist? recording_path
|
12
|
+
ReplayChannel.new recording_path
|
13
|
+
else
|
14
|
+
channel = Channel.new rfconn_path
|
15
|
+
ChannelRecorder.new channel, recording_path
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require_relative './helper.rb'
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
describe SpheroPwn::ChannelRecorder do
|
6
|
+
describe 'with a channel that always responds' do
|
7
|
+
before do
|
8
|
+
@channel = MiniTest::Mock.new
|
9
|
+
@channel.expect :send_bytes, @channel, ["Hello\n"]
|
10
|
+
@channel.expect :recv_bytes, 'Hello', [5]
|
11
|
+
@channel.expect :recv_bytes, " too!\n", [6]
|
12
|
+
@channel.expect :send_bytes, @channel, ['OK']
|
13
|
+
@channel.expect :send_bytes, @channel, ["Bye\n"]
|
14
|
+
@channel.expect :recv_bytes, 'Bye', [3]
|
15
|
+
@channel.expect :recv_bytes, " bye\n", [5]
|
16
|
+
@channel.expect :close, @channel, []
|
17
|
+
end
|
18
|
+
|
19
|
+
after { @channel.verify }
|
20
|
+
|
21
|
+
it 'proxies calls correctly' do
|
22
|
+
output = StringIO.new
|
23
|
+
recorder = File.stub :open, output do
|
24
|
+
SpheroPwn::ChannelRecorder.new @channel, '/tmp/stubbed'
|
25
|
+
end
|
26
|
+
|
27
|
+
assert_equal recorder, recorder.send_bytes("Hello\n")
|
28
|
+
assert_equal 'Hello', recorder.recv_bytes(5)
|
29
|
+
assert_equal " too!\n", recorder.recv_bytes(6)
|
30
|
+
assert_equal recorder, recorder.send_bytes('OK')
|
31
|
+
assert_equal recorder, recorder.send_bytes("Bye\n")
|
32
|
+
assert_equal 'Bye', recorder.recv_bytes(3)
|
33
|
+
assert_equal " bye\n", recorder.recv_bytes(5)
|
34
|
+
assert_equal recorder, recorder.close
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'produces the output file correctly' do
|
38
|
+
output = StringIO.new
|
39
|
+
recorder = File.stub :open, output do
|
40
|
+
SpheroPwn::ChannelRecorder.new @channel, '/tmp/stubbed'
|
41
|
+
end
|
42
|
+
|
43
|
+
recorder.send_bytes "Hello\n"
|
44
|
+
recorder.recv_bytes 5
|
45
|
+
recorder.recv_bytes 6
|
46
|
+
recorder.send_bytes 'OK'
|
47
|
+
recorder.send_bytes "Bye\n"
|
48
|
+
recorder.recv_bytes 3
|
49
|
+
recorder.recv_bytes 5
|
50
|
+
recorder.close
|
51
|
+
|
52
|
+
golden = <<END_STRING
|
53
|
+
> 48 65 6C 6C 6F 0A
|
54
|
+
< 48 65 6C 6C 6F 20 74 6F 6F 21 0A
|
55
|
+
> 4F 4B
|
56
|
+
> 42 79 65 0A
|
57
|
+
< 42 79 65 20 62 79 65 0A
|
58
|
+
END_STRING
|
59
|
+
assert_equal golden, output.string
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe 'with a channel that sometimes returns nils' do
|
64
|
+
before do
|
65
|
+
@channel = MiniTest::Mock.new
|
66
|
+
@channel.expect :send_bytes, @channel, ["Hello\n"]
|
67
|
+
@channel.expect :recv_bytes, nil, [5]
|
68
|
+
@channel.expect :recv_bytes, "Hello too!\n", [11]
|
69
|
+
@channel.expect :close, @channel, []
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'proxies calls correctly' do
|
73
|
+
output = StringIO.new
|
74
|
+
recorder = File.stub :open, output do
|
75
|
+
SpheroPwn::ChannelRecorder.new @channel, '/tmp/stubbed'
|
76
|
+
end
|
77
|
+
|
78
|
+
assert_equal recorder, recorder.send_bytes("Hello\n")
|
79
|
+
assert_equal nil, recorder.recv_bytes(5)
|
80
|
+
assert_equal "Hello too!\n", recorder.recv_bytes(11)
|
81
|
+
assert_equal recorder, recorder.close
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'produces the output file correctly' do
|
85
|
+
output = StringIO.new
|
86
|
+
recorder = File.stub :open, output do
|
87
|
+
SpheroPwn::ChannelRecorder.new @channel, '/tmp/stubbed'
|
88
|
+
end
|
89
|
+
|
90
|
+
recorder.send_bytes "Hello\n"
|
91
|
+
recorder.recv_bytes 5
|
92
|
+
recorder.recv_bytes 11
|
93
|
+
recorder.close
|
94
|
+
|
95
|
+
golden = <<END_STRING
|
96
|
+
> 48 65 6C 6C 6F 0A
|
97
|
+
< 48 65 6C 6C 6F 20 74 6F 6F 21 0A
|
98
|
+
END_STRING
|
99
|
+
assert_equal golden, output.string
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|