blob: 8bfb55132b2ac24a1615b279ab2caa80b99ff2b2 [file] [log] [blame]
Damjan Marionf56b77a2016-10-03 19:44:57 +02001#!/usr/bin/env python
2## @package framework
3# Module to handle test case execution.
4#
5# The module provides a set of tools for constructing and running tests and
6# representing the results.
7
8import logging
9logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
10
11import os
12import subprocess
13import unittest
14from inspect import getdoc
15
16from scapy.utils import wrpcap, rdpcap
17from scapy.packet import Raw
18
19## Static variables to store color formatting strings.
20#
21# These variables (RED, GREEN, YELLOW and LPURPLE) are used to configure
22# the color of the text to be printed in the terminal. Variable END is used
23# to revert the text color to the default one.
24RED = '\033[91m'
25GREEN = '\033[92m'
26YELLOW = '\033[93m'
27LPURPLE = '\033[94m'
28END = '\033[0m'
29
30## Private class to create packet info object.
31#
32# Help process information about the next packet.
33# Set variables to default values.
34class _PacketInfo(object):
35 index = -1
36 src = -1
37 dst = -1
38 data = None
39 ## @var index
40 # Integer variable to store the index of the packet.
41 ## @var src
42 # Integer variable to store the index of the source packet generator
43 # interface of the packet.
44 ## @var dst
45 # Integer variable to store the index of the destination packet generator
46 # interface of the packet.
47 ## @var data
48 # Object variable to store the copy of the former packet.
49
50## Subclass of the python unittest.TestCase class.
51#
52# This subclass is a base class for test cases that are implemented as classes.
53# It provides methods to create and run test case.
54class VppTestCase(unittest.TestCase):
55
56 ## Class method to set class constants necessary to run test case.
57 # @param cls The class pointer.
58 @classmethod
59 def setUpConstants(cls):
60 cls.vpp_bin = os.getenv('VPP_TEST_BIN', "vpp")
Pierre Pfistercd8e3182016-10-07 16:30:03 +010061 cls.plugin_path = os.getenv('VPP_TEST_PLUGIN_PATH')
Damjan Marionf56b77a2016-10-03 19:44:57 +020062 cls.vpp_api_test_bin = os.getenv("VPP_TEST_API_TEST_BIN",
63 "vpp-api-test")
64 cls.vpp_cmdline = [cls.vpp_bin, "unix", "nodaemon", "api-segment", "{",
65 "prefix", "unittest", "}"]
Pierre Pfistercd8e3182016-10-07 16:30:03 +010066 if cls.plugin_path is not None:
67 cls.vpp_cmdline.extend(["plugin_path", cls.plugin_path])
68
Damjan Marionf56b77a2016-10-03 19:44:57 +020069 cls.vpp_api_test_cmdline = [cls.vpp_api_test_bin, "chroot", "prefix",
70 "unittest"]
71 try:
72 cls.verbose = int(os.getenv("V", 0))
73 except:
74 cls.verbose = 0
75
76 ## @var vpp_bin
77 # String variable to store the path to vpp (vector packet processor).
78 ## @var vpp_api_test_bin
79 # String variable to store the path to vpp_api_test (vpp API test tool).
80 ## @var vpp_cmdline
81 # List of command line attributes for vpp.
82 ## @var vpp_api_test_cmdline
83 # List of command line attributes for vpp_api_test.
84 ## @var verbose
85 # Integer variable to store required verbosity level.
86
87 ## Class method to start the test case.
88 # 1. Initiate test case constants and set test case variables to default
89 # values.
90 # 2. Remove files from the shared memory.
91 # 3. Start vpp as a subprocess.
92 # @param cls The class pointer.
93 @classmethod
94 def setUpClass(cls):
95 cls.setUpConstants()
96 cls.pg_streams = []
97 cls.MY_MACS = {}
98 cls.MY_IP4S = {}
99 cls.MY_IP6S = {}
100 cls.VPP_MACS = {}
101 cls.VPP_IP4S = {}
102 cls.VPP_IP6S = {}
103 cls.packet_infos = {}
104 print "=================================================================="
105 print YELLOW + getdoc(cls) + END
106 print "=================================================================="
107 os.system("rm -f /dev/shm/unittest-global_vm")
108 os.system("rm -f /dev/shm/unittest-vpe-api")
109 os.system("rm -f /dev/shm/unittest-db")
110 cls.vpp = subprocess.Popen(cls.vpp_cmdline, stderr=subprocess.PIPE)
111 ## @var pg_streams
112 # List variable to store packet-generator streams for interfaces.
113 ## @var MY_MACS
114 # Dictionary variable to store host MAC addresses connected to packet
115 # generator interfaces.
116 ## @var MY_IP4S
117 # Dictionary variable to store host IPv4 addresses connected to packet
118 # generator interfaces.
119 ## @var MY_IP6S
120 # Dictionary variable to store host IPv6 addresses connected to packet
121 # generator interfaces.
122 ## @var VPP_MACS
123 # Dictionary variable to store VPP MAC addresses of the packet
124 # generator interfaces.
125 ## @var VPP_IP4S
126 # Dictionary variable to store VPP IPv4 addresses of the packet
127 # generator interfaces.
128 ## @var VPP_IP6S
129 # Dictionary variable to store VPP IPv6 addresses of the packet
130 # generator interfaces.
131 ## @var vpp
132 # Test case object variable to store file descriptor of running vpp
133 # subprocess with open pipe to the standard error stream per
134 # VppTestCase object.
135
136 ## Class method to do cleaning when all tests (test_) defined for
137 # VppTestCase class are finished.
138 # 1. Terminate vpp and kill all vpp instances.
139 # 2. Remove files from the shared memory.
140 # @param cls The class pointer.
141 @classmethod
142 def quit(cls):
143 cls.vpp.terminate()
144 cls.vpp = None
145 os.system("rm -f /dev/shm/unittest-global_vm")
146 os.system("rm -f /dev/shm/unittest-vpe-api")
147 os.system("rm -f /dev/shm/unittest-db")
148
149 ## Class method to define tear down action of the VppTestCase class.
150 # @param cls The class pointer.
151 @classmethod
152 def tearDownClass(cls):
153 cls.quit()
154
155 ## Method to define tear down VPP actions of the test case.
156 # @param self The object pointer.
157 def tearDown(self):
158 self.cli(2, "show int")
159 self.cli(2, "show trace")
160 self.cli(2, "show hardware")
161 self.cli(2, "show ip arp")
162 self.cli(2, "show ip fib")
163 self.cli(2, "show error")
164 self.cli(2, "show run")
165
166 ## Method to define setup action of the test case.
167 # @param self The object pointer.
168 def setUp(self):
169 self.cli(2, "clear trace")
170
171 ## Class method to print logs.
172 # Based on set level of verbosity print text in the terminal.
173 # @param cls The class pointer.
174 # @param s String variable to store text to be printed.
175 # @param v Integer variable to store required level of verbosity.
176 @classmethod
177 def log(cls, s, v=1):
178 if cls.verbose >= v:
179 print "LOG: " + LPURPLE + s + END
180
181 ## Class method to execute api commands.
182 # Based on set level of verbosity print the output of the api command in
183 # the terminal.
184 # @param cls The class pointer.
185 # @param s String variable to store api command string.
186 @classmethod
187 def api(cls, s):
188 p = subprocess.Popen(cls.vpp_api_test_cmdline,
189 stdout=subprocess.PIPE,
190 stdin=subprocess.PIPE,
191 stderr=subprocess.PIPE)
192 if cls.verbose > 0:
193 print "API: " + RED + s + END
194 p.stdin.write(s)
195 out = p.communicate()[0]
196 out = out.replace("vat# ", "", 2)
197 if cls.verbose > 0:
198 if len(out) > 1:
199 print YELLOW + out + END
200 ## @var p
201 # Object variable to store file descriptor of vpp_api_test subprocess
202 # with open pipes to the standard output, inputs and error streams.
203 ## @var out
204 # Tuple variable to store standard output of vpp_api_test subprocess
205 # where the string "vat# " is replaced by empty string later.
206
207 ## Class method to execute cli commands.
208 # Based on set level of verbosity of the log and verbosity defined by
209 # environmental variable execute the cli command and print the output in
210 # the terminal.
211 # CLI command is executed via vpp API test tool (exec + cli_command)
212 # @param cls The class pointer.
213 # @param v Integer variable to store required level of verbosity.
214 # @param s String variable to store cli command string.
215 @classmethod
216 def cli(cls, v, s):
217 if cls.verbose < v:
218 return
219 p = subprocess.Popen(cls.vpp_api_test_cmdline,
220 stdout=subprocess.PIPE,
221 stdin=subprocess.PIPE,
222 stderr=subprocess.PIPE)
223 if cls.verbose > 0:
224 print "CLI: " + RED + s + END
225 p.stdin.write('exec ' + s)
226 out = p.communicate()[0]
227 out = out.replace("vat# ", "", 2)
228 if cls.verbose > 0:
229 if len(out) > 1:
230 print YELLOW + out + END
231 ## @var p
232 # Object variable to store file descriptor of vpp_api_test subprocess
233 # with open pipes to the standard output, inputs and error streams.
234 ## @var out
235 # Tuple variable to store standard output of vpp_api_test subprocess
236 # where the string "vat# " is replaced by empty string later.
237
238 ## Class method to create incoming packet stream for the packet-generator
239 # interface.
240 # Delete old /tmp/pgX_in.pcap file if exists and create the empty one and
241 # fill it with provided packets and add it to pg_streams list.
242 # @param cls The class pointer.
243 # @param i Integer variable to store the index of the packet-generator
244 # interface to create packet stream for.
245 # @param pkts List variable to store packets to be added to the stream.
246 @classmethod
247 def pg_add_stream(cls, i, pkts):
Klement Sekerab80f9d12016-10-05 14:05:33 +0200248 os.system("rm -f /tmp/pg%u_in.pcap" % i)
Damjan Marionf56b77a2016-10-03 19:44:57 +0200249 wrpcap("/tmp/pg%u_in.pcap" % i, pkts)
250 # no equivalent API command
251 cls.cli(0, "packet-generator new pcap /tmp/pg%u_in.pcap source pg%u"
252 " name pcap%u" % (i, i, i))
253 cls.pg_streams.append('pcap%u' % i)
254
255 ## Class method to enable packet capturing for the packet-generator
256 # interface.
257 # Delete old /tmp/pgX_out.pcap file if exists and set the packet-generator
258 # to capture outgoing packets to /tmp/pgX_out.pcap file.
259 # @param cls The class pointer.
260 # @param args List variable to store the indexes of the packet-generator
261 # interfaces to start packet capturing for.
262 @classmethod
263 def pg_enable_capture(cls, args):
264 for i in args:
Klement Sekerab80f9d12016-10-05 14:05:33 +0200265 os.system("rm -f /tmp/pg%u_out.pcap" % i)
Damjan Marionf56b77a2016-10-03 19:44:57 +0200266 cls.cli(0, "packet-generator capture pg%u pcap /tmp/pg%u_out.pcap"
267 % (i, i))
268
269 ## Class method to start packet sending.
270 # Start to send packets for all defined pg streams. Delete every stream
271 # from the stream list when sent and clear the pg_streams list.
272 # @param cls The class pointer.
273 @classmethod
274 def pg_start(cls):
275 cls.cli(2, "trace add pg-input 50") # 50 is maximum
276 cls.cli(0, 'packet-generator enable')
277 for stream in cls.pg_streams:
278 cls.cli(0, 'packet-generator delete %s' % stream)
279 cls.pg_streams = []
280
281 ## Class method to return captured packets.
282 # Return packet captured for the defined packet-generator interface. Open
283 # the corresponding pcap file (/tmp/pgX_out.pcap), read the content and
284 # store captured packets to output variable.
285 # @param cls The class pointer.
286 # @param o Integer variable to store the index of the packet-generator
287 # interface.
288 # @return output List of packets captured on the defined packet-generator
289 # interface. If the corresponding pcap file (/tmp/pgX_out.pcap) does not
290 # exist return empty list.
291 @classmethod
292 def pg_get_capture(cls, o):
293 pcap_filename = "/tmp/pg%u_out.pcap" % o
294 try:
295 output = rdpcap(pcap_filename)
296 except IOError: # TODO
297 cls.log("WARNING: File %s does not exist, probably because no"
298 " packets arrived" % pcap_filename)
299 return []
300 return output
301 ## @var pcap_filename
302 # File descriptor to the corresponding pcap file.
303
304 ## Class method to create packet-generator interfaces.
305 # Create packet-generator interfaces and add host MAC addresses connected
306 # to these packet-generator interfaces to the MY_MACS dictionary.
307 # @param cls The class pointer.
308 # @param args List variable to store the indexes of the packet-generator
309 # interfaces to be created.
310 @classmethod
311 def create_interfaces(cls, args):
312 for i in args:
313 cls.MY_MACS[i] = "02:00:00:00:ff:%02x" % i
314 cls.log("My MAC address is %s" % (cls.MY_MACS[i]))
315 cls.api("pg_create_interface if_id %u" % i)
316 cls.api("sw_interface_set_flags pg%u admin-up" % i)
317
318 ## Static method to extend packet to specified size
319 # Extend provided packet to the specified size (including Ethernet FCS).
320 # The packet is extended by adding corresponding number of spaces to the
321 # packet payload.
322 # NOTE: Currently works only when Raw layer is present.
323 # @param packet Variable to store packet object.
324 # @param size Integer variable to store the required size of the packet.
325 @staticmethod
326 def extend_packet(packet, size):
327 packet_len = len(packet) + 4
328 extend = size - packet_len
329 if extend > 0:
330 packet[Raw].load += ' ' * extend
331 ## @var packet_len
332 # Integer variable to store the current packet length including
333 # Ethernet FCS.
334 ## @var extend
335 # Integer variable to store the size of the packet extension.
336
337 ## Method to add packet info object to the packet_infos list.
338 # Extend the existing packet_infos list with the given information from
339 # the packet.
340 # @param self The object pointer.
341 # @param info Object to store required information from the packet.
342 def add_packet_info_to_list(self, info):
343 info.index = len(self.packet_infos)
344 self.packet_infos[info.index] = info
345 ## @var info.index
346 # Info object attribute to store the packet order in the stream.
347 ## @var packet_infos
348 # List variable to store required information from packets.
349
350 ## Method to create packet info object.
351 # Create the existing packet_infos list with the given information from
352 # the packet.
353 # @param self The object pointer.
354 # @param pg_id Integer variable to store the index of the packet-generator
355 # interface.
356 def create_packet_info(self, pg_id, target_id):
357 info = _PacketInfo()
358 self.add_packet_info_to_list(info)
359 info.src = pg_id
360 info.dst = target_id
361 return info
362 ## @var info
363 # Object to store required information from packet.
364 ## @var info.src
365 # Info object attribute to store the index of the source packet
366 # generator interface of the packet.
367 ## @var info.dst
368 # Info object attribute to store the index of the destination packet
369 # generator interface of the packet.
370
371 ## Static method to return packet info string.
372 # Create packet info string from the provided info object that will be put
373 # to the packet payload.
374 # @param info Object to store required information from the packet.
375 # @return String of information about packet's order in the stream, source
376 # and destination packet generator interface.
377 @staticmethod
378 def info_to_payload(info):
379 return "%d %d %d" % (info.index, info.src, info.dst)
380
381 ## Static method to create packet info object from the packet payload.
382 # Create packet info object and set its attribute values based on data
383 # gained from the packet payload.
384 # @param payload String variable to store packet payload.
385 # @return info Object to store required information about the packet.
386 @staticmethod
387 def payload_to_info(payload):
388 numbers = payload.split()
389 info = _PacketInfo()
390 info.index = int(numbers[0])
391 info.src = int(numbers[1])
392 info.dst = int(numbers[2])
393 return info
394 ## @var info.index
395 # Info object attribute to store the packet order in the stream.
396 ## @var info.src
397 # Info object attribute to store the index of the source packet
398 # generator interface of the packet.
399 ## @var info.dst
400 # Info object attribute to store the index of the destination packet
401 # generator interface of the packet.
402
403 ## Method to return packet info object of the next packet in
404 # the packet_infos list.
405 # Get the next packet info object from the packet_infos list by increasing
406 # the packet_infos list index by one.
407 # @param self The object pointer.
408 # @param info Object to store required information about the packet.
409 # @return packet_infos[next_index] Next info object from the packet_infos
410 # list with stored information about packets. Return None if the end of
411 # the list is reached.
412 def get_next_packet_info(self, info):
413 if info is None:
414 next_index = 0
415 else:
416 next_index = info.index + 1
417 if next_index == len(self.packet_infos):
418 return None
419 else:
420 return self.packet_infos[next_index]
421 ## @var next_index
422 # Integer variable to store the index of the next info object.
423
424 ## Method to return packet info object of the next packet with the required
425 # source packet generator interface.
426 # Iterate over the packet_infos list and search for the next packet info
427 # object with the required source packet generator interface.
428 # @param self The object pointer.
429 # @param src_pg Integer variable to store index of requested source packet
430 # generator interface.
431 # @param info Object to store required information about the packet.
432 # @return packet_infos[next_index] Next info object from the packet_infos
433 # list with stored information about packets. Return None if the end of
434 # the list is reached.
435 def get_next_packet_info_for_interface(self, src_pg, info):
436 while True:
437 info = self.get_next_packet_info(info)
438 if info is None:
439 return None
440 if info.src == src_pg:
441 return info
442 ## @var info.src
443 # Info object attribute to store the index of the source packet
444 # generator interface of the packet.
445
446 ## Method to return packet info object of the next packet with required
447 # source and destination packet generator interfaces.
448 # Search for the next packet info object with the required source and
449 # destination packet generator interfaces.
450 # @param self The object pointer.
451 # @param src_pg Integer variable to store the index of the requested source
452 # packet generator interface.
453 # @param dst_pg Integer variable to store the index of the requested source
454 # packet generator interface.
455 # @param info Object to store required information about the packet.
456 # @return info Object with the info about the next packet with with
457 # required source and destination packet generator interfaces. Return None
458 # if there is no other packet with required data.
459 def get_next_packet_info_for_interface2(self, src_pg, dst_pg, info):
460 while True:
461 info = self.get_next_packet_info_for_interface(src_pg, info)
462 if info is None:
463 return None
464 if info.dst == dst_pg:
465 return info
466 ## @var info.dst
467 # Info object attribute to store the index of the destination packet
468 # generator interface of the packet.
469
470
471## Subclass of the python unittest.TestResult class.
472#
473# This subclass provides methods to compile information about which tests have
474# succeeded and which have failed.
475class VppTestResult(unittest.TestResult):
476 ## The constructor.
477 # @param stream File descriptor to store where to report test results. Set
478 # to the standard error stream by default.
479 # @param descriptions Boolean variable to store information if to use test
480 # case descriptions.
481 # @param verbosity Integer variable to store required verbosity level.
482 def __init__(self, stream, descriptions, verbosity):
483 unittest.TestResult.__init__(self, stream, descriptions, verbosity)
484 self.stream = stream
485 self.descriptions = descriptions
486 self.verbosity = verbosity
487 self.result_string = None
488 ## @var result_string
489 # String variable to store the test case result string.
490
491
492 ## Method called when the test case succeeds.
493 # Run the default implementation (that does nothing) and set the result
494 # string in case of test case success.
495 # @param self The object pointer.
496 # @param test Object variable to store the test case instance.
497 def addSuccess(self, test):
498 unittest.TestResult.addSuccess(self, test)
499 self.result_string = GREEN + "OK" + END
500 ## @var result_string
501 # String variable to store the test case result string.
502
503 ## Method called when the test case signals a failure.
504 # Run the default implementation that appends a tuple (test, formatted_err)
505 # to the instance's failures attribute, where formatted_err is a formatted
506 # traceback derived from err and set the result string in case of test case
507 # success.
508 # @param self The object pointer.
509 # @param test Object variable to store the test case instance.
510 # @param err Tuple variable to store the error data:
511 # (type, value, traceback).
512 def addFailure(self, test, err):
513 unittest.TestResult.addFailure(self, test, err)
514 self.result_string = RED + "FAIL" + END
515 ## @var result_string
516 # String variable to store the test case result string.
517
518 ## Method called when the test case raises an unexpected exception.
519 # Run the default implementation that appends a tuple (test, formatted_err)
520 # to the instance's error attribute, where formatted_err is a formatted
521 # traceback derived from err and set the result string in case of test case
522 # unexpected failure.
523 # @param self The object pointer.
524 # @param test Object variable to store the test case instance.
525 # @param err Tuple variable to store the error data:
526 # (type, value, traceback).
527 def addError(self, test, err):
528 unittest.TestResult.addError(self, test, err)
529 self.result_string = RED + "ERROR" + END
530 ## @var result_string
531 # String variable to store the test case result string.
532
533 ## Method to get the description of the test case.
534 # Used to get the description string from the test case object.
535 # @param self The object pointer.
536 # @param test Object variable to store the test case instance.
537 # @return String of the short description if exist otherwise return test
538 # case name string.
539 def getDescription(self, test):
540 # TODO: if none print warning not raise exception
541 short_description = test.shortDescription()
542 if self.descriptions and short_description:
543 return short_description
544 else:
545 return str(test)
546 ## @var short_description
547 # String variable to store the short description of the test case.
548
549 ## Method called when the test case is about to be run.
550 # Run the default implementation and based on the set verbosity level write
551 # the starting string to the output stream.
552 # @param self The object pointer.
553 # @param test Object variable to store the test case instance.
554 def startTest(self, test):
555 unittest.TestResult.startTest(self, test)
556 if self.verbosity > 0:
557 self.stream.writeln("Starting " + self.getDescription(test) + " ...")
558 self.stream.writeln("------------------------------------------------------------------")
559
560 ## Method called after the test case has been executed.
561 # Run the default implementation and based on the set verbosity level write
562 # the result string to the output stream.
563 # @param self The object pointer.
564 # @param test Object variable to store the test case instance.
565 def stopTest(self, test):
566 unittest.TestResult.stopTest(self, test)
567 if self.verbosity > 0:
568 self.stream.writeln("------------------------------------------------------------------")
569 self.stream.writeln("%-60s%s" % (self.getDescription(test), self.result_string))
570 self.stream.writeln("------------------------------------------------------------------")
571 else:
572 self.stream.writeln("%-60s%s" % (self.getDescription(test), self.result_string))
573
574 ## Method to write errors and failures information to the output stream.
575 # Write content of errors and failures lists to the output stream.
576 # @param self The object pointer.
577 def printErrors(self):
578 self.stream.writeln()
579 self.printErrorList('ERROR', self.errors)
580 self.printErrorList('FAIL', self.failures)
581 ## @var errors
582 # List variable containing 2-tuples of TestCase instances and strings
583 # holding formatted tracebacks. Each tuple represents a test which
584 # raised an unexpected exception.
585 ## @var failures
586 # List variable containing 2-tuples of TestCase instances and strings
587 # holding formatted tracebacks. Each tuple represents a test where
588 # a failure was explicitly signalled using the TestCase.assert*()
589 # methods.
590
591 ## Method to write the error information to the output stream.
592 # Write content of error lists to the output stream together with error
593 # type and test case description.
594 # @param self The object pointer.
595 # @param flavour String variable to store error type.
596 # @param errors List variable to store 2-tuples of TestCase instances and
597 # strings holding formatted tracebacks.
598 def printErrorList(self, flavour, errors):
599 for test, err in errors:
600 self.stream.writeln('=' * 70)
601 self.stream.writeln("%s: %s" % (flavour, self.getDescription(test)))
602 self.stream.writeln('-' * 70)
603 self.stream.writeln("%s" % err)
604 ## @var test
605 # Object variable to store the test case instance.
606 ## @var err
607 # String variable to store formatted tracebacks.
608
609
610## Subclass of the python unittest.TextTestRunner class.
611#
612# A basic test runner implementation which prints results on standard error.
613class VppTestRunner(unittest.TextTestRunner):
614 ## Class object variable to store the results of a set of tests.
615 resultclass = VppTestResult
616
617 ## Method to run the test.
618 # Print debug message in the terminal and run the standard run() method
619 # of the test runner collecting the result into the test result object.
620 # @param self The object pointer.
621 # @param test Object variable to store the test case instance.
622 # @return Test result object of the VppTestRunner.
623 def run(self, test):
624 print "Running tests using custom test runner" # debug message
625 return super(VppTestRunner, self).run(test)