summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorshadi@chromium.org <shadi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-12-20 01:46:37 +0000
committershadi@chromium.org <shadi@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-12-20 01:46:37 +0000
commit8bc584e620bac5f0119897fa87160da99f5ff256 (patch)
tree961810fcd6cbf06e65c92684b9b4a9b7f4ddac40
parent8a9e1a19230ab3711d141d4b969db81aa4d10ca5 (diff)
downloadchromium_src-8bc584e620bac5f0119897fa87160da99f5ff256.zip
chromium_src-8bc584e620bac5f0119897fa87160da99f5ff256.tar.gz
chromium_src-8bc584e620bac5f0119897fa87160da99f5ff256.tar.bz2
Merging CNS server with scripts.
Merging the constrained network scripts (http://crrev.com/114355) with the constrained network server (http://crrev.com/110458). Some changes made to the CNS scripts to support local server setup. BUG=104242 TEST=Unit tests and ran locally Review URL: http://codereview.chromium.org/8856001 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@115062 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--chrome/test/data/media/html/media_constrained_network.html4
-rwxr-xr-xchrome/test/functional/media/media_constrained_network_perf.py15
-rwxr-xr-xmedia/tools/constrained_network_server/cn.py13
-rwxr-xr-xmedia/tools/constrained_network_server/cns.py91
-rwxr-xr-xmedia/tools/constrained_network_server/cns_test.py31
-rwxr-xr-xmedia/tools/constrained_network_server/traffic_control.py4
-rw-r--r--media/tools/constrained_network_server/traffic_control_test.py48
7 files changed, 156 insertions, 50 deletions
diff --git a/chrome/test/data/media/html/media_constrained_network.html b/chrome/test/data/media/html/media_constrained_network.html
index c20f772..37e56a9 100644
--- a/chrome/test/data/media/html/media_constrained_network.html
+++ b/chrome/test/data/media/html/media_constrained_network.html
@@ -6,7 +6,7 @@
</head>
<body>
- <video/>
+ <video controls/>
</body>
<script type="text/javascript">
@@ -33,7 +33,7 @@
playTime = new Date().getTime() - startTime;
durMs = video.duration * 1000;
- extra_play_percentage = Math.max(0, (playTime - durMs) / durMs)
+ extra_play_percentage = Math.max(0, (playTime - durMs) * 100 / durMs)
}, false);
// Called by the PyAuto controller to initiate testing.
diff --git a/chrome/test/functional/media/media_constrained_network_perf.py b/chrome/test/functional/media/media_constrained_network_perf.py
index d6dc09e..83a4583 100755
--- a/chrome/test/functional/media/media_constrained_network_perf.py
+++ b/chrome/test/functional/media/media_constrained_network_perf.py
@@ -55,9 +55,11 @@ _TEST_HTML_PATH = os.path.join(
# Number of threads to use during testing.
_TEST_THREADS = 3
-# File name of video to collect metrics for.
+# File name of video to collect metrics for and its duration (used for timeout).
# TODO(dalecurtis): Should be set on the command line.
_TEST_VIDEO = 'roller.webm'
+_TEST_VIDEO_DURATION_SEC = 28.53
+
# Path to CNS executable relative to source root.
_CNS_PATH = os.path.join(
@@ -107,10 +109,11 @@ class TestWorker(threading.Thread):
tab = self._FindTabLocked(unique_url)
if self._epp < 0:
- self._epp = self._pyauto.GetDOMValue(
- 'extra_play_percentage', tab_index=tab)
+ self._epp = int(self._pyauto.GetDOMValue('extra_play_percentage',
+ tab_index=tab))
if self._ttp < 0:
- self._ttp = self._pyauto.GetDOMValue('time_to_playback', tab_index=tab)
+ self._ttp = int(self._pyauto.GetDOMValue('time_to_playback',
+ tab_index=tab))
return self._epp >= 0 and self._ttp >= 0
def run(self):
@@ -152,7 +155,8 @@ class TestWorker(threading.Thread):
# here since pyauto.WaitUntil doesn't call into Chrome.
self._epp = self._ttp = -1
self._pyauto.WaitUntil(
- self._HaveMetrics, args=[unique_url], retry_sleep=2)
+ self._HaveMetrics, args=[unique_url], retry_sleep=2,
+ timeout=_TEST_VIDEO_DURATION_SEC * 10)
# Record results.
# TODO(dalecurtis): Support reference builds.
@@ -179,6 +183,7 @@ class MediaConstrainedNetworkPerfTest(pyauto.PyUITest):
'--interface', 'lo',
'--www-root', os.path.join(
self.DataDir(), 'pyauto_private', 'media')]
+
process = subprocess.Popen(cmd, stderr=subprocess.PIPE)
# Wait for server to start up.
diff --git a/media/tools/constrained_network_server/cn.py b/media/tools/constrained_network_server/cn.py
index c2fd877..b79d8fb 100755
--- a/media/tools/constrained_network_server/cn.py
+++ b/media/tools/constrained_network_server/cn.py
@@ -1,5 +1,4 @@
#!/usr/bin/env python
-
# Copyright (c) 2011 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
@@ -67,17 +66,15 @@ def _ParseArgs():
parser.add_option('--server-port', type='int',
help='Port to forward traffic on --port to.')
parser.add_option('--bandwidth', type='int',
- help=('Bandwidth of the network in kbps. Default: '
- '%defaultkbps.'))
+ help='Bandwidth of the network in kbps.')
parser.add_option('--latency', type='int',
help=('Latency (delay) added to each outgoing packet in '
- 'ms. Default: %defaultms.'))
+ 'ms.'))
parser.add_option('--loss', type='int',
- help=('Packet-loss percentage on outgoing packets. '
- 'Default: %default%.'))
+ help='Packet-loss percentage on outgoing packets. ')
parser.add_option('--interface', type='string',
- help=('Interface to setup constraints on. Use lo for a '
- 'local client. Default: %default.'))
+ help=('Interface to setup constraints on. Use "lo" for a '
+ 'local client.'))
parser.add_option('-v', '--verbose', action='store_true', dest='verbose',
default=False, help='Turn on verbose output.')
options, args = parser.parse_args()
diff --git a/media/tools/constrained_network_server/cns.py b/media/tools/constrained_network_server/cns.py
index 6f3df21..70e68163 100755
--- a/media/tools/constrained_network_server/cns.py
+++ b/media/tools/constrained_network_server/cns.py
@@ -18,6 +18,7 @@ import signal
import sys
import threading
import time
+import traffic_control
try:
import cherrypy
@@ -91,13 +92,31 @@ class PortAllocator(object):
for port in xrange(self._port_range[0], self._port_range[1]):
if port in self._ports:
continue
+ if self._SetupPort(port, **kwargs):
+ kwargs['port'] = port
+ self._ports[port] = {'last_update': time.time(), 'key': full_key,
+ 'config': kwargs}
+ return port
+
+ def _SetupPort(self, port, **kwargs):
+ """Setup network constraints on port using the requested parameters.
- # TODO(dalecurtis): Integrate with shadi's scripts.
- # We've found an open port so call the script and set it up.
- #Port.Setup(port=port, **kwargs)
+ Args:
+ port: The port number to setup network constraints on.
+ **kwargs: Network constraints to set up on the port.
- self._ports[port] = {'last_update': time.time(), 'key': full_key}
- return port
+ Returns:
+ True if setting the network constraints on the port was successful, false
+ otherwise.
+ """
+ kwargs['port'] = port
+ try:
+ cherrypy.log('Setting up port %d' % port)
+ traffic_control.CreateConstrainedPort(kwargs)
+ return True
+ except traffic_control.TrafficControlError as e:
+ cherrypy.log('Error: %s\nOutput: %s', e.msg, e.error)
+ return False
def _CleanupLocked(self, all_ports):
"""Internal cleanup method, expects lock to have already been acquired.
@@ -113,19 +132,28 @@ class PortAllocator(object):
expired = now - status['last_update'] > self._expiry_time_secs
if all_ports or expired:
cherrypy.log('Cleaning up port %d' % port)
+ self._DeletePort(port)
+ del self._ports[port]
- # TODO(dalecurtis): Integrate with shadi's scripts.
- #Port.Delete(port=port)
+ def _DeletePort(self, port):
+ """Deletes network constraints on port.
- del self._ports[port]
+ Args:
+ port: The port number associated with the network constraints.
+ """
+ try:
+ traffic_control.DeleteConstrainedPort(self._ports[port]['config'])
+ except traffic_control.TrafficControlError as e:
+ cherrypy.log('Error: %s\nOutput: %s', e.msg, e.error)
- def Cleanup(self, all_ports=False):
+ def Cleanup(self, interface, all_ports=False):
"""Cleans up expired ports, or if all_ports=True, all allocated ports.
By default, ports which haven't been used for self._expiry_time_secs are
torn down. If all_ports=True then they are torn down regardless.
Args:
+ interface: Interface the constrained network is setup on.
all_ports: Should all ports be torn down regardless of expiration?
"""
with self._port_lock:
@@ -179,14 +207,12 @@ class ConstrainedNetworkServer(object):
return cherrypy.lib.static.serve_file(sanitized_path)
# Validate inputs. isdigit() guarantees a natural number.
- if bandwidth and not bandwidth.isdigit():
- raise cherrypy.HTTPError(400, 'Invalid bandwidth constraint.')
-
- if latency and not latency.isdigit():
- raise cherrypy.HTTPError(400, 'Invalid latency constraint.')
-
- if loss and not loss.isdigit() and not int(loss) <= 100:
- raise cherrypy.HTTPError(400, 'Invalid loss constraint.')
+ bandwidth = self._ParseIntParameter(
+ bandwidth, 'Invalid bandwidth constraint.', lambda x: x > 0)
+ latency = self._ParseIntParameter(
+ latency, 'Invalid latency constraint.', lambda x: x >= 0)
+ loss = self._ParseIntParameter(
+ loss, 'Invalid loss constraint.', lambda x: x <= 100 and x >= 0)
# Allocate a port using the given constraints. If a port with the requested
# key is already allocated, it will be reused.
@@ -194,14 +220,15 @@ class ConstrainedNetworkServer(object):
# TODO(dalecurtis): The key cherrypy.request.remote.ip might not be unique
# if build slaves are sharing the same VM.
constrained_port = self._port_allocator.Get(
- cherrypy.request.remote.ip, bandwidth=bandwidth, latency=latency,
+ cherrypy.request.remote.ip, server_port=self._options.port,
+ interface=self._options.interface, bandwidth=bandwidth, latency=latency,
loss=loss)
if not constrained_port:
raise cherrypy.HTTPError(503, 'Service unavailable. Out of ports.')
# Build constrained URL. Only pass on the file parameter.
- constrained_url = '%s?file=%s' % (
+ constrained_url = '%s?f=%s' % (
cherrypy.url().replace(
':%d' % self._options.port, ':%d' % constrained_port),
f)
@@ -209,6 +236,30 @@ class ConstrainedNetworkServer(object):
# Redirect request to the constrained port.
cherrypy.lib.cptools.redirect(constrained_url, internal=False)
+ def _ParseIntParameter(self, param, msg, check):
+ """Returns integer value of param and verifies it satisfies the check.
+
+ Args:
+ param: Parameter name to check.
+ msg: Message in error if raised.
+ check: Check to verify the parameter value.
+
+ Returns:
+ None if param is None, integer value of param otherwise.
+
+ Raises:
+ cherrypy.HTTPError if param can not be converted to integer or if it does
+ not satisfy the check.
+ """
+ if param:
+ try:
+ int_value = int(param)
+ if check(int_value):
+ return int_value
+ except:
+ pass
+ raise cherrypy.HTTPError(400, msg)
+
def ParseArgs():
"""Define and parse the command-line arguments."""
@@ -267,7 +318,7 @@ def Main():
finally:
# Disable Ctrl-C handler to prevent interruption of cleanup.
signal.signal(signal.SIGINT, lambda signal, frame: None)
- pa.Cleanup(all_ports=True)
+ pa.Cleanup(options.interface, all_ports=True)
if __name__ == '__main__':
diff --git a/media/tools/constrained_network_server/cns_test.py b/media/tools/constrained_network_server/cns_test.py
index 2be56df..319f33a 100755
--- a/media/tools/constrained_network_server/cns_test.py
+++ b/media/tools/constrained_network_server/cns_test.py
@@ -13,6 +13,10 @@ import unittest
import urllib2
import cns
+import traffic_control
+
+# The local interface to test on.
+_INTERFACE = 'lo'
class PortAllocatorTest(unittest.TestCase):
@@ -29,12 +33,28 @@ class PortAllocatorTest(unittest.TestCase):
# TODO(dalecurtis): Mock out actual calls to shadi's port setup.
self._pa = cns.PortAllocator(cns._DEFAULT_CNS_PORT_RANGE, self._EXPIRY_TIME)
+ self._MockTrafficControl()
def tearDown(self):
- self._pa.Cleanup(all_ports=True)
+ self._pa.Cleanup(_INTERFACE, all_ports=True)
# Ensure ports are cleaned properly.
self.assertEquals(self._pa._ports, {})
time.time = self._old_time
+ self._RestoreTrafficControl()
+
+ def _MockTrafficControl(self):
+ self.old_CreateConstrainedPort = traffic_control.CreateConstrainedPort
+ self.old_DeleteConstrainedPort = traffic_control.DeleteConstrainedPort
+ self.old_TearDown = traffic_control.TearDown
+
+ traffic_control.CreateConstrainedPort = lambda config: True
+ traffic_control.DeleteConstrainedPort = lambda config: True
+ traffic_control.TearDown = lambda config: True
+
+ def _RestoreTrafficControl(self):
+ traffic_control.CreateConstrainedPort = self.old_CreateConstrainedPort
+ traffic_control.DeleteConstrainedPort = self.old_DeleteConstrainedPort
+ traffic_control.TearDown = self.old_TearDown
def testPortAllocator(self):
# Ensure Get() succeeds and returns the correct port.
@@ -94,11 +114,11 @@ class ConstrainedNetworkServerTest(unittest.TestCase):
cns._DEFAULT_SERVING_PORT)
# Setting for latency testing.
- _LATENCY_TEST_SECS = 5
+ _LATENCY_TEST_SECS = 1
def _StartServer(self):
"""Starts the CNS, returns pid."""
- cmd = ['python', 'cns.py']
+ cmd = ['python', 'cns.py', '--interface=%s' % _INTERFACE]
process = subprocess.Popen(cmd, stderr=subprocess.PIPE)
# Wait for server to startup.
@@ -125,7 +145,7 @@ class ConstrainedNetworkServerTest(unittest.TestCase):
def tearDown(self):
os.unlink(self._file)
- os.kill(self._server_pid, signal.SIGKILL)
+ os.kill(self._server_pid, signal.SIGTERM)
def testServerServesFiles(self):
now = time.time()
@@ -140,6 +160,9 @@ class ConstrainedNetworkServerTest(unittest.TestCase):
self.assertTrue(time.time() - now < self._LATENCY_TEST_SECS)
def testServerLatencyConstraint(self):
+ """Tests serving a file with a latency network constraint."""
+ # Abort if does not have root access.
+ self.assertEqual(os.geteuid(), 0, 'You need root access to run this test.')
now = time.time()
base_url = '%sf=%s' % (self._SERVER_URL, self._relative_fn)
diff --git a/media/tools/constrained_network_server/traffic_control.py b/media/tools/constrained_network_server/traffic_control.py
index d6527ff..aa8a157 100755
--- a/media/tools/constrained_network_server/traffic_control.py
+++ b/media/tools/constrained_network_server/traffic_control.py
@@ -259,13 +259,11 @@ def _GetFilterHandleId(interface, port):
"""
command = ['tc', 'filter', 'list', 'dev', interface, 'parent', '1:']
output = _Exec(command, msg='Error listing filters.')
-
# Search for the filter handle ID associated with class ID '1:port'.
handle_id_re = re.search(
- '([0-9a-fA-F]{3}::[0-9a-fA-F]{3}).*(?=flowid 1:%x$)' % port, output)
+ '([0-9a-fA-F]{3}::[0-9a-fA-F]{3}).*(?=flowid 1:%x\s)' % port, output)
if handle_id_re:
return handle_id_re.group(1)
-
raise TrafficControlError(('Could not find filter handle ID for class ID '
'1:%x.') % port)
diff --git a/media/tools/constrained_network_server/traffic_control_test.py b/media/tools/constrained_network_server/traffic_control_test.py
index 583becb..a84462f 100644
--- a/media/tools/constrained_network_server/traffic_control_test.py
+++ b/media/tools/constrained_network_server/traffic_control_test.py
@@ -24,7 +24,7 @@ class TrafficControlTests(unittest.TestCase):
def setUp(self):
"""Setup a dummy interface."""
# If we update to python version 2.7 or newer we can use setUpClass() or
- # unittest.skipIf()
+ # unittest.skipIf().
if os.getuid() != 0:
sys.exit('You need root access to run these tests.')
@@ -64,7 +64,7 @@ class TrafficControlTests(unittest.TestCase):
traffic_control._AddRootQdisc(config['interface'])
output = traffic_control._Exec(command)
- # Assert htb root is added
+ # Assert htb root is added.
self.assertTrue(root_detail in output)
def testConfigureClassAdd(self):
@@ -75,7 +75,7 @@ class TrafficControlTests(unittest.TestCase):
'server_port': 33333,
'bandwidth': 2000
}
- # Convert Kbps to Kbit
+ # Convert Kbps to Kbit.
rate = config['bandwidth'] * 8
class_detail = ('class htb 1:%x root prio 0 rate %dKbit ceil %dKbit' %
(config['port'], rate, rate))
@@ -91,15 +91,15 @@ class TrafficControlTests(unittest.TestCase):
# Add class to root.
traffic_control._ConfigureClass('add', config)
- # Assert class is added
+ # Assert class is added.
command = ['tc', 'class', 'ls', 'dev', config['interface']]
output = traffic_control._Exec(command)
self.assertTrue(class_detail in output)
- # Delete class
+ # Delete class.
traffic_control._ConfigureClass('del', config)
- # Assert class is deleted
+ # Assert class is deleted.
command = ['tc', 'class', 'ls', 'dev', config['interface']]
output = traffic_control._Exec(command)
self.assertFalse(class_detail in output)
@@ -129,15 +129,47 @@ class TrafficControlTests(unittest.TestCase):
handle_id_re = re.search(qdisc_re_detail, output)
self.assertEqual(handle_id_re, None)
- # Add qdisc to class
+ # Add qdisc to class.
traffic_control._AddSubQdisc(config)
- # Assert qdisc is added
+ # Assert qdisc is added.
command = ['tc', 'qdisc', 'ls', 'dev', config['interface']]
output = traffic_control._Exec(command)
handle_id_re = re.search(qdisc_re_detail, output)
self.assertNotEqual(handle_id_re, None)
+ def testAddDeleteFilter(self):
+ config = {
+ 'interface': self._INTERFACE,
+ 'port': 12345,
+ 'bandwidth': 2000
+ }
+ # Assert no filter exists.
+ command = ['tc', 'filter', 'list', 'dev', config['interface'], 'parent',
+ '1:0']
+ output = traffic_control._Exec(command)
+ self.assertEqual(output, '')
+
+ # Create the root and class to which the filter will be attached.
+ # Add root qdisc.
+ traffic_control._AddRootQdisc(config['interface'])
+
+ # Add class to root.
+ traffic_control._ConfigureClass('add', config)
+
+ # Add the filter.
+ traffic_control._AddFilter(config['interface'], config['port'])
+ handle_id = traffic_control._GetFilterHandleId(config['interface'],
+ config['port'])
+ self.assertNotEqual(handle_id, None)
+
+ # Delete the filter.
+ # The output of tc filter list is not None because tc adds default filters.
+ traffic_control._DeleteFilter(config['interface'], config['port'])
+ self.assertRaises(traffic_control.TrafficControlError,
+ traffic_control._GetFilterHandleId, config['interface'],
+ config['port'])
+
if __name__ == '__main__':
unittest.main()