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:
@@ -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
@@ -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
@@ -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__':
|
||||||
|
|||||||
Reference in New Issue
Block a user