| OLD | NEW |
| (Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright 2011 Google Inc. All Rights Reserved. |
| 3 # |
| 4 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 # you may not use this file except in compliance with the License. |
| 6 # You may obtain a copy of the License at |
| 7 # |
| 8 # http://www.apache.org/licenses/LICENSE-2.0 |
| 9 # |
| 10 # Unless required by applicable law or agreed to in writing, software |
| 11 # distributed under the License is distributed on an "AS IS" BASIS, |
| 12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 # See the License for the specific language governing permissions and |
| 14 # limitations under the License. |
| 15 |
| 16 """System integration test for traffic shaping. |
| 17 |
| 18 Usage: |
| 19 $ sudo ./trafficshaper_test.py |
| 20 """ |
| 21 |
| 22 import daemonserver |
| 23 import logging |
| 24 import multiprocessing |
| 25 import platformsettings |
| 26 import socket |
| 27 import SocketServer |
| 28 import trafficshaper |
| 29 import unittest |
| 30 |
| 31 |
| 32 RESPONSE_SIZE_KEY = 'response-size:' |
| 33 TEST_DNS_PORT = 5555 |
| 34 TEST_HTTP_PORT = 8888 |
| 35 TIMER = platformsettings.get_platform_settings().timer |
| 36 |
| 37 |
| 38 def GetElapsedMs(start_time, end_time): |
| 39 """Return milliseconds elapsed between |start_time| and |end_time|. |
| 40 |
| 41 Args: |
| 42 start_time: seconds as a float (or string representation of float). |
| 43 end_time: seconds as a float (or string representation of float). |
| 44 Return: |
| 45 milliseconds elapsed as integer. |
| 46 """ |
| 47 return int((float(end_time) - float(start_time)) * 1000) |
| 48 |
| 49 |
| 50 class TrafficShaperTest(unittest.TestCase): |
| 51 |
| 52 def testBadBandwidthRaises(self): |
| 53 self.assertRaises(trafficshaper.BandwidthValueError, |
| 54 trafficshaper.TrafficShaper, |
| 55 down_bandwidth='1KBit/s') |
| 56 |
| 57 |
| 58 class TimedUdpHandler(SocketServer.DatagramRequestHandler): |
| 59 """UDP handler that returns the time when the request was handled.""" |
| 60 |
| 61 def handle(self): |
| 62 data = self.rfile.read() |
| 63 read_time = self.server.timer() |
| 64 self.wfile.write(str(read_time)) |
| 65 |
| 66 |
| 67 class TimedTcpHandler(SocketServer.StreamRequestHandler): |
| 68 """Tcp handler that returns the time when the request was read. |
| 69 |
| 70 It can respond with the number of bytes specified in the request. |
| 71 The request looks like: |
| 72 request_data -> RESPONSE_SIZE_KEY num_reponse_bytes '\n' ANY_DATA |
| 73 """ |
| 74 |
| 75 def handle(self): |
| 76 data = self.rfile.read() |
| 77 read_time = self.server.timer() |
| 78 contents = str(read_time) |
| 79 if data.startswith(RESPONSE_SIZE_KEY): |
| 80 num_response_bytes = int(data[len(RESPONSE_SIZE_KEY):data.index('\n')]) |
| 81 contents = '%s\n%s' % (contents, |
| 82 '\x00' * (num_response_bytes - len(contents) - 1)) |
| 83 self.wfile.write(contents) |
| 84 |
| 85 |
| 86 class TimedUdpServer(SocketServer.ThreadingUDPServer, |
| 87 daemonserver.DaemonServer): |
| 88 """A simple UDP server similar to dnsproxy.""" |
| 89 |
| 90 # Override SocketServer.TcpServer setting to avoid intermittent errors. |
| 91 allow_reuse_address = True |
| 92 |
| 93 def __init__(self, host, port, timer=TIMER): |
| 94 SocketServer.ThreadingUDPServer.__init__( |
| 95 self, (host, port), TimedUdpHandler) |
| 96 self.timer = timer |
| 97 |
| 98 def cleanup(self): |
| 99 pass |
| 100 |
| 101 |
| 102 class TimedTcpServer(SocketServer.ThreadingTCPServer, |
| 103 daemonserver.DaemonServer): |
| 104 """A simple TCP server similar to httpproxy.""" |
| 105 |
| 106 # Override SocketServer.TcpServer setting to avoid intermittent errors. |
| 107 allow_reuse_address = True |
| 108 |
| 109 def __init__(self, host, port, timer=TIMER): |
| 110 SocketServer.ThreadingTCPServer.__init__( |
| 111 self, (host, port), TimedTcpHandler) |
| 112 self.timer = timer |
| 113 |
| 114 def cleanup(self): |
| 115 try: |
| 116 self.shutdown() |
| 117 except KeyboardInterrupt, e: |
| 118 pass |
| 119 |
| 120 |
| 121 class TcpTestSocketCreator: |
| 122 """A TCP socket creator suitable for with-statement.""" |
| 123 |
| 124 def __init__(self, host, port, timeout=1.0): |
| 125 self.address = (host, port) |
| 126 self.timeout = timeout |
| 127 |
| 128 def __enter__(self): |
| 129 self.socket = socket.create_connection(self.address, timeout=self.timeout) |
| 130 return self.socket |
| 131 |
| 132 def __exit__(self, *args): |
| 133 self.socket.close() |
| 134 |
| 135 |
| 136 class TimedTestCase(unittest.TestCase): |
| 137 def assertAlmostEqual(self, expected, actual, tolerance=0.05): |
| 138 """Like the following with nicer default message: |
| 139 assertTrue(expected <= actual + tolerance && |
| 140 expected >= actual - tolerance) |
| 141 """ |
| 142 delta = tolerance * expected |
| 143 if actual > expected + delta or actual < expected - delta: |
| 144 self.fail('%s is not equal to expected %s +/- %s%%' % ( |
| 145 actual, expected, 100 * tolerance)) |
| 146 |
| 147 |
| 148 class TcpTrafficShaperTest(TimedTestCase): |
| 149 |
| 150 def setUp(self): |
| 151 platform_settings = platformsettings.get_platform_settings() |
| 152 self.host = platform_settings.get_server_ip_address() |
| 153 self.port = TEST_HTTP_PORT |
| 154 self.tcp_socket_creator = TcpTestSocketCreator(self.host, self.port) |
| 155 self.timer = TIMER |
| 156 |
| 157 def TrafficShaper(self, **kwargs): |
| 158 return trafficshaper.TrafficShaper( |
| 159 host=self.host, port=self.port, dns_port=None, **kwargs) |
| 160 |
| 161 def GetTcpSendTimeMs(self, num_bytes): |
| 162 """Return time in milliseconds to send |num_bytes|.""" |
| 163 |
| 164 with self.tcp_socket_creator as s: |
| 165 start_time = self.timer() |
| 166 request_data = '\x00' * num_bytes |
| 167 |
| 168 s.sendall(request_data) |
| 169 # TODO(slamm): Figure out why partial is shutdown needed to make it work. |
| 170 s.shutdown(socket.SHUT_WR) |
| 171 read_time = s.recv(1024) |
| 172 return GetElapsedMs(start_time, read_time) |
| 173 |
| 174 def GetTcpReceiveTimeMs(self, num_bytes): |
| 175 """Return time in milliseconds to receive |num_bytes|.""" |
| 176 |
| 177 with self.tcp_socket_creator as s: |
| 178 s.sendall('%s%s\n' % (RESPONSE_SIZE_KEY, num_bytes)) |
| 179 # TODO(slamm): Figure out why partial is shutdown needed to make it work. |
| 180 s.shutdown(socket.SHUT_WR) |
| 181 num_remaining_bytes = num_bytes |
| 182 read_time = None |
| 183 while num_remaining_bytes > 0: |
| 184 response_data = s.recv(4096) |
| 185 num_remaining_bytes -= len(response_data) |
| 186 if not read_time: |
| 187 read_time, padding = response_data.split('\n') |
| 188 return GetElapsedMs(read_time, self.timer()) |
| 189 |
| 190 def testTcpConnectToIp(self): |
| 191 """Verify that it takes |delay_ms| to establish a TCP connection.""" |
| 192 with TimedTcpServer(self.host, self.port): |
| 193 for delay_ms in (100, 175): |
| 194 with self.TrafficShaper(delay_ms=delay_ms): |
| 195 start_time = self.timer() |
| 196 with self.tcp_socket_creator: |
| 197 connect_time = GetElapsedMs(start_time, self.timer()) |
| 198 self.assertAlmostEqual(delay_ms, connect_time, tolerance=0.12) |
| 199 |
| 200 def testTcpUploadShaping(self): |
| 201 """Verify that 'up' bandwidth is shaped on TCP connections.""" |
| 202 num_bytes = 1024 * 100 |
| 203 bandwidth_kbits = 2000 |
| 204 expected_ms = 8.0 * num_bytes / bandwidth_kbits |
| 205 with TimedTcpServer(self.host, self.port): |
| 206 with self.TrafficShaper(up_bandwidth='%sKbit/s' % bandwidth_kbits): |
| 207 self.assertAlmostEqual(expected_ms, self.GetTcpSendTimeMs(num_bytes)) |
| 208 |
| 209 def testTcpDownloadShaping(self): |
| 210 """Verify that 'down' bandwidth is shaped on TCP connections.""" |
| 211 num_bytes = 1024 * 100 |
| 212 bandwidth_kbits = 2000 |
| 213 expected_ms = 8.0 * num_bytes / bandwidth_kbits |
| 214 with TimedTcpServer(self.host, self.port): |
| 215 with self.TrafficShaper(down_bandwidth='%sKbit/s' % bandwidth_kbits): |
| 216 self.assertAlmostEqual(expected_ms, self.GetTcpReceiveTimeMs(num_bytes)) |
| 217 |
| 218 def testTcpInterleavedDownloads(self): |
| 219 # TODO(slamm): write tcp interleaved downloads test |
| 220 pass |
| 221 |
| 222 |
| 223 class UdpTrafficShaperTest(TimedTestCase): |
| 224 |
| 225 def setUp(self): |
| 226 platform_settings = platformsettings.get_platform_settings() |
| 227 self.host = platform_settings.get_server_ip_address() |
| 228 self.dns_port = TEST_DNS_PORT |
| 229 self.timer = TIMER |
| 230 |
| 231 def TrafficShaper(self, **kwargs): |
| 232 return trafficshaper.TrafficShaper( |
| 233 host=self.host, port=None, dns_port=self.dns_port, **kwargs) |
| 234 |
| 235 def GetUdpSendReceiveTimesMs(self): |
| 236 """Return time in milliseconds to send |num_bytes|.""" |
| 237 start_time = self.timer() |
| 238 udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) |
| 239 udp_socket.sendto('test data\n', (self.host, self.dns_port)) |
| 240 read_time = udp_socket.recv(1024) |
| 241 return (GetElapsedMs(start_time, read_time), |
| 242 GetElapsedMs(read_time, self.timer())) |
| 243 |
| 244 def testUdpDelay(self): |
| 245 for delay_ms in (100, 170): |
| 246 expected_ms = delay_ms / 2 |
| 247 with TimedUdpServer(self.host, self.dns_port): |
| 248 with self.TrafficShaper(delay_ms=delay_ms): |
| 249 send_ms, receive_ms = self.GetUdpSendReceiveTimesMs() |
| 250 self.assertAlmostEqual(expected_ms, send_ms, tolerance=0.10) |
| 251 self.assertAlmostEqual(expected_ms, receive_ms, tolerance=0.10) |
| 252 |
| 253 |
| 254 def testUdpInterleavedDelay(self): |
| 255 # TODO(slamm): write udp interleaved udp delay test |
| 256 pass |
| 257 |
| 258 |
| 259 class TcpAndUdpTrafficShaperTest(TimedTestCase): |
| 260 # TODO(slamm): Test concurrent TCP and UDP traffic |
| 261 pass |
| 262 |
| 263 |
| 264 # TODO(slamm): Packet loss rate (try different ports) |
| 265 |
| 266 |
| 267 if __name__ == '__main__': |
| 268 #logging.getLogger().setLevel(logging.DEBUG) |
| 269 unittest.main() |
| OLD | NEW |