diff options
author | 2016-10-28 13:20:27 +0200 | |
---|---|---|
committer | 2016-10-31 23:48:25 +0000 | |
commit | 277b89c946e6fdc764ee48726fcd3df1c189eda9 (patch) | |
tree | 1ced5e4a47a00290251e89929df0945ae64370c8 /test | |
parent | cede3798b7f3c35cab2a8268c584fd22bf3a34f6 (diff) |
add vpp debugging support to test framework
improve test documentation
Change-Id: Ia9678aa2532ecb4cb33736aedb4a31aa3f2a3f93
Signed-off-by: Klement Sekera <ksekera@cisco.com>
Diffstat (limited to 'test')
-rw-r--r-- | test/Makefile | 50 | ||||
-rw-r--r-- | test/doc/Makefile | 188 | ||||
-rw-r--r-- | test/doc/conf.py | 1 | ||||
-rw-r--r-- | test/framework.py | 214 | ||||
-rw-r--r-- | test/hook.py | 112 | ||||
-rw-r--r-- | test/log.py | 71 | ||||
-rw-r--r-- | test/test_l2bd.py | 2 | ||||
-rw-r--r-- | test/test_l2xc.py | 12 | ||||
-rw-r--r-- | test/vpp_papi_provider.py | 19 |
9 files changed, 490 insertions, 179 deletions
diff --git a/test/Makefile b/test/Makefile index c90679e8..63cee203 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,22 +1,58 @@ -PYTHON_VENV_PATH=$(PYTHON_PATH)/virtualenv +.PHONY: verify-python-path -test: clean +verify-python-path: +ifndef VPP_PYTHON_PREFIX + $(error VPP_PYTHON_PREFIX is not set) +endif + +PYTHON_VENV_PATH=$(VPP_PYTHON_PREFIX)/virtualenv + +test: wipe verify-python-path @virtualenv $(PYTHON_VENV_PATH) @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install scapy" @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install pexpect" @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && cd $(WS_ROOT)/vpp-api/python && python setup.py install" @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python run_tests.py discover -p test_$(TEST)\"*.py\"" -retest: clean +retest: wipe verify-python-path @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && python run_tests.py discover -p test_$(TEST)\"*.py\"" -.PHONY: clean doc +.PHONY: wipe doc -clean: +wipe: verify-python-path @rm -f /dev/shm/vpp-unittest-* @rm -rf /tmp/vpp-unittest-* -doc: +doc: verify-python-path @virtualenv $(PYTHON_VENV_PATH) @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && pip install sphinx" - @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && make -C doc html" + @bash -c "source $(PYTHON_VENV_PATH)/bin/activate && make -C doc WS_ROOT=$(WS_ROOT) BR=$(BR) NO_VPP_PAPI=1 html" + +wipe-doc: + @make -C doc wipe BR=$(BR) + +help: + @echo "Running tests:" + @echo "" + @echo " test - build and run functional tests" + @echo " test-debug - build and run functional tests (debug build)" + @echo " retest - run functional tests" + @echo " retest-debug - run functional tests (debug build)" + @echo " wipe-test - wipe (temporary) files generated by unit tests" + @echo "" + @echo "Arguments controlling test runs:" + @echo " V=[0|1|2] - set test verbosity level" + @echo " DEBUG=<type> - set VPP debugging kind" + @echo " DEBUG=core - detect coredump and load it in gdb on crash" + @echo " DEBUG=gdb - allow easy debugging by printing VPP PID " + @echo " and waiting for user input before running " + @echo " and tearing down a testcase" + @echo " DEBUG=gdbserver - run gdb inside a gdb server, otherwise " + @echo " same as above" + @echo " STEP=[yes|no] - ease debugging by stepping through a testcase " + @echo " TEST=<name> - only run specific test" + @echo "" + @echo "Creating test documentation" + @echo " test-doc - generate documentation for test framework" + @echo " wipe-test-doc - wipe documentation for test framework" + @echo "" diff --git a/test/doc/Makefile b/test/doc/Makefile index 00655389..809abef8 100644 --- a/test/doc/Makefile +++ b/test/doc/Makefile @@ -3,20 +3,34 @@ # You can set these variables from the command line. SPHINXOPTS = -SPHINXBUILD = sphinx-build +SRC_DOC_DIR = $(WS_ROOT)/test/doc +SPHINXBUILD = sphinx-build PAPER = -BUILDDIR = _build +BUILD_DOC_ROOT = $(BR)/test-doc +BUILD_DOC_DIR = $(BUILD_DOC_ROOT)/build +API_DOC_GEN_DIR = $(BUILD_DOC_ROOT)/apidoc # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +ALLSPHINXOPTS = -d $(BUILD_DOC_DIR)/.sphinx-cache $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(API_DOC_GEN_DIR) -c $(SRC_DOC_DIR) # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +INDEX_REL_PATH:=$(shell realpath --relative-to=$(API_DOC_GEN_DIR) $(SRC_DOC_DIR)/index.rst) +IN_VENV:=$(shell if pip -V | grep "virtualenv" 2>&1 > /dev/null; then echo 1; else echo 0; fi) + +.PHONY: verify-virtualenv +verify-virtualenv: +ifeq ($(IN_VENV),0) + $(error "Not running inside virtualenv (are you running 'make test-doc' from root?)") +endif .PHONY: regen-api-doc -regen-api-doc: - sphinx-apidoc -o . .. +regen-api-doc: verify-virtualenv + @mkdir -p $(API_DOC_GEN_DIR) + #@echo ".. include:: $(INDEX_REL_PATH)" > $(API_DOC_GEN_DIR)/index.rst + @cp $(SRC_DOC_DIR)/index.rst $(API_DOC_GEN_DIR) + sphinx-apidoc -o $(API_DOC_GEN_DIR) .. .PHONY: help help: @@ -48,182 +62,182 @@ help: @echo " coverage to run coverage check of the documentation (if enabled)" @echo " dummy to check syntax errors of document sources" -.PHONY: clean -clean: - rm -rf $(BUILDDIR)/* +.PHONY: wipe +wipe: + rm -rf $(BUILD_DOC_ROOT) .PHONY: html -html: regen-api-doc - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html +html: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/html @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + @echo "Build finished. The HTML pages are in $(BUILD_DOC_DIR)/html." .PHONY: dirhtml -dirhtml: regen-api-doc - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml +dirhtml: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/dirhtml @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + @echo "Build finished. The HTML pages are in $(BUILD_DOC_DIR)/dirhtml." .PHONY: singlehtml -singlehtml: regen-api-doc - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml +singlehtml: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/singlehtml @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + @echo "Build finished. The HTML page is in $(BUILD_DOC_DIR)/singlehtml." .PHONY: pickle -pickle: regen-api-doc - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle +pickle: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json -json: regen-api-doc - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json +json: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp -htmlhelp: regen-api-doc - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp +htmlhelp: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." + ".hhp project file in $(BUILD_DOC_DIR)/htmlhelp." .PHONY: qthelp -qthelp: regen-api-doc - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp +qthelp: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/VPPtestframework.qhcp" + ".qhcp project file in $(BUILD_DOC_DIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILD_DOC_DIR)/qthelp/VPPtestframework.qhcp" @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/VPPtestframework.qhc" + @echo "# assistant -collectionFile $(BUILD_DOC_DIR)/qthelp/VPPtestframework.qhc" .PHONY: applehelp -applehelp: regen-api-doc - $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp +applehelp: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/applehelp @echo - @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "Build finished. The help book is in $(BUILD_DOC_DIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp -devhelp: regen-api-doc - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp +devhelp: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/VPPtestframework" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/VPPtestframework" + @echo "# ln -s $(BUILD_DOC_DIR)/devhelp $$HOME/.local/share/devhelp/VPPtestframework" @echo "# devhelp" .PHONY: epub -epub: regen-api-doc - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub +epub: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/epub @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + @echo "Build finished. The epub file is in $(BUILD_DOC_DIR)/epub." .PHONY: epub3 epub3: - $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/epub3 @echo - @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + @echo "Build finished. The epub3 file is in $(BUILD_DOC_DIR)/epub3." .PHONY: latex -latex: regen-api-doc - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex +latex: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/latex @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Build finished; the LaTeX files are in $(BUILD_DOC_DIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf -latexpdf: regen-api-doc - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex +latexpdf: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/latex @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + $(MAKE) -C $(BUILD_DOC_DIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILD_DOC_DIR)/latex." .PHONY: latexpdfja -latexpdfja: regen-api-doc - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex +latexpdfja: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + $(MAKE) -C $(BUILD_DOC_DIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILD_DOC_DIR)/latex." .PHONY: text -text: regen-api-doc - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text +text: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/text @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." + @echo "Build finished. The text files are in $(BUILD_DOC_DIR)/text." .PHONY: man -man: regen-api-doc - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man +man: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/man @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + @echo "Build finished. The manual pages are in $(BUILD_DOC_DIR)/man." .PHONY: texinfo -texinfo: regen-api-doc - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo +texinfo: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/texinfo @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Build finished. The Texinfo files are in $(BUILD_DOC_DIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info -info: regen-api-doc - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo +info: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/texinfo @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + make -C $(BUILD_DOC_DIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILD_DOC_DIR)/texinfo." .PHONY: gettext -gettext: regen-api-doc - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale +gettext: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILD_DOC_DIR)/locale @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + @echo "Build finished. The message catalogs are in $(BUILD_DOC_DIR)/locale." .PHONY: changes -changes: regen-api-doc - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes +changes: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/changes @echo - @echo "The overview file is in $(BUILDDIR)/changes." + @echo "The overview file is in $(BUILD_DOC_DIR)/changes." .PHONY: linkcheck -linkcheck: regen-api-doc - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck +linkcheck: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." + "or in $(BUILD_DOC_DIR)/linkcheck/output.txt." .PHONY: doctest -doctest: regen-api-doc - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest +doctest: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." + "results in $(BUILD_DOC_DIR)/doctest/output.txt." .PHONY: coverage -coverage: regen-api-doc - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage +coverage: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." + "results in $(BUILD_DOC_DIR)/coverage/python.txt." .PHONY: xml -xml: regen-api-doc - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml +xml: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/xml @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + @echo "Build finished. The XML files are in $(BUILD_DOC_DIR)/xml." .PHONY: pseudoxml -pseudoxml: regen-api-doc - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml +pseudoxml: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/pseudoxml @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + @echo "Build finished. The pseudo-XML files are in $(BUILD_DOC_DIR)/pseudoxml." .PHONY: dummy -dummy: regen-api-doc - $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy +dummy: regen-api-doc verify-virtualenv + $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILD_DOC_DIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." diff --git a/test/doc/conf.py b/test/doc/conf.py index aa841714..cec9ddee 100644 --- a/test/doc/conf.py +++ b/test/doc/conf.py @@ -20,6 +20,7 @@ import os import sys sys.path.insert(0, os.path.abspath('..')) + # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. diff --git a/test/framework.py b/test/framework.py index 02ffb7ad..5f75e010 100644 --- a/test/framework.py +++ b/test/framework.py @@ -2,20 +2,20 @@ from abc import * import os -import sys import subprocess import unittest import tempfile +import time import resource from time import sleep +from Queue import Queue +from threading import Thread from inspect import getdoc -from hook import PollHook +from hook import StepHook, PollHook from vpp_pg_interface import VppPGInterface from vpp_papi_provider import VppPapiProvider - from scapy.packet import Raw - -from logging import * +from log import * """ Test framework module. @@ -24,41 +24,6 @@ from logging import * representing the results. """ -handler = StreamHandler(sys.stdout) -getLogger().addHandler(handler) -try: - verbose = int(os.getenv("V", 0)) -except: - verbose = 0 -# 40 = ERROR, 30 = WARNING, 20 = INFO, 10 = DEBUG, 0 = NOTSET (all messages) -getLogger().setLevel(40 - 10 * verbose) -getLogger("scapy.runtime").addHandler(handler) -getLogger("scapy.runtime").setLevel(ERROR) - -# Static variables to store color formatting strings. -# -# These variables (RED, GREEN, YELLOW and LPURPLE) are used to configure -# the color of the text to be printed in the terminal. Variable COLOR_RESET -# is used to revert the text color to the default one. -if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty(): - RED = '\033[91m' - GREEN = '\033[92m' - YELLOW = '\033[93m' - LPURPLE = '\033[94m' - COLOR_RESET = '\033[0m' -else: - RED = '' - GREEN = '' - YELLOW = '' - LPURPLE = '' - COLOR_RESET = '' - - -""" @var formatting delimiter consisting of '=' characters """ -double_line_delim = '=' * 70 -""" @var formatting delimiter consisting of '-' characters """ -single_line_delim = '-' * 70 - class _PacketInfo(object): """Private class to create packet info object. @@ -84,6 +49,11 @@ class _PacketInfo(object): data = None +def pump_output(out, queue): + for line in iter(out.readline, b''): + queue.put(line) + + class VppTestCase(unittest.TestCase): """ Subclass of the python unittest.TestCase class. @@ -108,23 +78,85 @@ class VppTestCase(unittest.TestCase): return cls.test_instance @classmethod + def set_debug_flags(cls, d): + cls.debug_core = False + cls.debug_gdb = False + cls.debug_gdbserver = False + if d is None: + return + dl = d.lower() + if dl == "core": + if resource.getrlimit(resource.RLIMIT_CORE)[0] <= 0: + # give a heads up if this is actually useless + cls.logger.critical("WARNING: core size limit is set 0, core " + "files will NOT be created") + cls.debug_core = True + elif dl == "gdb": + cls.debug_gdb = True + elif dl == "gdbserver": + cls.debug_gdbserver = True + else: + raise Exception("Unrecognized DEBUG option: '%s'" % d) + + @classmethod def setUpConstants(cls): """ Set-up the test case class based on environment variables """ try: - cls.interactive = True if int(os.getenv("I")) > 0 else False + s = os.getenv("STEP") + cls.step = True if s.lower() in ("y", "yes", "1") else False except: - cls.interactive = False - if cls.interactive and resource.getrlimit(resource.RLIMIT_CORE)[0] <= 0: - # give a heads up if this is actually useless - critical("WARNING: core size limit is set 0, core files will NOT " - "be created") + cls.step = False + try: + d = os.getenv("DEBUG") + except: + d = None + cls.set_debug_flags(d) cls.vpp_bin = os.getenv('VPP_TEST_BIN', "vpp") cls.plugin_path = os.getenv('VPP_TEST_PLUGIN_PATH') cls.vpp_cmdline = [cls.vpp_bin, "unix", "nodaemon", "api-segment", "{", "prefix", cls.shm_prefix, "}"] if cls.plugin_path is not None: cls.vpp_cmdline.extend(["plugin_path", cls.plugin_path]) - info("vpp_cmdline: %s" % cls.vpp_cmdline) + cls.logger.info("vpp_cmdline: %s" % cls.vpp_cmdline) + + @classmethod + def wait_for_enter(cls): + if cls.debug_gdbserver: + print(double_line_delim) + print("Spawned GDB server with PID: %d" % cls.vpp.pid) + elif cls.debug_gdb: + print(double_line_delim) + print("Spawned VPP with PID: %d" % cls.vpp.pid) + else: + cls.logger.debug("Spawned VPP with PID: %d" % cls.vpp.pid) + return + print(single_line_delim) + print("You can debug the VPP using e.g.:") + if cls.debug_gdbserver: + print("gdb " + cls.vpp_bin + " -ex 'target remote localhost:7777'") + print("Now is the time to attach a gdb by running the above " + "command, set up breakpoints etc. and then resume VPP from " + "within gdb by issuing the 'continue' command") + elif cls.debug_gdb: + print("gdb " + cls.vpp_bin + " -ex 'attach %s'" % cls.vpp.pid) + print("Now is the time to attach a gdb by running the above " + "command and set up breakpoints etc.") + print(single_line_delim) + raw_input("Press ENTER to continue running the testcase...") + + @classmethod + def run_vpp(cls): + cmdline = cls.vpp_cmdline + + if cls.debug_gdbserver: + cmdline = ['gdbserver', 'localhost:7777'] + cls.vpp_cmdline + cls.logger.info("Gdbserver cmdline is %s", " ".join(cmdline)) + + cls.vpp = subprocess.Popen(cmdline, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=1) + cls.wait_for_enter() @classmethod def setUpClass(cls): @@ -132,37 +164,66 @@ class VppTestCase(unittest.TestCase): Perform class setup before running the testcase Remove shared memory files, start vpp and connect the vpp-api """ + cls.logger = getLogger(cls.__name__) cls.tempdir = tempfile.mkdtemp( prefix='vpp-unittest-' + cls.__name__ + '-') cls.shm_prefix = cls.tempdir.split("/")[-1] os.chdir(cls.tempdir) - info("Temporary dir is %s, shm prefix is %s", - cls.tempdir, cls.shm_prefix) + cls.logger.info("Temporary dir is %s, shm prefix is %s", + cls.tempdir, cls.shm_prefix) cls.setUpConstants() cls.pg_streams = [] cls.packet_infos = {} cls.verbose = 0 print(double_line_delim) - print(YELLOW + getdoc(cls) + COLOR_RESET) + print(colorize(getdoc(cls), YELLOW)) print(double_line_delim) # need to catch exceptions here because if we raise, then the cleanup # doesn't get called and we might end with a zombie vpp try: - cls.vpp = subprocess.Popen(cls.vpp_cmdline, stderr=subprocess.PIPE) - debug("Spawned VPP with PID: %d" % cls.vpp.pid) + cls.run_vpp() cls.vpp_dead = False cls.vapi = VppPapiProvider(cls.shm_prefix, cls.shm_prefix) - cls.vapi.register_hook(PollHook(cls)) - cls.vapi.connect() + if cls.step: + cls.vapi.register_hook(StepHook(cls)) + else: + cls.vapi.register_hook(PollHook(cls)) + time.sleep(0.1) + try: + cls.vapi.connect() + except: + if cls.debug_gdbserver: + print(colorize("You're running VPP inside gdbserver but " + "VPP-API connection failed, did you forget " + "to 'continue' VPP from within gdb?", RED)) + raise + cls.vpp_stdout_queue = Queue() + cls.vpp_stdout_reader_thread = Thread( + target=pump_output, args=(cls.vpp.stdout, cls.vpp_stdout_queue)) + cls.vpp_stdout_reader_thread.start() + cls.vpp_stderr_queue = Queue() + cls.vpp_stderr_reader_thread = Thread( + target=pump_output, args=(cls.vpp.stderr, cls.vpp_stderr_queue)) + cls.vpp_stderr_reader_thread.start() except: cls.vpp.terminate() del cls.vpp + raise @classmethod def quit(cls): """ Disconnect vpp-api, kill vpp and cleanup shared memory files """ + if (cls.debug_gdbserver or cls.debug_gdb) and hasattr(cls, 'vpp'): + cls.vpp.poll() + if cls.vpp.returncode is None: + print(double_line_delim) + print("VPP or GDB server is still running") + print(single_line_delim) + raw_input("When done debugging, press ENTER to kill the process" + " and finish running the testcase...") + if hasattr(cls, 'vpp'): cls.vapi.disconnect() cls.vpp.poll() @@ -170,6 +231,29 @@ class VppTestCase(unittest.TestCase): cls.vpp.terminate() del cls.vpp + if hasattr(cls, 'vpp_stdout_queue'): + cls.logger.info(single_line_delim) + cls.logger.info('VPP output to stdout while running %s:', + cls.__name__) + cls.logger.info(single_line_delim) + f = open(cls.tempdir + '/vpp_stdout.txt', 'w') + while not cls.vpp_stdout_queue.empty(): + line = cls.vpp_stdout_queue.get_nowait() + f.write(line) + cls.logger.info('VPP stdout: %s' % line.rstrip('\n')) + + if hasattr(cls, 'vpp_stderr_queue'): + cls.logger.info(single_line_delim) + cls.logger.info('VPP output to stderr while running %s:', + cls.__name__) + cls.logger.info(single_line_delim) + f = open(cls.tempdir + '/vpp_stderr.txt', 'w') + while not cls.vpp_stderr_queue.empty(): + line = cls.vpp_stderr_queue.get_nowait() + f.write(line) + cls.logger.info('VPP stderr: %s' % line.rstrip('\n')) + cls.logger.info(single_line_delim) + @classmethod def tearDownClass(cls): """ Perform final cleanup after running all tests in this test-case """ @@ -178,11 +262,11 @@ class VppTestCase(unittest.TestCase): def tearDown(self): """ Show various debug prints after each test """ if not self.vpp_dead: - info(self.vapi.cli("show int")) - info(self.vapi.cli("show trace")) - info(self.vapi.cli("show hardware")) - info(self.vapi.cli("show error")) - info(self.vapi.cli("show run")) + self.logger.info(self.vapi.cli("show int")) + self.logger.info(self.vapi.cli("show trace")) + self.logger.info(self.vapi.cli("show hardware")) + self.logger.info(self.vapi.cli("show error")) + self.logger.info(self.vapi.cli("show run")) def setUp(self): """ Clear trace before running each test""" @@ -392,7 +476,7 @@ class VppTestResult(unittest.TestResult): """ unittest.TestResult.addSuccess(self, test) - self.result_string = GREEN + "OK" + COLOR_RESET + self.result_string = colorize("OK", GREEN) def addSkip(self, test, reason): """ @@ -403,7 +487,7 @@ class VppTestResult(unittest.TestResult): """ unittest.TestResult.addSkip(self, test, reason) - self.result_string = YELLOW + "SKIP" + COLOR_RESET + self.result_string = colorize("SKIP", YELLOW) def addFailure(self, test, err): """ @@ -415,10 +499,10 @@ class VppTestResult(unittest.TestResult): """ unittest.TestResult.addFailure(self, test, err) if hasattr(test, 'tempdir'): - self.result_string = RED + "FAIL" + COLOR_RESET + \ + self.result_string = colorize("FAIL", RED) + \ ' [ temp dir used by test case: ' + test.tempdir + ' ]' else: - self.result_string = RED + "FAIL" + COLOR_RESET + ' [no temp dir]' + self.result_string = colorize("FAIL", RED) + ' [no temp dir]' def addError(self, test, err): """ @@ -430,10 +514,10 @@ class VppTestResult(unittest.TestResult): """ unittest.TestResult.addError(self, test, err) if hasattr(test, 'tempdir'): - self.result_string = RED + "ERROR" + COLOR_RESET + \ + self.result_string = colorize("ERROR", RED) + \ ' [ temp dir used by test case: ' + test.tempdir + ' ]' else: - self.result_string = RED + "ERROR" + COLOR_RESET + ' [no temp dir]' + self.result_string = colorize("ERROR", RED) + ' [no temp dir]' def getDescription(self, test): """ diff --git a/test/hook.py b/test/hook.py index 9489aa9d..3ae14737 100644 --- a/test/hook.py +++ b/test/hook.py @@ -1,7 +1,8 @@ import signal import os import pexpect -from logging import * +import traceback +from log import * class Hook(object): @@ -9,6 +10,9 @@ class Hook(object): Generic hooks before/after API/CLI calls """ + def __init__(self, logger): + self.logger = logger + def before_api(self, api_name, api_args): """ Function called before API call @@ -17,7 +21,8 @@ class Hook(object): @param api_name: name of the API @param api_args: tuple containing the API arguments """ - debug("API: %s (%s)" % (api_name, api_args)) + self.logger.debug("API: %s (%s)" % + (api_name, api_args), extra={'color': RED}) def after_api(self, api_name, api_args): """ @@ -35,7 +40,7 @@ class Hook(object): @param cli: CLI string """ - debug("CLI: %s" % (cli)) + self.logger.debug("CLI: %s" % (cli), extra={'color': RED}) def after_cli(self, cli): """ @@ -54,6 +59,7 @@ class PollHook(Hook): def __init__(self, testcase): self.vpp_dead = False self.testcase = testcase + self.logger = testcase.logger def spawn_gdb(self, gdb_path, core_path): gdb_cmdline = gdb_path + ' ' + self.testcase.vpp_bin + ' ' + core_path @@ -74,11 +80,12 @@ class PollHook(Hook): self.spawn_gdb(gdb_path, core_path) return else: - error("Debugger '%s' does not exist or is not an executable.." % - gdb_path) + self.logger.error( + "Debugger '%s' does not exist or is not an executable.." % + gdb_path) - critical('core file present, debug with: gdb ' + - self.testcase.vpp_bin + ' ' + core_path) + self.logger.critical('core file present, debug with: gdb ' + + self.testcase.vpp_bin + ' ' + core_path) def poll_vpp(self): """ @@ -97,7 +104,7 @@ class PollHook(Hook): msg = "VPP subprocess died unexpectedly with returncode %d [%s]" % ( self.testcase.vpp.returncode, signaldict[abs(self.testcase.vpp.returncode)]) - critical(msg) + self.logger.critical(msg) core_path = self.testcase.tempdir + '/core' if os.path.isfile(core_path): self.on_crash(core_path) @@ -126,3 +133,92 @@ class PollHook(Hook): """ super(PollHook, self).after_cli(cli) self.poll_vpp() + + +class StepHook(PollHook): + """ Hook which requires user to press ENTER before doing any API/CLI """ + + def __init__(self, testcase): + self.skip_stack = None + self.skip_num = None + self.skip_count = 0 + super(StepHook, self).__init__(testcase) + + def skip(self): + if self.skip_stack is None: + return False + stack = traceback.extract_stack() + counter = 0 + skip = True + for e in stack: + if counter > self.skip_num: + break + if e[0] != self.skip_stack[counter][0]: + skip = False + if e[1] != self.skip_stack[counter][1]: + skip = False + counter += 1 + if skip: + self.skip_count += 1 + return True + else: + print("%d API/CLI calls skipped in specified stack " + "frame" % self.skip_count) + self.skip_count = 0 + self.skip_stack = None + self.skip_num = None + return False + + def user_input(self): + print('number\tfunction\tfile\tcode') + counter = 0 + stack = traceback.extract_stack() + for e in stack: + print('%02d.\t%s\t%s:%d\t[%s]' % (counter, e[2], e[0], e[1], e[3])) + counter += 1 + print(single_line_delim) + print("You can enter a number of stack frame chosen from above") + print("Calls in/below that stack frame will be not be stepped anymore") + print(single_line_delim) + while True: + choice = raw_input("Enter your choice, if any, and press ENTER to " + "continue running the testcase...") + if choice == "": + choice = None + try: + if choice is not None: + num = int(choice) + except: + print("Invalid input") + continue + if choice is not None and (num < 0 or num >= len(stack)): + print("Invalid choice") + continue + break + if choice is not None: + self.skip_stack = stack + self.skip_num = num + + def before_cli(self, cli): + """ Wait for ENTER before executing CLI """ + if self.skip(): + print("Skip pause before executing CLI: %s" % cli) + else: + print(double_line_delim) + print("Test paused before executing CLI: %s" % cli) + print(single_line_delim) + self.user_input() + super(StepHook, self).before_cli(cli) + + def before_api(self, api_name, api_args): + """ Wait for ENTER before executing API """ + if self.skip(): + print("Skip pause before executing API: %s (%s)" + % (api_name, api_args)) + else: + print(double_line_delim) + print("Test paused before executing API: %s (%s)" + % (api_name, api_args)) + print(single_line_delim) + self.user_input() + super(StepHook, self).before_api(api_name, api_args) diff --git a/test/log.py b/test/log.py new file mode 100644 index 00000000..38610b7e --- /dev/null +++ b/test/log.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python + +import sys +import os +import logging + +""" @var formatting delimiter consisting of '=' characters """ +double_line_delim = '=' * 70 +""" @var formatting delimiter consisting of '-' characters """ +single_line_delim = '-' * 70 + + +def colorize(msg, color): + return color + msg + COLOR_RESET + + +class ColorFormatter(logging.Formatter): + + def init(self, fmt=None, datefmt=None): + super(ColorFormatter, self).__init__(fmt, datefmt) + + def format(self, record): + message = super(ColorFormatter, self).format(record) + if hasattr(record, 'color'): + message = colorize(message, record.color) + return message + +handler = logging.StreamHandler(sys.stdout) +handler.setFormatter(ColorFormatter()) + +global_logger = logging.getLogger() +global_logger.addHandler(handler) +try: + verbose = int(os.getenv("V", 0)) +except: + verbose = 0 + +# 40 = ERROR, 30 = WARNING, 20 = INFO, 10 = DEBUG, 0 = NOTSET (all messages) +if verbose >= 2: + log_level = 10 +elif verbose == 1: + log_level = 20 +else: + log_level = 40 + +scapy_logger = logging.getLogger("scapy.runtime") +scapy_logger.setLevel(logging.ERROR) + + +def getLogger(name): + logger = logging.getLogger(name) + logger.setLevel(log_level) + return logger + +# Static variables to store color formatting strings. +# +# These variables (RED, GREEN, YELLOW and LPURPLE) are used to configure +# the color of the text to be printed in the terminal. Variable COLOR_RESET +# is used to revert the text color to the default one. +if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty(): + RED = '\033[91m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + LPURPLE = '\033[94m' + COLOR_RESET = '\033[0m' +else: + RED = '' + GREEN = '' + YELLOW = '' + LPURPLE = '' + COLOR_RESET = '' diff --git a/test/test_l2bd.py b/test/test_l2bd.py index 0fced5d9..53e1ee05 100644 --- a/test/test_l2bd.py +++ b/test/test_l2bd.py @@ -189,7 +189,7 @@ class TestL2bd(VppTestCase): MAC learning enabled learn 100 MAC enries 3 interfaces: untagged, dot1q, dot1ad (dot1q used instead of dot1ad - in the first version) + in the first version) 2.sending l2 eth pkts between 3 interface 64B, 512B, 1518B, 9200B (ether_size) diff --git a/test/test_l2xc.py b/test/test_l2xc.py index d448a04d..35c7a818 100644 --- a/test/test_l2xc.py +++ b/test/test_l2xc.py @@ -78,7 +78,6 @@ class TestL2xc(VppTestCase): interfaces. Create host IPv4 address for every host MAC address too. :param count: Number of hosts to create MAC and IPv4 addresses for. - Type: int """ for pg_if in self.pg_interfaces: # self.MY_MACS[i.sw_if_index] = [] @@ -151,12 +150,11 @@ class TestL2xc(VppTestCase): """ L2XC test Test scenario: - 1.config - 2 pairs of 2 interfaces, l2xconnected - - 2.sending l2 eth packets between 4 interfaces - 64B, 512B, 1518B, 9018B (ether_size) - burst of packets per interface + 1. config + 2 pairs of 2 interfaces, l2xconnected + 2. sending l2 eth packets between 4 interfaces + 64B, 512B, 1518B, 9018B (ether_size) + burst of packets per interface """ # Create incoming packet streams for packet-generator interfaces diff --git a/test/vpp_papi_provider.py b/test/vpp_papi_provider.py index 454f3edc..261a0f4a 100644 --- a/test/vpp_papi_provider.py +++ b/test/vpp_papi_provider.py @@ -1,7 +1,19 @@ -import vpp_papi +import os from logging import error from hook import Hook +do_import = True +try: + no_vpp_papi = os.getenv("NO_VPP_PAPI") + if no_vpp_papi == "1": + do_import = False +except: + pass + +if do_import: + import vpp_papi + + # from vnet/vnet/mpls/mpls_types.h MPLS_IETF_MAX_LABEL = 0xfffff MPLS_LABEL_INVALID = MPLS_IETF_MAX_LABEL + 1 @@ -16,7 +28,7 @@ class VppPapiProvider(object): """ def __init__(self, name, shm_prefix): - self.hook = Hook() + self.hook = Hook("vpp-papi-provider") self.name = name self.shm_prefix = shm_prefix @@ -130,7 +142,6 @@ class VppPapiProvider(object): default_router, max_interval, min_interval, lifetime, initial_count, initial_interval, async)) - def vxlan_add_del_tunnel( self, src_addr, @@ -177,7 +188,7 @@ class VppPapiProvider(object): :param rx_sw_if_index: Software interface index of Rx interface. :param tx_sw_if_index: Software interface index of Tx interface. :param enable: Create cross-connect if equal to 1, delete cross-connect - if equal to 0. + if equal to 0. :type rx_sw_if_index: str or int :type rx_sw_if_index: str or int :type enable: int |