sphero_pwn 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|