mirror of
https://github.com/google/pebble.git
synced 2025-05-21 02:45:00 +00:00
Import of the watch repository from Pebble
This commit is contained in:
commit
3b92768480
10334 changed files with 2564465 additions and 0 deletions
14
python_libs/pulse2/tests/__init__.py
Normal file
14
python_libs/pulse2/tests/__init__.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
60
python_libs/pulse2/tests/fake_timer.py
Normal file
60
python_libs/pulse2/tests/fake_timer.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
class FakeTimer(object):
|
||||
|
||||
TIMERS = []
|
||||
|
||||
def __init__(self, interval, function):
|
||||
self.interval = interval
|
||||
self.function = function
|
||||
self.started = False
|
||||
self.expired = False
|
||||
self.cancelled = False
|
||||
type(self).TIMERS.append(self)
|
||||
|
||||
def __repr__(self):
|
||||
state_flags = ''.join([
|
||||
'S' if self.started else 'N',
|
||||
'X' if self.expired else '.',
|
||||
'C' if self.cancelled else '.'])
|
||||
return '<FakeTimer({}, {}) {} at {:#x}>'.format(
|
||||
self.interval, self.function, state_flags, id(self))
|
||||
|
||||
def start(self):
|
||||
if self.started:
|
||||
raise RuntimeError("threads can only be started once")
|
||||
self.started = True
|
||||
|
||||
def cancel(self):
|
||||
self.cancelled = True
|
||||
|
||||
def expire(self):
|
||||
'''Simulate the timeout expiring.'''
|
||||
assert self.started, 'timer not yet started'
|
||||
assert not self.expired, 'timer can only expire once'
|
||||
self.expired = True
|
||||
self.function()
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return self.started and not self.expired and not self.cancelled
|
||||
|
||||
@classmethod
|
||||
def clear_timer_list(cls):
|
||||
cls.TIMERS = []
|
||||
|
||||
@classmethod
|
||||
def get_active_timers(cls):
|
||||
return [t for t in cls.TIMERS if t.is_active]
|
156
python_libs/pulse2/tests/test_framing.py
Normal file
156
python_libs/pulse2/tests/test_framing.py
Normal file
|
@ -0,0 +1,156 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import unittest
|
||||
|
||||
from pebble.pulse2 import framing
|
||||
|
||||
|
||||
class TestEncodeFrame(unittest.TestCase):
|
||||
|
||||
def test_empty_frame(self):
|
||||
# CRC-32 of nothing is 0
|
||||
# COBS encoding of b'\0\0\0\0' is b'\x01\x01\x01\x01\x01' (5 bytes)
|
||||
self.assertEqual(framing.encode_frame(b''),
|
||||
b'\x55\x01\x01\x01\x01\x01\x55')
|
||||
|
||||
def test_simple_data(self):
|
||||
self.assertEqual(framing.encode_frame(b'abcdefg'),
|
||||
b'\x55\x0cabcdefg\xa6\x6a\x2a\x31\x55')
|
||||
|
||||
def test_flag_in_datagram(self):
|
||||
# ASCII 'U' is 0x55 hex
|
||||
self.assertEqual(framing.encode_frame(b'QUACK'),
|
||||
b'\x55\x0aQ\0ACK\xdf\x8d\x80\x74\x55')
|
||||
|
||||
def test_flag_in_fcs(self):
|
||||
# crc32(b'R') -> 0x5767df55
|
||||
# Since there is an \x55 byte in the FCS, it must be substituted,
|
||||
# just like when that byte value is present in the datagram itself.
|
||||
self.assertEqual(framing.encode_frame(b'R'),
|
||||
b'\x55\x06R\0\xdf\x67\x57\x55')
|
||||
|
||||
|
||||
class TestFrameSplitter(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.splitter = framing.FrameSplitter()
|
||||
|
||||
def test_basic_functionality(self):
|
||||
self.splitter.write(b'\x55abcdefg\x55foobar\x55asdf\x55')
|
||||
self.assertEqual(list(self.splitter),
|
||||
[b'abcdefg', b'foobar', b'asdf'])
|
||||
|
||||
def test_wait_for_sync(self):
|
||||
self.splitter.write(b'garbage data\x55frame 1\x55')
|
||||
self.assertEqual(list(self.splitter), [b'frame 1'])
|
||||
|
||||
def test_doubled_flags(self):
|
||||
self.splitter.write(b'\x55abcd\x55\x55efgh\x55')
|
||||
self.assertEqual(list(self.splitter), [b'abcd', b'efgh'])
|
||||
|
||||
def test_multiple_writes(self):
|
||||
self.splitter.write(b'\x55ab')
|
||||
self.assertEqual(list(self.splitter), [])
|
||||
self.splitter.write(b'cd\x55')
|
||||
self.assertEqual(list(self.splitter), [b'abcd'])
|
||||
|
||||
def test_lots_of_writes(self):
|
||||
for char in b'\x55abcd\x55ef':
|
||||
self.splitter.write(bytearray([char]))
|
||||
self.assertEqual(list(self.splitter), [b'abcd'])
|
||||
|
||||
def test_iteration_pops_frames(self):
|
||||
self.splitter.write(b'\x55frame 1\x55frame 2\x55frame 3\x55')
|
||||
self.assertEqual(next(iter(self.splitter)), b'frame 1')
|
||||
self.assertEqual(list(self.splitter), [b'frame 2', b'frame 3'])
|
||||
|
||||
def test_stopiteration_latches(self):
|
||||
# The iterator protocol requires that once an iterator raises
|
||||
# StopIteration, it must continue to do so for all subsequent calls
|
||||
# to its next() method.
|
||||
self.splitter.write(b'\x55frame 1\x55')
|
||||
iterator = iter(self.splitter)
|
||||
self.assertEqual(next(iterator), b'frame 1')
|
||||
with self.assertRaises(StopIteration):
|
||||
next(iterator)
|
||||
next(iterator)
|
||||
self.splitter.write(b'\x55frame 2\x55')
|
||||
with self.assertRaises(StopIteration):
|
||||
next(iterator)
|
||||
self.assertEqual(list(self.splitter), [b'frame 2'])
|
||||
|
||||
def test_max_frame_length(self):
|
||||
splitter = framing.FrameSplitter(max_frame_length=6)
|
||||
splitter.write(
|
||||
b'\x5512345\x55123456\x551234567\x551234\x5512345678\x55')
|
||||
self.assertEqual(list(splitter), [b'12345', b'123456', b'1234'])
|
||||
|
||||
def test_dynamic_max_length_1(self):
|
||||
self.splitter.write(b'\x5512345')
|
||||
self.splitter.max_frame_length = 6
|
||||
self.splitter.write(b'6\x551234567\x551234\x55')
|
||||
self.assertEqual(list(self.splitter), [b'123456', b'1234'])
|
||||
|
||||
def test_dynamic_max_length_2(self):
|
||||
self.splitter.write(b'\x551234567')
|
||||
self.splitter.max_frame_length = 6
|
||||
self.splitter.write(b'89\x55123456\x55')
|
||||
self.assertEqual(list(self.splitter), [b'123456'])
|
||||
|
||||
|
||||
class TestDecodeTransparency(unittest.TestCase):
|
||||
|
||||
def test_easy_decode(self):
|
||||
self.assertEqual(framing.decode_transparency(b'\x06abcde'), b'abcde')
|
||||
|
||||
def test_escaped_flag(self):
|
||||
self.assertEqual(framing.decode_transparency(b'\x06Q\0ACK'), b'QUACK')
|
||||
|
||||
def test_flag_byte_in_frame(self):
|
||||
with self.assertRaises(framing.DecodeError):
|
||||
framing.decode_transparency(b'\x06ab\x55de')
|
||||
|
||||
def test_truncated_cobs_block(self):
|
||||
with self.assertRaises(framing.DecodeError):
|
||||
framing.decode_transparency(b'\x0aabc')
|
||||
|
||||
|
||||
class TestStripFCS(unittest.TestCase):
|
||||
|
||||
def test_frame_too_short(self):
|
||||
with self.assertRaises(framing.CorruptFrame):
|
||||
framing.strip_fcs(b'abcd')
|
||||
|
||||
def test_good_fcs(self):
|
||||
self.assertEqual(framing.strip_fcs(b'abcd\x11\xcd\x82\xed'), b'abcd')
|
||||
|
||||
def test_frame_corrupted(self):
|
||||
with self.assertRaises(framing.CorruptFrame):
|
||||
framing.strip_fcs(b'abce\x11\xcd\x82\xed')
|
||||
|
||||
def test_fcs_corrupted(self):
|
||||
with self.assertRaises(framing.CorruptFrame):
|
||||
framing.strip_fcs(b'abcd\x13\xcd\x82\xed')
|
||||
|
||||
|
||||
class TestDecodeFrame(unittest.TestCase):
|
||||
|
||||
def test_it_works(self):
|
||||
# Not much to test; decode_frame is just chained decode_transparency
|
||||
# with strip_fcs, and both of those have already been tested separately.
|
||||
self.assertEqual(framing.decode_frame(b'\x0aQ\0ACK\xdf\x8d\x80t'),
|
||||
b'QUACK')
|
261
python_libs/pulse2/tests/test_link.py
Normal file
261
python_libs/pulse2/tests/test_link.py
Normal file
|
@ -0,0 +1,261 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
import mock
|
||||
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
|
||||
from pebble.pulse2 import exceptions, framing, link, ppp
|
||||
|
||||
|
||||
class FakeIOStream(object):
|
||||
|
||||
def __init__(self):
|
||||
self.read_queue = queue.Queue()
|
||||
self.write_queue = queue.Queue()
|
||||
self.closed = False
|
||||
|
||||
def read(self, length):
|
||||
if self.closed:
|
||||
raise IOError('I/O operation on closed FakeIOStream')
|
||||
try:
|
||||
return self.read_queue.get(timeout=0.001)
|
||||
except queue.Empty:
|
||||
return b''
|
||||
|
||||
def write(self, data):
|
||||
if self.closed:
|
||||
raise IOError('I/O operation on closed FakeIOStream')
|
||||
self.write_queue.put(data)
|
||||
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
def pop_all_written_data(self):
|
||||
data = []
|
||||
try:
|
||||
while True:
|
||||
data.append(self.write_queue.get_nowait())
|
||||
except queue.Empty:
|
||||
pass
|
||||
return data
|
||||
|
||||
|
||||
class TestInterface(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.iostream = FakeIOStream()
|
||||
self.uut = link.Interface(self.iostream)
|
||||
self.addCleanup(self.iostream.close)
|
||||
# Speed up test execution by overriding the LCP timeout
|
||||
self.uut.lcp.restart_timeout = 0.001
|
||||
self.uut.lcp.ping = self.fake_ping
|
||||
self.ping_should_succeed = True
|
||||
|
||||
def fake_ping(self, cb, *args, **kwargs):
|
||||
cb(self.ping_should_succeed)
|
||||
|
||||
def test_send_packet(self):
|
||||
self.uut.send_packet(0x8889, b'data')
|
||||
self.assertIn(framing.encode_frame(ppp.encapsulate(0x8889, b'data')),
|
||||
self.iostream.pop_all_written_data())
|
||||
|
||||
def test_connect_returns_socket(self):
|
||||
self.assertIsNotNone(self.uut.connect(0xf0f1))
|
||||
|
||||
def test_send_from_socket(self):
|
||||
socket = self.uut.connect(0xf0f1)
|
||||
socket.send(b'data')
|
||||
self.assertIn(framing.encode_frame(ppp.encapsulate(0xf0f1, b'data')),
|
||||
self.iostream.pop_all_written_data())
|
||||
|
||||
def test_interface_closing_closes_sockets_and_iostream(self):
|
||||
socket1 = self.uut.connect(0xf0f1)
|
||||
socket2 = self.uut.connect(0xf0f3)
|
||||
self.uut.close()
|
||||
self.assertTrue(socket1.closed)
|
||||
self.assertTrue(socket2.closed)
|
||||
self.assertTrue(self.iostream.closed)
|
||||
|
||||
def test_iostream_closing_closes_interface_and_sockets(self):
|
||||
socket = self.uut.connect(0xf0f1)
|
||||
self.iostream.close()
|
||||
time.sleep(0.01) # Wait for receive thread to notice
|
||||
self.assertTrue(self.uut.closed)
|
||||
self.assertTrue(socket.closed)
|
||||
|
||||
def test_opening_two_sockets_on_same_protocol_is_an_error(self):
|
||||
socket1 = self.uut.connect(0xf0f1)
|
||||
with self.assertRaisesRegexp(ValueError, 'socket is already bound'):
|
||||
socket2 = self.uut.connect(0xf0f1)
|
||||
|
||||
def test_closing_socket_allows_another_to_be_opened(self):
|
||||
socket1 = self.uut.connect(0xf0f1)
|
||||
socket1.close()
|
||||
socket2 = self.uut.connect(0xf0f1)
|
||||
self.assertIsNot(socket1, socket2)
|
||||
|
||||
def test_sending_from_closed_interface_is_an_error(self):
|
||||
self.uut.close()
|
||||
with self.assertRaisesRegexp(ValueError, 'closed interface'):
|
||||
self.uut.send_packet(0x8889, b'data')
|
||||
|
||||
def test_get_link_returns_None_when_lcp_is_down(self):
|
||||
self.assertIsNone(self.uut.get_link(timeout=0))
|
||||
|
||||
def test_get_link_from_closed_interface_is_an_error(self):
|
||||
self.uut.close()
|
||||
with self.assertRaisesRegexp(ValueError, 'closed interface'):
|
||||
self.uut.get_link(timeout=0)
|
||||
|
||||
def test_get_link_when_lcp_is_up(self):
|
||||
self.uut.on_link_up()
|
||||
self.assertIsNotNone(self.uut.get_link(timeout=0))
|
||||
|
||||
def test_link_object_is_closed_when_lcp_goes_down(self):
|
||||
self.uut.on_link_up()
|
||||
link = self.uut.get_link(timeout=0)
|
||||
self.assertFalse(link.closed)
|
||||
self.uut.on_link_down()
|
||||
self.assertTrue(link.closed)
|
||||
|
||||
def test_lcp_bouncing_doesnt_reopen_old_link_object(self):
|
||||
self.uut.on_link_up()
|
||||
link1 = self.uut.get_link(timeout=0)
|
||||
self.uut.on_link_down()
|
||||
self.uut.on_link_up()
|
||||
link2 = self.uut.get_link(timeout=0)
|
||||
self.assertTrue(link1.closed)
|
||||
self.assertFalse(link2.closed)
|
||||
|
||||
def test_close_gracefully_shuts_down_lcp(self):
|
||||
self.uut.lcp.receive_configure_request_acceptable(0, b'')
|
||||
self.uut.lcp.receive_configure_ack()
|
||||
self.uut.close()
|
||||
self.assertTrue(self.uut.lcp.is_finished.is_set())
|
||||
|
||||
def test_ping_failure_triggers_lcp_restart(self):
|
||||
self.ping_should_succeed = False
|
||||
self.uut.lcp.restart = mock.Mock()
|
||||
self.uut.on_link_up()
|
||||
self.assertIsNone(self.uut.get_link(timeout=0))
|
||||
self.uut.lcp.restart.assert_called_once_with()
|
||||
|
||||
|
||||
class TestInterfaceSocket(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.interface = mock.MagicMock()
|
||||
self.uut = link.InterfaceSocket(self.interface, 0xf2f1)
|
||||
|
||||
def test_socket_is_not_closed_when_constructed(self):
|
||||
self.assertFalse(self.uut.closed)
|
||||
|
||||
def test_send(self):
|
||||
self.uut.send(b'data')
|
||||
self.interface.send_packet.assert_called_once_with(0xf2f1, b'data')
|
||||
|
||||
def test_close_sets_socket_as_closed(self):
|
||||
self.uut.close()
|
||||
self.assertTrue(self.uut.closed)
|
||||
|
||||
def test_close_unregisters_socket_with_interface(self):
|
||||
self.uut.close()
|
||||
self.interface.unregister_socket.assert_called_once_with(0xf2f1)
|
||||
|
||||
def test_close_calls_on_close_handler(self):
|
||||
on_close = mock.Mock()
|
||||
self.uut.on_close = on_close
|
||||
self.uut.close()
|
||||
on_close.assert_called_once_with()
|
||||
|
||||
def test_send_after_close_is_an_error(self):
|
||||
self.uut.close()
|
||||
with self.assertRaises(exceptions.SocketClosed):
|
||||
self.uut.send(b'data')
|
||||
|
||||
def test_handle_packet(self):
|
||||
self.uut.on_packet = mock.Mock()
|
||||
self.uut.handle_packet(b'data')
|
||||
self.uut.on_packet.assert_called_once_with(b'data')
|
||||
|
||||
def test_handle_packet_does_not_call_on_packet_handler_after_close(self):
|
||||
on_packet = mock.Mock()
|
||||
self.uut.on_packet = on_packet
|
||||
self.uut.close()
|
||||
self.uut.handle_packet(b'data')
|
||||
on_packet.assert_not_called()
|
||||
|
||||
def test_context_manager(self):
|
||||
with self.uut as uut:
|
||||
self.assertIs(self.uut, uut)
|
||||
self.assertFalse(self.uut.closed)
|
||||
self.assertTrue(self.uut.closed)
|
||||
|
||||
def test_close_is_idempotent(self):
|
||||
on_close = mock.Mock()
|
||||
self.uut.on_close = on_close
|
||||
self.uut.close()
|
||||
self.uut.close()
|
||||
self.assertEqual(1, self.interface.unregister_socket.call_count)
|
||||
self.assertEqual(1, on_close.call_count)
|
||||
|
||||
|
||||
class TestLink(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
transports_patcher = mock.patch.dict(
|
||||
link.Link.TRANSPORTS, {'fake': mock.Mock()}, clear=True)
|
||||
transports_patcher.start()
|
||||
self.addCleanup(transports_patcher.stop)
|
||||
|
||||
self.uut = link.Link(mock.Mock(), 1500)
|
||||
|
||||
def test_open_socket(self):
|
||||
socket = self.uut.open_socket(
|
||||
transport='fake', port=0xabcd, timeout=1.0)
|
||||
self.uut.transports['fake'].open_socket.assert_called_once_with(
|
||||
0xabcd, 1.0)
|
||||
self.assertIs(socket, self.uut.transports['fake'].open_socket())
|
||||
|
||||
def test_down(self):
|
||||
self.uut.down()
|
||||
self.assertTrue(self.uut.closed)
|
||||
self.uut.transports['fake'].down.assert_called_once_with()
|
||||
|
||||
def test_on_close_callback_when_going_down(self):
|
||||
self.uut.on_close = mock.Mock()
|
||||
self.uut.down()
|
||||
self.uut.on_close.assert_called_once_with()
|
||||
|
||||
def test_open_socket_after_down_is_an_error(self):
|
||||
self.uut.down()
|
||||
with self.assertRaisesRegexp(ValueError, 'closed Link'):
|
||||
self.uut.open_socket('fake', 0xabcd)
|
||||
|
||||
def test_open_socket_with_bad_transport_name(self):
|
||||
with self.assertRaisesRegexp(KeyError, "Unknown transport 'bad'"):
|
||||
self.uut.open_socket('bad', 0xabcd)
|
168
python_libs/pulse2/tests/test_pcmp.py
Normal file
168
python_libs/pulse2/tests/test_pcmp.py
Normal file
|
@ -0,0 +1,168 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import unittest
|
||||
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
import mock
|
||||
|
||||
from pebble.pulse2 import pcmp
|
||||
from .fake_timer import FakeTimer
|
||||
|
||||
|
||||
class TestPCMP(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.uut = pcmp.PulseControlMessageProtocol(mock.Mock(), 1)
|
||||
|
||||
def test_close_unregisters_the_socket(self):
|
||||
self.uut.close()
|
||||
self.uut.transport.unregister_socket.assert_called_once_with(1)
|
||||
|
||||
def test_close_is_idempotent(self):
|
||||
self.uut.close()
|
||||
self.uut.close()
|
||||
self.assertEqual(1, self.uut.transport.unregister_socket.call_count)
|
||||
|
||||
def test_send_unknown_code(self):
|
||||
self.uut.send_unknown_code(42)
|
||||
self.uut.transport.send.assert_called_once_with(1, b'\x82\x2a')
|
||||
|
||||
def test_send_echo_request(self):
|
||||
self.uut.send_echo_request(b'abcdefg')
|
||||
self.uut.transport.send.assert_called_once_with(1, b'\x01abcdefg')
|
||||
|
||||
def test_send_echo_reply(self):
|
||||
self.uut.send_echo_reply(b'abcdefg')
|
||||
self.uut.transport.send.assert_called_once_with(1, b'\x02abcdefg')
|
||||
|
||||
def test_on_receive_empty_packet(self):
|
||||
self.uut.on_receive(b'')
|
||||
self.uut.transport.send.assert_not_called()
|
||||
|
||||
def test_on_receive_message_with_unknown_code(self):
|
||||
self.uut.on_receive(b'\x00')
|
||||
self.uut.transport.send.assert_called_once_with(1, b'\x82\x00')
|
||||
|
||||
def test_on_receive_malformed_unknown_code_message_1(self):
|
||||
self.uut.on_receive(b'\x82')
|
||||
self.uut.transport.send.assert_not_called()
|
||||
|
||||
def test_on_receive_malformed_unknown_code_message_2(self):
|
||||
self.uut.on_receive(b'\x82\x00\x01')
|
||||
self.uut.transport.send.assert_not_called()
|
||||
|
||||
def test_on_receive_discard_request(self):
|
||||
self.uut.on_receive(b'\x03')
|
||||
self.uut.transport.send.assert_not_called()
|
||||
|
||||
def test_on_receive_discard_request_with_data(self):
|
||||
self.uut.on_receive(b'\x03asdfasdfasdf')
|
||||
self.uut.transport.send.assert_not_called()
|
||||
|
||||
def test_on_receive_echo_request(self):
|
||||
self.uut.on_receive(b'\x01')
|
||||
self.uut.transport.send.assert_called_once_with(1, b'\x02')
|
||||
|
||||
def test_on_receive_echo_request_with_data(self):
|
||||
self.uut.on_receive(b'\x01a')
|
||||
self.uut.transport.send.assert_called_once_with(1, b'\x02a')
|
||||
|
||||
def test_on_receive_echo_reply(self):
|
||||
self.uut.on_receive(b'\x02')
|
||||
self.uut.transport.send.assert_not_called()
|
||||
|
||||
def test_on_receive_echo_reply_with_data(self):
|
||||
self.uut.on_receive(b'\x02abc')
|
||||
self.uut.transport.send.assert_not_called()
|
||||
|
||||
def test_on_receive_port_closed_with_no_handler(self):
|
||||
self.uut.on_receive(b'\x81\xab\xcd')
|
||||
self.uut.transport.send.assert_not_called()
|
||||
|
||||
def test_on_receive_port_closed(self):
|
||||
self.uut.on_port_closed = mock.Mock()
|
||||
self.uut.on_receive(b'\x81\xab\xcd')
|
||||
self.uut.on_port_closed.assert_called_once_with(0xabcd)
|
||||
|
||||
def test_on_receive_malformed_port_closed_message_1(self):
|
||||
self.uut.on_port_closed = mock.Mock()
|
||||
self.uut.on_receive(b'\x81\xab')
|
||||
self.uut.on_port_closed.assert_not_called()
|
||||
|
||||
def test_on_receive_malformed_port_closed_message_2(self):
|
||||
self.uut.on_port_closed = mock.Mock()
|
||||
self.uut.on_receive(b'\x81\xab\xcd\xef')
|
||||
self.uut.on_port_closed.assert_not_called()
|
||||
|
||||
|
||||
class TestPing(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
FakeTimer.clear_timer_list()
|
||||
timer_patcher = mock.patch('threading.Timer', new=FakeTimer)
|
||||
timer_patcher.start()
|
||||
self.addCleanup(timer_patcher.stop)
|
||||
self.uut = pcmp.PulseControlMessageProtocol(mock.Mock(), 1)
|
||||
|
||||
def test_successful_ping(self):
|
||||
cb = mock.Mock()
|
||||
self.uut.ping(cb)
|
||||
self.uut.on_receive(b'\x02')
|
||||
cb.assert_called_once_with(True)
|
||||
self.assertFalse(FakeTimer.get_active_timers())
|
||||
|
||||
def test_ping_succeeds_after_retry(self):
|
||||
cb = mock.Mock()
|
||||
self.uut.ping(cb, attempts=2)
|
||||
FakeTimer.TIMERS[-1].expire()
|
||||
self.uut.on_receive(b'\x02')
|
||||
cb.assert_called_once_with(True)
|
||||
self.assertFalse(FakeTimer.get_active_timers())
|
||||
|
||||
def test_ping_succeeds_after_multiple_retries(self):
|
||||
cb = mock.Mock()
|
||||
self.uut.ping(cb, attempts=3)
|
||||
timer1 = FakeTimer.TIMERS[-1]
|
||||
timer1.expire()
|
||||
timer2 = FakeTimer.TIMERS[-1]
|
||||
self.assertIsNot(timer1, timer2)
|
||||
timer2.expire()
|
||||
self.uut.on_receive(b'\x02')
|
||||
cb.assert_called_once_with(True)
|
||||
self.assertFalse(FakeTimer.get_active_timers())
|
||||
|
||||
def test_failed_ping(self):
|
||||
cb = mock.Mock()
|
||||
self.uut.ping(cb, attempts=1)
|
||||
FakeTimer.TIMERS[-1].expire()
|
||||
cb.assert_called_once_with(False)
|
||||
self.assertFalse(FakeTimer.get_active_timers())
|
||||
|
||||
def test_ping_fails_after_multiple_retries(self):
|
||||
cb = mock.Mock()
|
||||
self.uut.ping(cb, attempts=3)
|
||||
for _ in range(3):
|
||||
FakeTimer.TIMERS[-1].expire()
|
||||
cb.assert_called_once_with(False)
|
||||
self.assertFalse(FakeTimer.get_active_timers())
|
||||
|
||||
def test_socket_close_aborts_ping(self):
|
||||
cb = mock.Mock()
|
||||
self.uut.ping(cb, attempts=3)
|
||||
self.uut.close()
|
||||
cb.assert_not_called()
|
||||
self.assertFalse(FakeTimer.get_active_timers())
|
589
python_libs/pulse2/tests/test_ppp.py
Normal file
589
python_libs/pulse2/tests/test_ppp.py
Normal file
|
@ -0,0 +1,589 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import unittest
|
||||
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
import mock
|
||||
|
||||
import construct
|
||||
|
||||
from pebble.pulse2 import ppp, exceptions
|
||||
|
||||
from .fake_timer import FakeTimer
|
||||
from . import timer_helper
|
||||
|
||||
|
||||
class TestPPPEncapsulation(unittest.TestCase):
|
||||
|
||||
def test_ppp_encapsulate(self):
|
||||
self.assertEqual(ppp.encapsulate(0xc021, b'Information'),
|
||||
b'\xc0\x21Information')
|
||||
|
||||
|
||||
class TestPPPUnencapsulate(unittest.TestCase):
|
||||
|
||||
def test_ppp_unencapsulate(self):
|
||||
protocol, information = ppp.unencapsulate(b'\xc0\x21Information')
|
||||
self.assertEqual((protocol, information), (0xc021, b'Information'))
|
||||
|
||||
def test_unencapsulate_empty_frame(self):
|
||||
with self.assertRaises(ppp.UnencapsulationError):
|
||||
ppp.unencapsulate(b'')
|
||||
|
||||
def test_unencapsulate_too_short_frame(self):
|
||||
with self.assertRaises(ppp.UnencapsulationError):
|
||||
ppp.unencapsulate(b'\x21')
|
||||
|
||||
def test_unencapsulate_empty_information(self):
|
||||
protocol, information = ppp.unencapsulate(b'\xc0\x21')
|
||||
self.assertEqual((protocol, information), (0xc021, b''))
|
||||
|
||||
|
||||
class TestConfigurationOptionsParser(unittest.TestCase):
|
||||
|
||||
def test_no_options(self):
|
||||
options = ppp.OptionList.parse(b'')
|
||||
self.assertEqual(len(options), 0)
|
||||
|
||||
def test_one_empty_option(self):
|
||||
options = ppp.OptionList.parse(b'\xaa\x02')
|
||||
self.assertEqual(len(options), 1)
|
||||
self.assertEqual(options[0].type, 0xaa)
|
||||
self.assertEqual(options[0].data, b'')
|
||||
|
||||
def test_one_option_with_length(self):
|
||||
options = ppp.OptionList.parse(b'\xab\x07Data!')
|
||||
self.assertEqual((0xab, b'Data!'), options[0])
|
||||
|
||||
def test_multiple_options_empty_first(self):
|
||||
options = ppp.OptionList.parse(b'\x22\x02\x23\x03a\x21\x04ab')
|
||||
self.assertEqual([(0x22, b''), (0x23, b'a'), (0x21, b'ab')], options)
|
||||
|
||||
def test_multiple_options_dataful_first(self):
|
||||
options = ppp.OptionList.parse(b'\x31\x08option\x32\x02')
|
||||
self.assertEqual([(0x31, b'option'), (0x32, b'')], options)
|
||||
|
||||
def test_option_with_length_too_short(self):
|
||||
with self.assertRaises(ppp.ParseError):
|
||||
ppp.OptionList.parse(b'\x41\x01')
|
||||
|
||||
def test_option_list_with_malformed_option(self):
|
||||
with self.assertRaises(ppp.ParseError):
|
||||
ppp.OptionList.parse(b'\x0a\x02\x0b\x01\x0c\x03a')
|
||||
|
||||
def test_truncated_terminal_option(self):
|
||||
with self.assertRaises(ppp.ParseError):
|
||||
ppp.OptionList.parse(b'\x61\x02\x62\x03a\x63\x0ccandleja')
|
||||
|
||||
|
||||
class TestConfigurationOptionsBuilder(unittest.TestCase):
|
||||
|
||||
def test_no_options(self):
|
||||
serialized = ppp.OptionList.build([])
|
||||
self.assertEqual(b'', serialized)
|
||||
|
||||
def test_one_empty_option(self):
|
||||
serialized = ppp.OptionList.build([ppp.Option(0xaa, b'')])
|
||||
self.assertEqual(b'\xaa\x02', serialized)
|
||||
|
||||
def test_one_option_with_length(self):
|
||||
serialized = ppp.OptionList.build([ppp.Option(0xbb, b'Data!')])
|
||||
self.assertEqual(b'\xbb\x07Data!', serialized)
|
||||
|
||||
def test_two_options(self):
|
||||
serialized = ppp.OptionList.build([
|
||||
ppp.Option(0xcc, b'foo'), ppp.Option(0xdd, b'xyzzy')])
|
||||
self.assertEqual(b'\xcc\x05foo\xdd\x07xyzzy', serialized)
|
||||
|
||||
|
||||
class TestLCPEnvelopeParsing(unittest.TestCase):
|
||||
|
||||
def test_packet_no_padding(self):
|
||||
parsed = ppp.LCPEncapsulation.parse(b'\x01\xab\x00\x0aabcdef')
|
||||
self.assertEqual(parsed.code, 1)
|
||||
self.assertEqual(parsed.identifier, 0xab)
|
||||
self.assertEqual(parsed.data, b'abcdef')
|
||||
self.assertEqual(parsed.padding, b'')
|
||||
|
||||
def test_padding(self):
|
||||
parsed = ppp.LCPEncapsulation.parse(b'\x01\xab\x00\x0aabcdefpadding')
|
||||
self.assertEqual(parsed.data, b'abcdef')
|
||||
self.assertEqual(parsed.padding, b'padding')
|
||||
|
||||
def test_truncated_packet(self):
|
||||
with self.assertRaises(ppp.ParseError):
|
||||
ppp.LCPEncapsulation.parse(b'\x01\xab\x00\x0aabcde')
|
||||
|
||||
def test_bogus_length(self):
|
||||
with self.assertRaises(ppp.ParseError):
|
||||
ppp.LCPEncapsulation.parse(b'\x01\xbc\x00\x03')
|
||||
|
||||
def test_empty_data(self):
|
||||
parsed = ppp.LCPEncapsulation.parse(b'\x03\x01\x00\x04')
|
||||
self.assertEqual((3, 1, b'', b''), parsed)
|
||||
|
||||
|
||||
class TestLCPEnvelopeBuilder(unittest.TestCase):
|
||||
|
||||
def test_build_empty_data(self):
|
||||
serialized = ppp.LCPEncapsulation.build(1, 0xfe, b'')
|
||||
self.assertEqual(b'\x01\xfe\x00\x04', serialized)
|
||||
|
||||
def test_build_with_data(self):
|
||||
serialized = ppp.LCPEncapsulation.build(3, 0x2a, b'Hello, world!')
|
||||
self.assertEqual(b'\x03\x2a\x00\x11Hello, world!', serialized)
|
||||
|
||||
|
||||
class TestProtocolRejectParsing(unittest.TestCase):
|
||||
|
||||
def test_protocol_and_info(self):
|
||||
self.assertEqual((0xabcd, b'asdfasdf'),
|
||||
ppp.ProtocolReject.parse(b'\xab\xcdasdfasdf'))
|
||||
|
||||
def test_empty_info(self):
|
||||
self.assertEqual((0xf00d, b''),
|
||||
ppp.ProtocolReject.parse(b'\xf0\x0d'))
|
||||
|
||||
def test_truncated_packet(self):
|
||||
with self.assertRaises(ppp.ParseError):
|
||||
ppp.ProtocolReject.parse(b'\xab')
|
||||
|
||||
|
||||
class TestMagicNumberAndDataParsing(unittest.TestCase):
|
||||
|
||||
def test_magic_and_data(self):
|
||||
self.assertEqual(
|
||||
(0xabcdef01, b'datadata'),
|
||||
ppp.MagicNumberAndData.parse(b'\xab\xcd\xef\x01datadata'))
|
||||
|
||||
def test_magic_no_data(self):
|
||||
self.assertEqual(
|
||||
(0xfeedface, b''),
|
||||
ppp.MagicNumberAndData.parse(b'\xfe\xed\xfa\xce'))
|
||||
|
||||
def test_truncated_packet(self):
|
||||
with self.assertRaises(ppp.ParseError):
|
||||
ppp.MagicNumberAndData.parse(b'abc')
|
||||
|
||||
|
||||
class TestMagicNumberAndDataBuilder(unittest.TestCase):
|
||||
|
||||
def test_build_empty_data(self):
|
||||
serialized = ppp.MagicNumberAndData.build(0x12345678, b'')
|
||||
self.assertEqual(b'\x12\x34\x56\x78', serialized)
|
||||
|
||||
def test_build_with_data(self):
|
||||
serialized = ppp.MagicNumberAndData.build(0xabcdef01, b'foobar')
|
||||
self.assertEqual(b'\xab\xcd\xef\x01foobar', serialized)
|
||||
|
||||
def test_build_with_named_attributes(self):
|
||||
serialized = ppp.MagicNumberAndData.build(magic_number=0, data=b'abc')
|
||||
self.assertEqual(b'\0\0\0\0abc', serialized)
|
||||
|
||||
|
||||
class TestControlProtocolRestartTimer(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
FakeTimer.clear_timer_list()
|
||||
timer_patcher = mock.patch('threading.Timer', new=FakeTimer)
|
||||
timer_patcher.start()
|
||||
self.addCleanup(timer_patcher.stop)
|
||||
|
||||
self.uut = ppp.ControlProtocol()
|
||||
self.uut.timeout_retry = mock.Mock()
|
||||
self.uut.timeout_giveup = mock.Mock()
|
||||
self.uut.restart_count = 5
|
||||
|
||||
def test_timeout_event_called_if_generation_ids_match(self):
|
||||
self.uut.restart_timer_expired(self.uut.restart_timer_generation_id)
|
||||
self.uut.timeout_retry.assert_called_once_with()
|
||||
|
||||
def test_timeout_event_not_called_if_generation_ids_mismatch(self):
|
||||
self.uut.restart_timer_expired(42)
|
||||
self.uut.timeout_retry.assert_not_called()
|
||||
self.uut.timeout_giveup.assert_not_called()
|
||||
|
||||
def test_timeout_event_not_called_after_stopped(self):
|
||||
self.uut.start_restart_timer(1)
|
||||
self.uut.stop_restart_timer()
|
||||
FakeTimer.TIMERS[-1].expire()
|
||||
self.uut.timeout_retry.assert_not_called()
|
||||
self.uut.timeout_giveup.assert_not_called()
|
||||
|
||||
def test_timeout_event_not_called_from_old_timer_after_restart(self):
|
||||
self.uut.start_restart_timer(1)
|
||||
zombie_timer = FakeTimer.get_active_timers()[-1]
|
||||
self.uut.start_restart_timer(1)
|
||||
zombie_timer.expire()
|
||||
self.uut.timeout_retry.assert_not_called()
|
||||
self.uut.timeout_giveup.assert_not_called()
|
||||
|
||||
def test_timeout_event_called_only_once_after_restart(self):
|
||||
self.uut.start_restart_timer(1)
|
||||
self.uut.start_restart_timer(1)
|
||||
for timer in FakeTimer.TIMERS:
|
||||
timer.expire()
|
||||
self.uut.timeout_retry.assert_called_once_with()
|
||||
self.uut.timeout_giveup.assert_not_called()
|
||||
|
||||
|
||||
class InstrumentedControlProtocol(ppp.ControlProtocol):
|
||||
|
||||
methods_to_mock = (
|
||||
'this_layer_up this_layer_down this_layer_started '
|
||||
'this_layer_finished send_packet start_restart_timer '
|
||||
'stop_restart_timer').split()
|
||||
attributes_to_mock = ('restart_timer',)
|
||||
|
||||
def __init__(self):
|
||||
ppp.ControlProtocol.__init__(self)
|
||||
for method in self.methods_to_mock:
|
||||
setattr(self, method, mock.Mock())
|
||||
for attr in self.attributes_to_mock:
|
||||
setattr(self, attr, mock.NonCallableMock())
|
||||
|
||||
|
||||
class ControlProtocolTestMixin(object):
|
||||
|
||||
CONTROL_CODE_ENUM = ppp.ControlCode
|
||||
|
||||
def _map_control_code(self, code):
|
||||
try:
|
||||
return int(code)
|
||||
except ValueError:
|
||||
return self.CONTROL_CODE_ENUM[code].value
|
||||
|
||||
def assert_packet_sent(self, code, identifier, body=b''):
|
||||
self.fsm.send_packet.assert_called_once_with(
|
||||
ppp.LCPEncapsulation.build(
|
||||
self._map_control_code(code), identifier, body))
|
||||
self.fsm.send_packet.reset_mock()
|
||||
|
||||
def incoming_packet(self, code, identifier, body=b''):
|
||||
self.fsm.packet_received(
|
||||
ppp.LCPEncapsulation.build(self._map_control_code(code),
|
||||
identifier, body))
|
||||
|
||||
|
||||
class TestControlProtocolFSM(ControlProtocolTestMixin, unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.addCleanup(timer_helper.cancel_all_timers)
|
||||
self.fsm = InstrumentedControlProtocol()
|
||||
|
||||
def test_open_down(self):
|
||||
self.fsm.open()
|
||||
self.fsm.this_layer_started.assert_called_once_with()
|
||||
self.fsm.this_layer_up.assert_not_called()
|
||||
self.fsm.this_layer_down.assert_not_called()
|
||||
self.fsm.this_layer_finished.assert_not_called()
|
||||
|
||||
def test_closed_up(self):
|
||||
self.fsm.up(mock.Mock())
|
||||
self.fsm.this_layer_up.assert_not_called()
|
||||
self.fsm.this_layer_down.assert_not_called()
|
||||
self.fsm.this_layer_started.assert_not_called()
|
||||
self.fsm.this_layer_finished.assert_not_called()
|
||||
|
||||
def test_trivial_handshake(self):
|
||||
self.fsm.open()
|
||||
self.fsm.up(mock.Mock())
|
||||
self.assert_packet_sent('Configure_Request', 0)
|
||||
self.incoming_packet('Configure_Ack', 0)
|
||||
self.incoming_packet('Configure_Request', 17)
|
||||
self.assert_packet_sent('Configure_Ack', 17)
|
||||
self.assertEqual('Opened', self.fsm.state)
|
||||
self.assertTrue(self.fsm.this_layer_up.called)
|
||||
self.assertEqual(self.fsm.restart_count, self.fsm.max_configure)
|
||||
|
||||
def test_terminate_cleanly(self):
|
||||
self.test_trivial_handshake()
|
||||
self.fsm.close()
|
||||
self.fsm.this_layer_down.assert_called_once_with()
|
||||
self.assert_packet_sent('Terminate_Request', 42)
|
||||
|
||||
def test_remote_terminate(self):
|
||||
self.test_trivial_handshake()
|
||||
self.incoming_packet('Terminate_Request', 42)
|
||||
self.assert_packet_sent('Terminate_Ack', 42)
|
||||
self.assertTrue(self.fsm.this_layer_down.called)
|
||||
self.assertTrue(self.fsm.start_restart_timer.called)
|
||||
self.fsm.this_layer_finished.assert_not_called()
|
||||
self.fsm.restart_timer_expired(self.fsm.restart_timer_generation_id)
|
||||
self.assertTrue(self.fsm.this_layer_finished.called)
|
||||
self.assertEqual('Stopped', self.fsm.state)
|
||||
|
||||
def test_remote_rejects_configure_request_code(self):
|
||||
self.fsm.open()
|
||||
self.fsm.up(mock.Mock())
|
||||
received_packet = self.fsm.send_packet.call_args[0][0]
|
||||
self.assert_packet_sent('Configure_Request', 0)
|
||||
self.incoming_packet('Code_Reject', 3, received_packet)
|
||||
self.assertEqual('Stopped', self.fsm.state)
|
||||
self.assertTrue(self.fsm.this_layer_finished.called)
|
||||
|
||||
def test_receive_extended_code(self):
|
||||
self.fsm.handle_unknown_code = mock.Mock()
|
||||
self.test_trivial_handshake()
|
||||
self.incoming_packet(42, 11, b'Life, the universe and everything')
|
||||
self.fsm.handle_unknown_code.assert_called_once_with(
|
||||
42, 11, b'Life, the universe and everything')
|
||||
|
||||
def test_receive_unimplemented_code(self):
|
||||
self.test_trivial_handshake()
|
||||
self.incoming_packet(0x55, 0)
|
||||
self.assert_packet_sent('Code_Reject', 0, b'\x55\0\0\x04')
|
||||
|
||||
def test_code_reject_truncates_rejected_packet(self):
|
||||
self.test_trivial_handshake()
|
||||
self.incoming_packet(0xaa, 0x20, 'a'*1496) # 1500-byte Info
|
||||
self.assert_packet_sent('Code_Reject', 0,
|
||||
b'\xaa\x20\x05\xdc' + b'a'*1492)
|
||||
|
||||
def test_code_reject_identifier_changes(self):
|
||||
self.test_trivial_handshake()
|
||||
self.incoming_packet(0xaa, 0)
|
||||
self.assert_packet_sent('Code_Reject', 0, b'\xaa\0\0\x04')
|
||||
self.incoming_packet(0xaa, 0)
|
||||
self.assert_packet_sent('Code_Reject', 1, b'\xaa\0\0\x04')
|
||||
|
||||
|
||||
# Local events: up, down, open, close
|
||||
# Option negotiation: reject, nak
|
||||
# Exceptional situations: catastrophic code-reject
|
||||
# Restart negotiation after opening
|
||||
# Remote Terminate-Req, -Ack at various points in the lifecycle
|
||||
# Negotiation infinite loop
|
||||
# Local side gives up on negotiation
|
||||
# Corrupt packets received
|
||||
|
||||
|
||||
class TestLCPReceiveEchoRequest(ControlProtocolTestMixin, unittest.TestCase):
|
||||
|
||||
CONTROL_CODE_ENUM = ppp.LCPCode
|
||||
|
||||
def setUp(self):
|
||||
self.addCleanup(timer_helper.cancel_all_timers)
|
||||
self.fsm = ppp.LinkControlProtocol(mock.Mock())
|
||||
self.fsm.send_packet = mock.Mock()
|
||||
self.fsm.state = 'Opened'
|
||||
|
||||
def send_echo_request(self, identifier=0, data=b'\0\0\0\0'):
|
||||
result = self.fsm.handle_unknown_code(
|
||||
ppp.LCPCode.Echo_Request.value, identifier, data)
|
||||
self.assertIsNot(result, NotImplemented)
|
||||
|
||||
def test_echo_request_is_dropped_when_not_in_opened_state(self):
|
||||
self.fsm.state = 'Ack-Sent'
|
||||
self.send_echo_request()
|
||||
self.fsm.send_packet.assert_not_called()
|
||||
|
||||
def test_echo_request_elicits_reply(self):
|
||||
self.send_echo_request()
|
||||
self.assert_packet_sent('Echo_Reply', 0, b'\0\0\0\0')
|
||||
|
||||
def test_echo_request_with_data_is_echoed_in_reply(self):
|
||||
self.send_echo_request(5, b'\0\0\0\0datadata')
|
||||
self.assert_packet_sent('Echo_Reply', 5, b'\0\0\0\0datadata')
|
||||
|
||||
def test_echo_request_missing_magic_number_field_is_dropped(self):
|
||||
self.send_echo_request(data=b'')
|
||||
self.fsm.send_packet.assert_not_called()
|
||||
|
||||
def test_echo_request_with_nonzero_magic_number_is_dropped(self):
|
||||
self.send_echo_request(data=b'\0\0\0\x01')
|
||||
self.fsm.send_packet.assert_not_called()
|
||||
|
||||
|
||||
class TestLCPPing(ControlProtocolTestMixin, unittest.TestCase):
|
||||
|
||||
CONTROL_CODE_ENUM = ppp.LCPCode
|
||||
|
||||
def setUp(self):
|
||||
FakeTimer.clear_timer_list()
|
||||
timer_patcher = mock.patch('threading.Timer', new=FakeTimer)
|
||||
timer_patcher.start()
|
||||
self.addCleanup(timer_patcher.stop)
|
||||
|
||||
self.fsm = ppp.LinkControlProtocol(mock.Mock())
|
||||
self.fsm.send_packet = mock.Mock()
|
||||
self.fsm.state = 'Opened'
|
||||
|
||||
def respond_to_ping(self):
|
||||
[echo_request_packet], _ = self.fsm.send_packet.call_args
|
||||
self.assertEqual(b'\x09'[0], echo_request_packet[0])
|
||||
echo_response_packet = b'\x0a' + echo_request_packet[1:]
|
||||
self.fsm.packet_received(echo_response_packet)
|
||||
|
||||
def test_ping_when_lcp_is_not_opened_is_an_error(self):
|
||||
cb = mock.Mock()
|
||||
self.fsm.state = 'Ack-Rcvd'
|
||||
with self.assertRaises(ppp.LinkStateError):
|
||||
self.fsm.ping(cb)
|
||||
cb.assert_not_called()
|
||||
|
||||
def test_zero_attempts_is_an_error(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.fsm.ping(mock.Mock(), attempts=0)
|
||||
|
||||
def test_negative_attempts_is_an_error(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.fsm.ping(mock.Mock(), attempts=-1)
|
||||
|
||||
def test_zero_timeout_is_an_error(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.fsm.ping(mock.Mock(), timeout=0)
|
||||
|
||||
def test_negative_timeout_is_an_error(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.fsm.ping(mock.Mock(), timeout=-0.1)
|
||||
|
||||
def test_straightforward_ping(self):
|
||||
cb = mock.Mock()
|
||||
self.fsm.ping(cb)
|
||||
cb.assert_not_called()
|
||||
self.assertEqual(1, self.fsm.send_packet.call_count)
|
||||
self.respond_to_ping()
|
||||
cb.assert_called_once_with(True)
|
||||
|
||||
def test_one_timeout_before_responding(self):
|
||||
cb = mock.Mock()
|
||||
self.fsm.ping(cb, attempts=2)
|
||||
FakeTimer.TIMERS[-1].expire()
|
||||
cb.assert_not_called()
|
||||
self.assertEqual(2, self.fsm.send_packet.call_count)
|
||||
self.respond_to_ping()
|
||||
cb.assert_called_once_with(True)
|
||||
|
||||
def test_one_attempt_with_no_reply(self):
|
||||
cb = mock.Mock()
|
||||
self.fsm.ping(cb, attempts=1)
|
||||
FakeTimer.TIMERS[-1].expire()
|
||||
self.assertEqual(1, self.fsm.send_packet.call_count)
|
||||
cb.assert_called_once_with(False)
|
||||
|
||||
def test_multiple_attempts_with_no_reply(self):
|
||||
cb = mock.Mock()
|
||||
self.fsm.ping(cb, attempts=2)
|
||||
timer_one = FakeTimer.TIMERS[-1]
|
||||
timer_one.expire()
|
||||
timer_two = FakeTimer.TIMERS[-1]
|
||||
self.assertIsNot(timer_one, timer_two)
|
||||
timer_two.expire()
|
||||
self.assertEqual(2, self.fsm.send_packet.call_count)
|
||||
cb.assert_called_once_with(False)
|
||||
|
||||
def test_late_reply(self):
|
||||
cb = mock.Mock()
|
||||
self.fsm.ping(cb, attempts=1)
|
||||
FakeTimer.TIMERS[-1].expire()
|
||||
self.respond_to_ping()
|
||||
cb.assert_called_once_with(False)
|
||||
|
||||
def test_this_layer_down_during_ping(self):
|
||||
cb = mock.Mock()
|
||||
self.fsm.ping(cb)
|
||||
self.fsm.this_layer_down()
|
||||
FakeTimer.TIMERS[-1].expire()
|
||||
cb.assert_not_called()
|
||||
|
||||
def test_echo_reply_with_wrong_identifier(self):
|
||||
cb = mock.Mock()
|
||||
self.fsm.ping(cb, attempts=1)
|
||||
[echo_request_packet], _ = self.fsm.send_packet.call_args
|
||||
echo_response_packet = bytearray(echo_request_packet)
|
||||
echo_response_packet[0] = 0x0a
|
||||
echo_response_packet[1] += 1
|
||||
self.fsm.packet_received(bytes(echo_response_packet))
|
||||
cb.assert_not_called()
|
||||
FakeTimer.TIMERS[-1].expire()
|
||||
cb.assert_called_once_with(False)
|
||||
|
||||
def test_echo_reply_with_wrong_data(self):
|
||||
cb = mock.Mock()
|
||||
self.fsm.ping(cb, attempts=1)
|
||||
[echo_request_packet], _ = self.fsm.send_packet.call_args
|
||||
# Generate a syntactically valid Echo-Reply with the right
|
||||
# identifier but completely different data.
|
||||
identifier = bytearray(echo_request_packet)[1]
|
||||
echo_response_packet = bytes(
|
||||
b'\x0a' + bytearray([identifier]) +
|
||||
b'\0\x26\0\0\0\0bad reply bad reply bad reply.')
|
||||
self.fsm.packet_received(bytes(echo_response_packet))
|
||||
cb.assert_not_called()
|
||||
FakeTimer.TIMERS[-1].expire()
|
||||
cb.assert_called_once_with(False)
|
||||
|
||||
def test_successive_pings_use_different_identifiers(self):
|
||||
self.fsm.ping(mock.Mock(), attempts=1)
|
||||
[echo_request_packet_1], _ = self.fsm.send_packet.call_args
|
||||
identifier_1 = bytearray(echo_request_packet_1)[1]
|
||||
self.respond_to_ping()
|
||||
self.fsm.ping(mock.Mock(), attempts=1)
|
||||
[echo_request_packet_2], _ = self.fsm.send_packet.call_args
|
||||
identifier_2 = bytearray(echo_request_packet_2)[1]
|
||||
self.assertNotEqual(identifier_1, identifier_2)
|
||||
|
||||
def test_unsolicited_echo_reply_doesnt_break_anything(self):
|
||||
self.fsm.packet_received(b'\x0a\0\0\x08\0\0\0\0')
|
||||
|
||||
def test_malformed_echo_reply(self):
|
||||
cb = mock.Mock()
|
||||
self.fsm.ping(cb, attempts=1)
|
||||
# Only three bytes of Magic-Number
|
||||
self.fsm.packet_received(b'\x0a\0\0\x07\0\0\0')
|
||||
cb.assert_not_called()
|
||||
|
||||
# Trying to start a second ping while the first ping is still happening
|
||||
def test_starting_a_ping_while_another_is_active_is_an_error(self):
|
||||
cb = mock.Mock()
|
||||
self.fsm.ping(cb, attempts=1)
|
||||
cb2 = mock.Mock()
|
||||
with self.assertRaises(exceptions.AlreadyInProgressError):
|
||||
self.fsm.ping(cb2, attempts=1)
|
||||
FakeTimer.TIMERS[-1].expire()
|
||||
cb.assert_called_once_with(False)
|
||||
cb2.assert_not_called()
|
||||
|
||||
|
||||
# General tests:
|
||||
# - Length too short for a valid packet
|
||||
# - Packet truncated (length field > packet len)
|
||||
# - Packet with padding
|
||||
|
||||
# OptionList codes:
|
||||
# 1 Configure-Request
|
||||
# 2 Configure-Ack
|
||||
# 3 Configure-Nak
|
||||
# 4 Configure-Reject
|
||||
|
||||
# Raw data codes:
|
||||
# 5 Terminate-Request
|
||||
# 6 Terminate-Ack
|
||||
# 7 Code-Reject
|
||||
|
||||
# 8 Protocol-Reject
|
||||
# - Empty Rejected-Information field
|
||||
# - Rejected-Protocol field too short
|
||||
|
||||
|
||||
# Magic number + data codes:
|
||||
# 10 Echo-Reply
|
||||
# 11 Discard-Request
|
||||
# 12 Identification (RFC 1570)
|
538
python_libs/pulse2/tests/test_transports.py
Normal file
538
python_libs/pulse2/tests/test_transports.py
Normal file
|
@ -0,0 +1,538 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import threading
|
||||
import unittest
|
||||
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
import mock
|
||||
|
||||
import construct
|
||||
|
||||
from pebble.pulse2 import exceptions, pcmp, transports
|
||||
|
||||
from .fake_timer import FakeTimer
|
||||
from . import timer_helper
|
||||
|
||||
|
||||
# Save a reference to the real threading.Timer for tests which need to
|
||||
# use timers even while threading.Timer is patched with FakeTimer.
|
||||
RealThreadingTimer = threading.Timer
|
||||
|
||||
|
||||
class CommonTransportBeforeOpenedTestCases(object):
|
||||
|
||||
def test_send_raises_exception(self):
|
||||
with self.assertRaises(exceptions.TransportNotReady):
|
||||
self.uut.send(0xdead, b'not gonna get through')
|
||||
|
||||
def test_open_socket_returns_None_when_ncp_fails_to_open(self):
|
||||
self.assertIsNone(self.uut.open_socket(0xbeef, timeout=0))
|
||||
|
||||
|
||||
class CommonTransportTestCases(object):
|
||||
|
||||
def test_send_raises_exception_after_transport_is_closed(self):
|
||||
self.uut.down()
|
||||
with self.assertRaises(exceptions.TransportNotReady):
|
||||
self.uut.send(0xaaaa, b'asdf')
|
||||
|
||||
def test_socket_is_closed_when_transport_is_closed(self):
|
||||
socket = self.uut.open_socket(0xabcd, timeout=0)
|
||||
self.uut.down()
|
||||
self.assertTrue(socket.closed)
|
||||
with self.assertRaises(exceptions.SocketClosed):
|
||||
socket.send(b'foo')
|
||||
|
||||
def test_opening_two_sockets_on_same_port_is_an_error(self):
|
||||
socket1 = self.uut.open_socket(0xabcd, timeout=0)
|
||||
with self.assertRaises(KeyError):
|
||||
socket2 = self.uut.open_socket(0xabcd, timeout=0)
|
||||
|
||||
def test_closing_a_socket_allows_another_to_be_opened(self):
|
||||
socket1 = self.uut.open_socket(0xabcd, timeout=0)
|
||||
socket1.close()
|
||||
socket2 = self.uut.open_socket(0xabcd, timeout=0)
|
||||
|
||||
def test_opening_socket_fails_after_transport_down(self):
|
||||
self.uut.this_layer_down()
|
||||
self.assertIsNone(self.uut.open_socket(0xabcd, timeout=0))
|
||||
|
||||
def test_opening_socket_succeeds_after_transport_bounces(self):
|
||||
self.uut.this_layer_down()
|
||||
self.uut.this_layer_up()
|
||||
self.uut.open_socket(0xabcd, timeout=0)
|
||||
|
||||
|
||||
class TestBestEffortTransportBeforeOpened(CommonTransportBeforeOpenedTestCases,
|
||||
unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
control_protocol_patcher = mock.patch(
|
||||
'pebble.pulse2.transports.TransportControlProtocol')
|
||||
control_protocol_patcher.start()
|
||||
self.addCleanup(control_protocol_patcher.stop)
|
||||
self.uut = transports.BestEffortApplicationTransport(
|
||||
interface=mock.MagicMock(), link_mtu=1500)
|
||||
self.uut.ncp.is_Opened.return_value = False
|
||||
|
||||
def test_open_socket_waits_for_ncp_to_open(self):
|
||||
self.uut.ncp.is_Opened.return_value = True
|
||||
def on_ping(cb, *args):
|
||||
self.uut.packet_received(transports.BestEffortPacket.build(
|
||||
construct.Container(port=0x0001, length=5,
|
||||
information=b'\x02', padding=b'')))
|
||||
cb(True)
|
||||
with mock.patch.object(pcmp.PulseControlMessageProtocol, 'ping') \
|
||||
as mock_ping:
|
||||
mock_ping.side_effect = on_ping
|
||||
open_thread = RealThreadingTimer(0.01, self.uut.this_layer_up)
|
||||
open_thread.daemon = True
|
||||
open_thread.start()
|
||||
self.assertIsNotNone(self.uut.open_socket(0xbeef, timeout=0.5))
|
||||
open_thread.join()
|
||||
|
||||
|
||||
class TestBestEffortTransport(CommonTransportTestCases, unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.addCleanup(timer_helper.cancel_all_timers)
|
||||
self.uut = transports.BestEffortApplicationTransport(
|
||||
interface=mock.MagicMock(), link_mtu=1500)
|
||||
self.uut.ncp.receive_configure_request_acceptable(0, [])
|
||||
self.uut.ncp.receive_configure_ack()
|
||||
self.uut.packet_received(transports.BestEffortPacket.build(
|
||||
construct.Container(port=0x0001, length=5,
|
||||
information=b'\x02', padding=b'')))
|
||||
|
||||
def test_send(self):
|
||||
self.uut.send(0xabcd, b'information')
|
||||
self.uut.link_socket.send.assert_called_with(
|
||||
transports.BestEffortPacket.build(construct.Container(
|
||||
port=0xabcd, length=15, information=b'information',
|
||||
padding=b'')))
|
||||
|
||||
def test_send_from_socket(self):
|
||||
socket = self.uut.open_socket(0xabcd, timeout=0)
|
||||
socket.send(b'info')
|
||||
self.uut.link_socket.send.assert_called_with(
|
||||
transports.BestEffortPacket.build(construct.Container(
|
||||
port=0xabcd, length=8, information=b'info', padding=b'')))
|
||||
|
||||
def test_receive_from_socket_with_empty_queue(self):
|
||||
socket = self.uut.open_socket(0xabcd, timeout=0)
|
||||
with self.assertRaises(exceptions.ReceiveQueueEmpty):
|
||||
socket.receive(block=False)
|
||||
|
||||
def test_receive_from_socket(self):
|
||||
socket = self.uut.open_socket(0xabcd, timeout=0)
|
||||
self.uut.packet_received(
|
||||
transports.BestEffortPacket.build(construct.Container(
|
||||
port=0xabcd, length=8, information=b'info', padding=b'')))
|
||||
self.assertEqual(b'info', socket.receive(block=False))
|
||||
|
||||
def test_receive_on_unopened_port_doesnt_reach_socket(self):
|
||||
socket = self.uut.open_socket(0xabcd, timeout=0)
|
||||
self.uut.packet_received(
|
||||
transports.BestEffortPacket.build(construct.Container(
|
||||
port=0xface, length=8, information=b'info', padding=b'')))
|
||||
with self.assertRaises(exceptions.ReceiveQueueEmpty):
|
||||
socket.receive(block=False)
|
||||
|
||||
def test_receive_malformed_packet(self):
|
||||
self.uut.packet_received(b'garbage')
|
||||
|
||||
def test_send_equal_to_mtu(self):
|
||||
self.uut.send(0xaaaa, b'a'*1496)
|
||||
|
||||
def test_send_greater_than_mtu(self):
|
||||
with self.assertRaisesRegexp(ValueError, 'Packet length'):
|
||||
self.uut.send(0xaaaa, b'a'*1497)
|
||||
|
||||
def test_transport_down_closes_link_socket_and_ncp(self):
|
||||
self.uut.down()
|
||||
self.uut.link_socket.close.assert_called_with()
|
||||
self.assertIsNone(self.uut.ncp.socket)
|
||||
|
||||
def test_pcmp_port_closed_message_closes_socket(self):
|
||||
socket = self.uut.open_socket(0xabcd, timeout=0)
|
||||
self.assertFalse(socket.closed)
|
||||
self.uut.packet_received(
|
||||
transports.BestEffortPacket.build(construct.Container(
|
||||
port=0x0001, length=7, information=b'\x81\xab\xcd',
|
||||
padding=b'')))
|
||||
self.assertTrue(socket.closed)
|
||||
|
||||
def test_pcmp_port_closed_message_without_socket(self):
|
||||
self.uut.packet_received(
|
||||
transports.BestEffortPacket.build(construct.Container(
|
||||
port=0x0001, length=7, information=b'\x81\xaa\xaa',
|
||||
padding=b'')))
|
||||
|
||||
|
||||
class TestReliableTransportPacketBuilders(unittest.TestCase):
|
||||
|
||||
def test_build_info_packet(self):
|
||||
self.assertEqual(
|
||||
b'\x1e\x3f\xbe\xef\x00\x14Data goes here',
|
||||
transports.build_reliable_info_packet(
|
||||
sequence_number=15, ack_number=31, poll=True,
|
||||
port=0xbeef, information=b'Data goes here'))
|
||||
|
||||
def test_build_receive_ready_packet(self):
|
||||
self.assertEqual(
|
||||
b'\x01\x18',
|
||||
transports.build_reliable_supervisory_packet(
|
||||
kind='RR', ack_number=12))
|
||||
|
||||
def test_build_receive_ready_poll_packet(self):
|
||||
self.assertEqual(
|
||||
b'\x01\x19',
|
||||
transports.build_reliable_supervisory_packet(
|
||||
kind='RR', ack_number=12, poll=True))
|
||||
|
||||
def test_build_receive_ready_final_packet(self):
|
||||
self.assertEqual(
|
||||
b'\x01\x19',
|
||||
transports.build_reliable_supervisory_packet(
|
||||
kind='RR', ack_number=12, final=True))
|
||||
|
||||
def test_build_receive_not_ready_packet(self):
|
||||
self.assertEqual(
|
||||
b'\x05\x18',
|
||||
transports.build_reliable_supervisory_packet(
|
||||
kind='RNR', ack_number=12))
|
||||
|
||||
def test_build_reject_packet(self):
|
||||
self.assertEqual(
|
||||
b'\x09\x18',
|
||||
transports.build_reliable_supervisory_packet(
|
||||
kind='REJ', ack_number=12))
|
||||
|
||||
|
||||
class TestReliableTransportBeforeOpened(CommonTransportBeforeOpenedTestCases,
|
||||
unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.addCleanup(timer_helper.cancel_all_timers)
|
||||
self.uut = transports.ReliableTransport(
|
||||
interface=mock.MagicMock(), link_mtu=1500)
|
||||
|
||||
def test_open_socket_waits_for_ncp_to_open(self):
|
||||
self.uut.ncp.is_Opened = mock.Mock()
|
||||
self.uut.ncp.is_Opened.return_value = True
|
||||
self.uut.command_socket.send = lambda packet: (
|
||||
self.uut.response_packet_received(
|
||||
transports.build_reliable_supervisory_packet(
|
||||
kind='RR', ack_number=0, final=True)))
|
||||
open_thread = RealThreadingTimer(0.01, self.uut.this_layer_up)
|
||||
open_thread.daemon = True
|
||||
open_thread.start()
|
||||
self.assertIsNotNone(self.uut.open_socket(0xbeef, timeout=0.5))
|
||||
open_thread.join()
|
||||
|
||||
|
||||
class TestReliableTransportConnectionEstablishment(unittest.TestCase):
|
||||
|
||||
expected_rr_packet = transports.build_reliable_supervisory_packet(
|
||||
kind='RR', ack_number=0, poll=True)
|
||||
|
||||
def setUp(self):
|
||||
FakeTimer.clear_timer_list()
|
||||
timer_patcher = mock.patch('threading.Timer', new=FakeTimer)
|
||||
timer_patcher.start()
|
||||
self.addCleanup(timer_patcher.stop)
|
||||
|
||||
control_protocol_patcher = mock.patch(
|
||||
'pebble.pulse2.transports.TransportControlProtocol')
|
||||
control_protocol_patcher.start()
|
||||
self.addCleanup(control_protocol_patcher.stop)
|
||||
|
||||
self.uut = transports.ReliableTransport(
|
||||
interface=mock.MagicMock(), link_mtu=1500)
|
||||
assert isinstance(self.uut.ncp, mock.MagicMock)
|
||||
self.uut.ncp.is_Opened.return_value = True
|
||||
self.uut.this_layer_up()
|
||||
|
||||
def send_rr_response(self):
|
||||
self.uut.response_packet_received(
|
||||
transports.build_reliable_supervisory_packet(
|
||||
kind='RR', ack_number=0, final=True))
|
||||
|
||||
def test_rr_packet_is_sent_after_this_layer_up_event(self):
|
||||
self.uut.command_socket.send.assert_called_once_with(
|
||||
self.expected_rr_packet)
|
||||
|
||||
def test_rr_command_is_retransmitted_until_response_is_received(self):
|
||||
for _ in range(3):
|
||||
FakeTimer.TIMERS[-1].expire()
|
||||
self.send_rr_response()
|
||||
|
||||
self.assertFalse(FakeTimer.get_active_timers())
|
||||
self.assertEqual(self.uut.command_socket.send.call_args_list,
|
||||
[mock.call(self.expected_rr_packet)]*4)
|
||||
self.assertIsNotNone(self.uut.open_socket(0xabcd, timeout=0))
|
||||
|
||||
def test_transport_negotiation_restarts_if_no_responses(self):
|
||||
for _ in range(self.uut.max_retransmits):
|
||||
FakeTimer.TIMERS[-1].expire()
|
||||
self.assertFalse(FakeTimer.get_active_timers())
|
||||
self.assertIsNone(self.uut.open_socket(0xabcd, timeout=0))
|
||||
self.uut.ncp.restart.assert_called_once_with()
|
||||
|
||||
|
||||
class TestReliableTransport(CommonTransportTestCases,
|
||||
unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
FakeTimer.clear_timer_list()
|
||||
timer_patcher = mock.patch('threading.Timer', new=FakeTimer)
|
||||
timer_patcher.start()
|
||||
self.addCleanup(timer_patcher.stop)
|
||||
|
||||
control_protocol_patcher = mock.patch(
|
||||
'pebble.pulse2.transports.TransportControlProtocol')
|
||||
control_protocol_patcher.start()
|
||||
self.addCleanup(control_protocol_patcher.stop)
|
||||
|
||||
self.uut = transports.ReliableTransport(
|
||||
interface=mock.MagicMock(), link_mtu=1500)
|
||||
assert isinstance(self.uut.ncp, mock.MagicMock)
|
||||
self.uut.ncp.is_Opened.return_value = True
|
||||
self.uut.this_layer_up()
|
||||
self.uut.command_socket.send.reset_mock()
|
||||
self.uut.response_packet_received(
|
||||
transports.build_reliable_supervisory_packet(
|
||||
kind='RR', ack_number=0, final=True))
|
||||
|
||||
def test_send_with_immediate_ack(self):
|
||||
self.uut.send(0xbeef, b'Just some packet data')
|
||||
self.uut.command_socket.send.assert_called_once_with(
|
||||
transports.build_reliable_info_packet(
|
||||
sequence_number=0, ack_number=0, poll=True,
|
||||
port=0xbeef, information=b'Just some packet data'))
|
||||
self.assertEqual(1, len(FakeTimer.get_active_timers()))
|
||||
self.uut.response_packet_received(
|
||||
transports.build_reliable_supervisory_packet(
|
||||
kind='RR', ack_number=1, final=True))
|
||||
self.assertTrue(all(t.cancelled for t in FakeTimer.TIMERS))
|
||||
|
||||
def test_send_with_one_timeout_before_ack(self):
|
||||
self.uut.send(0xabcd, b'this will be sent twice')
|
||||
active_timers = FakeTimer.get_active_timers()
|
||||
self.assertEqual(1, len(active_timers))
|
||||
active_timers[0].expire()
|
||||
self.assertEqual(1, len(FakeTimer.get_active_timers()))
|
||||
self.uut.command_socket.send.assert_has_calls(
|
||||
[mock.call(transports.build_reliable_info_packet(
|
||||
sequence_number=0, ack_number=0,
|
||||
poll=True, port=0xabcd,
|
||||
information=b'this will be sent twice'))]*2)
|
||||
self.uut.response_packet_received(
|
||||
transports.build_reliable_supervisory_packet(
|
||||
kind='RR', ack_number=1, final=True))
|
||||
self.assertTrue(all(t.cancelled for t in FakeTimer.TIMERS))
|
||||
|
||||
def test_send_with_no_response(self):
|
||||
self.uut.send(0xd00d, b'blarg')
|
||||
for _ in xrange(self.uut.max_retransmits):
|
||||
FakeTimer.get_active_timers()[-1].expire()
|
||||
self.uut.ncp.restart.assert_called_once_with()
|
||||
|
||||
def test_receive_info_packet(self):
|
||||
socket = self.uut.open_socket(0xcafe, timeout=0)
|
||||
self.uut.command_packet_received(transports.build_reliable_info_packet(
|
||||
sequence_number=0, ack_number=0, poll=True, port=0xcafe,
|
||||
information=b'info'))
|
||||
self.assertEqual(b'info', socket.receive(block=False))
|
||||
self.uut.response_socket.send.assert_called_once_with(
|
||||
transports.build_reliable_supervisory_packet(
|
||||
kind='RR', ack_number=1, final=True))
|
||||
|
||||
def test_receive_duplicate_packet(self):
|
||||
socket = self.uut.open_socket(0xba5e, timeout=0)
|
||||
packet = transports.build_reliable_info_packet(
|
||||
sequence_number=0, ack_number=0, poll=True, port=0xba5e,
|
||||
information=b'all your base are belong to us')
|
||||
self.uut.command_packet_received(packet)
|
||||
self.assertEqual(b'all your base are belong to us',
|
||||
socket.receive(block=False))
|
||||
self.uut.response_socket.reset_mock()
|
||||
self.uut.command_packet_received(packet)
|
||||
self.uut.response_socket.send.assert_called_once_with(
|
||||
transports.build_reliable_supervisory_packet(
|
||||
kind='RR', ack_number=1, final=True))
|
||||
with self.assertRaises(exceptions.ReceiveQueueEmpty):
|
||||
socket.receive(block=False)
|
||||
|
||||
def test_queueing_multiple_packets_to_send(self):
|
||||
packets = [(0xfeed, b'Some data'),
|
||||
(0x6789, b'More data'),
|
||||
(0xfeed, b'Third packet')]
|
||||
for protocol, information in packets:
|
||||
self.uut.send(protocol, information)
|
||||
|
||||
for seq, (port, information) in enumerate(packets):
|
||||
self.uut.command_socket.send.assert_called_once_with(
|
||||
transports.build_reliable_info_packet(
|
||||
sequence_number=seq, ack_number=0, poll=True,
|
||||
port=port, information=information))
|
||||
self.uut.command_socket.send.reset_mock()
|
||||
self.uut.response_packet_received(
|
||||
transports.build_reliable_supervisory_packet(
|
||||
kind='RR', ack_number=seq+1, final=True))
|
||||
|
||||
def test_send_equal_to_mtu(self):
|
||||
self.uut.send(0xaaaa, b'a'*1494)
|
||||
|
||||
def test_send_greater_than_mtu(self):
|
||||
with self.assertRaisesRegexp(ValueError, 'Packet length'):
|
||||
self.uut.send(0xaaaa, b'a'*1496)
|
||||
|
||||
def test_send_from_socket(self):
|
||||
socket = self.uut.open_socket(0xabcd, timeout=0)
|
||||
socket.send(b'info')
|
||||
self.uut.command_socket.send.assert_called_with(
|
||||
transports.build_reliable_info_packet(
|
||||
sequence_number=0, ack_number=0,
|
||||
poll=True, port=0xabcd, information=b'info'))
|
||||
|
||||
def test_receive_from_socket_with_empty_queue(self):
|
||||
socket = self.uut.open_socket(0xabcd, timeout=0)
|
||||
with self.assertRaises(exceptions.ReceiveQueueEmpty):
|
||||
socket.receive(block=False)
|
||||
|
||||
def test_receive_from_socket(self):
|
||||
socket = self.uut.open_socket(0xabcd, timeout=0)
|
||||
self.uut.command_packet_received(transports.build_reliable_info_packet(
|
||||
sequence_number=0, ack_number=0, poll=True, port=0xabcd,
|
||||
information=b'info info info'))
|
||||
self.assertEqual(b'info info info', socket.receive(block=False))
|
||||
|
||||
def test_receive_on_unopened_port_doesnt_reach_socket(self):
|
||||
socket = self.uut.open_socket(0xabcd, timeout=0)
|
||||
self.uut.command_packet_received(transports.build_reliable_info_packet(
|
||||
sequence_number=0, ack_number=0, poll=True, port=0x3333,
|
||||
information=b'info'))
|
||||
with self.assertRaises(exceptions.ReceiveQueueEmpty):
|
||||
socket.receive(block=False)
|
||||
|
||||
def test_receive_malformed_command_packet(self):
|
||||
self.uut.command_packet_received(b'garbage')
|
||||
self.uut.ncp.restart.assert_called_once_with()
|
||||
|
||||
def test_receive_malformed_response_packet(self):
|
||||
self.uut.response_packet_received(b'garbage')
|
||||
self.uut.ncp.restart.assert_called_once_with()
|
||||
|
||||
def test_transport_down_closes_link_sockets_and_ncp(self):
|
||||
self.uut.down()
|
||||
self.uut.command_socket.close.assert_called_with()
|
||||
self.uut.response_socket.close.assert_called_with()
|
||||
self.uut.ncp.down.assert_called_with()
|
||||
|
||||
def test_pcmp_port_closed_message_closes_socket(self):
|
||||
socket = self.uut.open_socket(0xabcd, timeout=0)
|
||||
self.assertFalse(socket.closed)
|
||||
self.uut.command_packet_received(transports.build_reliable_info_packet(
|
||||
sequence_number=0, ack_number=0, poll=True, port=0x0001,
|
||||
information=b'\x81\xab\xcd'))
|
||||
self.assertTrue(socket.closed)
|
||||
|
||||
def test_pcmp_port_closed_message_without_socket(self):
|
||||
self.uut.command_packet_received(transports.build_reliable_info_packet(
|
||||
sequence_number=0, ack_number=0, poll=True, port=0x0001,
|
||||
information=b'\x81\xaa\xaa'))
|
||||
|
||||
|
||||
class TestSocket(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.uut = transports.Socket(mock.Mock(), 1234)
|
||||
|
||||
def test_empty_receive_queue(self):
|
||||
with self.assertRaises(exceptions.ReceiveQueueEmpty):
|
||||
self.uut.receive(block=False)
|
||||
|
||||
def test_empty_receive_queue_blocking(self):
|
||||
with self.assertRaises(exceptions.ReceiveQueueEmpty):
|
||||
self.uut.receive(timeout=0.001)
|
||||
|
||||
def test_receive(self):
|
||||
self.uut.on_receive(b'data')
|
||||
self.assertEqual(b'data', self.uut.receive(block=False))
|
||||
with self.assertRaises(exceptions.ReceiveQueueEmpty):
|
||||
self.uut.receive(block=False)
|
||||
|
||||
def test_receive_twice(self):
|
||||
self.uut.on_receive(b'one')
|
||||
self.uut.on_receive(b'two')
|
||||
self.assertEqual(b'one', self.uut.receive(block=False))
|
||||
self.assertEqual(b'two', self.uut.receive(block=False))
|
||||
|
||||
def test_receive_interleaved(self):
|
||||
self.uut.on_receive(b'one')
|
||||
self.assertEqual(b'one', self.uut.receive(block=False))
|
||||
self.uut.on_receive(b'two')
|
||||
self.assertEqual(b'two', self.uut.receive(block=False))
|
||||
|
||||
def test_send(self):
|
||||
self.uut.send(b'data')
|
||||
self.uut.transport.send.assert_called_once_with(1234, b'data')
|
||||
|
||||
def test_close(self):
|
||||
self.uut.close()
|
||||
self.uut.transport.unregister_socket.assert_called_once_with(1234)
|
||||
|
||||
def test_send_after_close_is_an_error(self):
|
||||
self.uut.close()
|
||||
with self.assertRaises(exceptions.SocketClosed):
|
||||
self.uut.send(b'data')
|
||||
|
||||
def test_receive_after_close_is_an_error(self):
|
||||
self.uut.close()
|
||||
with self.assertRaises(exceptions.SocketClosed):
|
||||
self.uut.receive(block=False)
|
||||
|
||||
def test_blocking_receive_after_close_is_an_error(self):
|
||||
self.uut.close()
|
||||
with self.assertRaises(exceptions.SocketClosed):
|
||||
self.uut.receive(timeout=0.001)
|
||||
|
||||
def test_close_during_blocking_receive_aborts_the_receive(self):
|
||||
thread_started = threading.Event()
|
||||
result = [None]
|
||||
|
||||
def test_thread():
|
||||
thread_started.set()
|
||||
try:
|
||||
self.uut.receive(timeout=0.3)
|
||||
except Exception as e:
|
||||
result[0] = e
|
||||
thread = threading.Thread(target=test_thread)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
assert thread_started.wait(timeout=0.5)
|
||||
self.uut.close()
|
||||
thread.join()
|
||||
self.assertIsInstance(result[0], exceptions.SocketClosed)
|
||||
|
||||
def test_close_is_idempotent(self):
|
||||
self.uut.close()
|
||||
self.uut.close()
|
||||
self.assertEqual(1, self.uut.transport.unregister_socket.call_count)
|
25
python_libs/pulse2/tests/timer_helper.py
Normal file
25
python_libs/pulse2/tests/timer_helper.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import threading
|
||||
|
||||
|
||||
def cancel_all_timers():
|
||||
'''Cancel all running timer threads in the process.
|
||||
'''
|
||||
for thread in threading.enumerate():
|
||||
try:
|
||||
thread.cancel()
|
||||
except AttributeError:
|
||||
pass
|
Loading…
Add table
Add a link
Reference in a new issue