test: Improve unit tests to verify basic functionality

Also a more complete ping test that parses all output to the CLI.

These tests expand the hifi-specific ones to not just cover whether
a topology can be created with options, but whether those options
are properly implemented within some tolerance, like CPU limits,
link bandwidth, delays, and even drops.
This commit is contained in:
Brandon Heller
2012-11-14 07:57:17 -08:00
parent fcd01592e1
commit 1f1d590c7a
3 changed files with 158 additions and 16 deletions
+8
View File
@@ -148,6 +148,14 @@ class CLI( Cmd ):
"Ping between first two hosts, useful for testing." "Ping between first two hosts, useful for testing."
self.mn.pingPair() self.mn.pingPair()
def do_pingallfull( self, _line ):
"Ping between first two hosts, returns all ping results."
self.mn.pingAllFull()
def do_pingpairfull( self, _line ):
"Ping between first two hosts, returns all ping results."
self.mn.pingPairFull()
def do_iperf( self, line ): def do_iperf( self, line ):
"Simple iperf TCP test between two (optionally specified) hosts." "Simple iperf TCP test between two (optionally specified) hosts."
args = line.split() args = line.split()
+72 -2
View File
@@ -428,9 +428,10 @@ class Mininet( object ):
sent, received = int( m.group( 1 ) ), int( m.group( 2 ) ) sent, received = int( m.group( 1 ) ), int( m.group( 2 ) )
return sent, received return sent, received
def ping( self, hosts=None ): def ping( self, hosts=None, timeout=None ):
"""Ping between all specified hosts. """Ping between all specified hosts.
hosts: list of hosts hosts: list of hosts
timeout: time to wait for a response, as string
returns: ploss packet loss percentage""" returns: ploss packet loss percentage"""
# should we check if running? # should we check if running?
packets = 0 packets = 0
@@ -443,7 +444,10 @@ class Mininet( object ):
output( '%s -> ' % node.name ) output( '%s -> ' % node.name )
for dest in hosts: for dest in hosts:
if node != dest: if node != dest:
result = node.cmd( 'ping -c1 ' + dest.IP() ) opts = ''
if timeout:
opts = '-W %s' % timeout
result = node.cmd( 'ping -c1 %s %s' % (opts, dest.IP()) )
sent, received = self._parsePing( result ) sent, received = self._parsePing( result )
packets += sent packets += sent
if received > sent: if received > sent:
@@ -459,6 +463,61 @@ class Mininet( object ):
( ploss, lost, packets ) ) ( ploss, lost, packets ) )
return ploss return ploss
@staticmethod
def _parsePingFull( pingOutput ):
"Parse ping output and return all data."
# Check for downed link
if 'connect: Network is unreachable' in pingOutput:
return (1, 0)
r = r'(\d+) packets transmitted, (\d+) received'
m = re.search( r, pingOutput )
if m is None:
error( '*** Error: could not parse ping output: %s\n' %
pingOutput )
return (1, 0, 0, 0, 0, 0)
sent, received = int( m.group( 1 ) ), int( m.group( 2 ) )
r = r'rtt min/avg/max/mdev = '
r += r'(\d+\.\d+)/(\d+\.\d+)/(\d+\.\d+)/(\d+\.\d+) ms'
m = re.search( r, pingOutput )
rttmin = float( m.group( 1 ) )
rttavg = float( m.group( 2 ) )
rttmax = float( m.group( 3 ) )
rttdev = float( m.group( 4 ) )
return sent, received, rttmin, rttavg, rttmax, rttdev
def pingFull( self, hosts=None, timeout=None ):
"""Ping between all specified hosts and return all data.
hosts: list of hosts
timeout: time to wait for a response, as string
returns: all ping data; see function body."""
# should we check if running?
# Each value is a tuple: (src, dsd, [all ping outputs])
all_outputs = []
if not hosts:
hosts = self.hosts
output( '*** Ping: testing ping reachability\n' )
for node in hosts:
output( '%s -> ' % node.name )
for dest in hosts:
if node != dest:
opts = ''
if timeout:
opts = '-W %s' % timeout
result = node.cmd( 'ping -c1 %s %s' % (opts, dest.IP()) )
outputs = self._parsePingFull( result )
sent, received, rttmin, rttavg, rttmax, rttdev = outputs
all_outputs.append( (node, dest, outputs) )
output( ( '%s ' % dest.name ) if received else 'X ' )
output( '\n' )
output( "*** Results: \n" )
for outputs in all_outputs:
src, dest, ping_outputs = outputs
sent, received, rttmin, rttavg, rttmax, rttdev = ping_outputs
output( " %s->%s: %s/%s, " % (src, dest, sent, received ) )
output( "rtt min/avg/max/mdev %0.3f/%0.3f/%0.3f/%0.3f ms\n" %
(rttmin, rttavg, rttmax, rttdev) )
return all_outputs
def pingAll( self ): def pingAll( self ):
"""Ping between all hosts. """Ping between all hosts.
returns: ploss packet loss percentage""" returns: ploss packet loss percentage"""
@@ -470,6 +529,17 @@ class Mininet( object ):
hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ] hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ]
return self.ping( hosts=hosts ) return self.ping( hosts=hosts )
def pingAllFull( self ):
"""Ping between all hosts.
returns: ploss packet loss percentage"""
return self.pingFull()
def pingPairFull( self ):
"""Ping between first two hosts, useful for testing.
returns: ploss packet loss percentage"""
hosts = [ self.hosts[ 0 ], self.hosts[ 1 ] ]
return self.pingFull( hosts=hosts )
@staticmethod @staticmethod
def _parseIperf( iperfOutput ): def _parseIperf( iperfOutput ):
"""Parse iperf output and return bandwidth. """Parse iperf output and return bandwidth.
+78 -14
View File
@@ -7,6 +7,8 @@ import unittest
from mininet.net import Mininet from mininet.net import Mininet
from mininet.node import OVSKernelSwitch from mininet.node import OVSKernelSwitch
from mininet.node import CPULimitedHost
from mininet.link import TCLink
from mininet.topo import Topo from mininet.topo import Topo
from mininet.log import setLogLevel from mininet.log import setLogLevel
@@ -18,7 +20,11 @@ N = 2
class SingleSwitchOptionsTopo(Topo): class SingleSwitchOptionsTopo(Topo):
"Single switch connected to n hosts." "Single switch connected to n hosts."
def __init__(self, n=2, hopts={}, lopts={}): def __init__(self, n=2, hopts=None, lopts=None):
if not hopts:
hopts = {}
if not lopts:
lopts = {}
Topo.__init__(self, hopts=hopts, lopts=lopts) Topo.__init__(self, hopts=hopts, lopts=lopts)
switch = self.addSwitch('s1') switch = self.addSwitch('s1')
for h in range(n): for h in range(n):
@@ -31,31 +37,89 @@ class testOptionsTopo( unittest.TestCase ):
def runOptionsTopoTest( self, n, hopts=None, lopts=None ): def runOptionsTopoTest( self, n, hopts=None, lopts=None ):
"Generic topology-with-options test runner." "Generic topology-with-options test runner."
mn = Mininet( SingleSwitchOptionsTopo( n=n, hopts=hopts ) ) mn = Mininet( topo=SingleSwitchOptionsTopo( n=n, hopts=hopts,
lopts=lopts ),
host=CPULimitedHost, link=TCLink )
dropped = mn.run( mn.ping ) dropped = mn.run( mn.ping )
self.assertEqual( dropped, 0 ) self.assertEqual( dropped, 0 )
def assertWithinTolerance(self, measured, expected, tolerance_frac):
"""Check that a given value is within a tolerance of expected
tolerance_frac: less-than-1.0 value; 0.8 would yield 20% tolerance.
"""
self.assertTrue( float(measured) >= float(expected) * tolerance_frac )
self.assertTrue( float(measured) >= float(expected) * tolerance_frac )
def testCPULimits( self ): def testCPULimits( self ):
hopts = { 'cpu': 0.5 / N } "Verify topology creation with CPU limits set for both schedulers."
self.runOptionsTopoTest(N, hopts=hopts) CPU_FRACTION = 0.1
CPU_TOLERANCE = 0.8 # CPU fraction below which test should fail
hopts = { 'cpu': CPU_FRACTION }
#self.runOptionsTopoTest( N, hopts=hopts )
mn = Mininet( SingleSwitchOptionsTopo( n=N, hopts=hopts ),
host=CPULimitedHost )
mn.start()
results = mn.runCpuLimitTest( cpu=CPU_FRACTION )
mn.stop()
for cpu in results:
self.assertWithinTolerance( cpu, CPU_FRACTION, CPU_TOLERANCE )
def testLinkBandwidth( self ): def testLinkBandwidth( self ):
lopts = { 'bw': 10, 'use_htb': True } "Verify that link bandwidths are accurate within a bound."
self.runOptionsTopoTest(N, lopts=lopts) BW = 5 # Mbps
BW_TOLERANCE = 0.8 # BW fraction below which test should fail
# Verify ability to create limited-link topo first;
lopts = { 'bw': BW, 'use_htb': True }
# Also verify correctness of limit limitng within a bound.
mn = Mininet( SingleSwitchOptionsTopo( n=N, lopts=lopts ),
link=TCLink )
bw_strs = mn.run( mn.iperf )
for bw_str in bw_strs:
bw = float( bw_str.split(' ')[0] )
self.assertWithinTolerance( bw, BW, BW_TOLERANCE )
def testLinkDelay( self ): def testLinkDelay( self ):
lopts = { 'delay': '5ms', 'use_htb': True } "Verify that link delays are accurate within a bound."
self.runOptionsTopoTest(N, lopts=lopts) DELAY_MS = 15
DELAY_TOLERANCE = 0.8 # Delay fraction below which test should fail
lopts = { 'delay': '%sms' % DELAY_MS, 'use_htb': True }
mn = Mininet( SingleSwitchOptionsTopo( n=N, lopts=lopts ),
link=TCLink )
ping_delays = mn.run( mn.pingFull )
test_outputs = ping_delays[0]
# Ignore unused variables below
# pylint: disable-msg=W0612
node, dest, ping_outputs = test_outputs
sent, received, rttmin, rttavg, rttmax, rttdev = ping_outputs
self.assertEqual( sent, received )
# pylint: enable-msg=W0612
for rttval in [rttmin, rttavg, rttmax]:
# Multiply delay by 4 to cover there & back on two links
self.assertWithinTolerance( rttval, DELAY_MS * 4.0,
DELAY_TOLERANCE)
def testLinkLoss( self ): def testLinkLoss( self ):
lopts = { 'loss': 10, 'use_htb': True } "Verify that we see packet drops with a high configured loss rate."
self.runOptionsTopoTest(N, lopts=lopts) LOSS_PERCENT = 99
REPS = 1
lopts = { 'loss': LOSS_PERCENT, 'use_htb': True }
mn = Mininet( topo=SingleSwitchOptionsTopo( n=N, lopts=lopts ),
host=CPULimitedHost, link=TCLink )
# Drops are probabilistic, but the chance of no dropped packets is
# 1 in 100 million with 4 hops for a link w/99% loss.
dropped_total = 0
mn.start()
for _ in range(REPS):
dropped_total += mn.ping(timeout='1')
mn.stop()
self.assertTrue(dropped_total > 0)
def testAllOptions( self ): def testMostOptions( self ):
lopts = { 'bw': 10, 'delay': '5ms', 'loss': 10, 'use_htb': True } "Verify topology creation with most link options and CPU limits."
lopts = { 'bw': 10, 'delay': '5ms', 'use_htb': True }
hopts = { 'cpu': 0.5 / N } hopts = { 'cpu': 0.5 / N }
self.runOptionsTopoTest(N, hopts=hopts, lopts=lopts) self.runOptionsTopoTest( N, hopts=hopts, lopts=lopts )
if __name__ == '__main__': if __name__ == '__main__':