-# Copyright (c) 2017 Cisco and/or its affiliates.
+# Copyright (c) 2018 Cisco and/or its affiliates.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
import re
import pandas as pd
import logging
+import xml.etree.ElementTree as ET
from robot.api import ExecutionResult, ResultVisitor
+from robot import errors
from collections import OrderedDict
from string import replace
+from os import remove
+
+from input_data_files import download_and_unzip_data_file
class ExecutionChecker(ResultVisitor):
REGEX_TOLERANCE = re.compile(r'^[\D\d]*LOSS_ACCEPTANCE:\s(\d*\.\d*)\s'
r'[\D\d]*')
- REGEX_VERSION = re.compile(r"(stdout: 'vat# vat# Version:)(\s*)(.*)")
+ REGEX_VERSION = re.compile(r"(return STDOUT Version:\s*)(.*)")
REGEX_TCP = re.compile(r'Total\s(rps|cps|throughput):\s([0-9]*).*$')
- def __init__(self, **metadata):
+ REGEX_MRR = re.compile(r'MaxReceivedRate_Results\s\[pkts/(\d*)sec\]:\s'
+ r'tx\s(\d*),\srx\s(\d*)')
+
+ def __init__(self, metadata):
"""Initialisation.
:param metadata: Key-value pairs to be included in "metadata" part of
self.parse_msg = {
"setup-version": self._get_version,
"teardown-vat-history": self._get_vat_history,
- "teardown-show-runtime": self._get_show_run
+ "test-show-runtime": self._get_show_run
}
@property
:returns: Nothing.
"""
- if msg.message.count("stdout: 'vat# vat# Version:"):
+ if msg.message.count("return STDOUT Version:"):
self._version = str(re.search(self.REGEX_VERSION, msg.message).
- group(3))
+ group(2))
self._data["metadata"]["version"] = self._version
self._msg_type = None
- logging.debug(" VPP version: {0}".format(self._version))
-
def _get_vat_history(self, msg):
"""Called when extraction of VAT command history is required.
self._data["suites"][suite.longname.lower().replace('"', "'").
replace(" ", "_")] = {
- "name": suite.name.lower(),
- "doc": doc_str,
- "parent": parent_name,
- "level": len(suite.longname.split("."))
- }
+ "name": suite.name.lower(),
+ "doc": doc_str,
+ "parent": parent_name,
+ "level": len(suite.longname.split("."))
+ }
suite.keywords.visit(self)
test_result["tags"] = tags
doc_str = test.doc.replace('"', "'").replace('\n', ' '). \
replace('\r', '').replace('[', ' |br| [')
- test_result["doc"] = replace(doc_str, ' |br| [', '[', maxreplace=1)
+ test_result["doc"] = replace(doc_str, ' |br| [', '[', maxreplace=1)
test_result["msg"] = test.message.replace('\n', ' |br| '). \
replace('\r', '').replace('"', "'")
- if test.status == "PASS" and ("NDRPDRDISC" in tags or "TCP" in tags):
-
+ if test.status == "PASS" and ("NDRPDRDISC" in tags or
+ "TCP" in tags or
+ "MRR" in tags):
if "NDRDISC" in tags:
test_type = "NDR"
elif "PDRDISC" in tags:
test_type = "PDR"
- elif "TCP" in tags: # Change to wrk?
+ elif "TCP" in tags:
test_type = "TCP"
+ elif "MRR" in tags:
+ test_type = "MRR"
else:
return
test_result["result"] = dict()
test_result["result"]["value"] = int(groups.group(2))
test_result["result"]["unit"] = groups.group(1)
+ elif test_type in ("MRR", ):
+ groups = re.search(self.REGEX_MRR, test.message)
+ test_result["result"] = dict()
+ test_result["result"]["duration"] = int(groups.group(1))
+ test_result["result"]["tx"] = int(groups.group(2))
+ test_result["result"]["rx"] = int(groups.group(3))
+ test_result["result"]["throughput"] = int(
+ test_result["result"]["rx"] /
+ test_result["result"]["duration"])
else:
test_result["status"] = test.status
elif keyword.type == "teardown":
self._lookup_kw_nr = 0
self.visit_teardown_kw(keyword)
+ else:
+ self._lookup_kw_nr = 0
+ self.visit_test_kw(keyword)
except AttributeError:
pass
"""
pass
+ def visit_test_kw(self, test_kw):
+ """Implements traversing through the test keyword and its child
+ keywords.
+
+ :param test_kw: Keyword to process.
+ :type test_kw: Keyword
+ :returns: Nothing.
+ """
+ for keyword in test_kw.keywords:
+ if self.start_test_kw(keyword) is not False:
+ self.visit_test_kw(keyword)
+ self.end_test_kw(keyword)
+
+ def start_test_kw(self, test_kw):
+ """Called when test keyword starts. Default implementation does
+ nothing.
+
+ :param test_kw: Keyword to process.
+ :type test_kw: Keyword
+ :returns: Nothing.
+ """
+ if test_kw.name.count("Show Runtime Counters On All Duts"):
+ self._lookup_kw_nr += 1
+ self._show_run_lookup_nr = 0
+ self._msg_type = "test-show-runtime"
+ test_kw.messages.visit(self)
+
+ def end_test_kw(self, test_kw):
+ """Called when keyword ends. Default implementation does nothing.
+
+ :param test_kw: Keyword to process.
+ :type test_kw: Keyword
+ :returns: Nothing.
+ """
+ pass
+
def visit_setup_kw(self, setup_kw):
"""Implements traversing through the teardown keyword and its child
keywords.
:type setup_kw: Keyword
:returns: Nothing.
"""
- if setup_kw.name.count("Vpp Show Version Verbose") \
+ if setup_kw.name.count("Show Vpp Version On All Duts") \
and not self._version:
self._msg_type = "setup-version"
setup_kw.messages.visit(self)
if teardown_kw.name.count("Show Vat History On All Duts"):
self._vat_history_lookup_nr = 0
self._msg_type = "teardown-vat-history"
- elif teardown_kw.name.count("Show Statistics On All Duts"):
- self._lookup_kw_nr += 1
- self._show_run_lookup_nr = 0
- self._msg_type = "teardown-show-runtime"
-
- if self._msg_type:
teardown_kw.messages.visit(self)
def end_teardown_kw(self, teardown_kw):
return self.data[job][build]["tests"]
@staticmethod
- def _parse_tests(job, build):
+ def _parse_tests(job, build, get_timestamp=False):
"""Process data from robot output.xml file and return JSON structured
data.
:param job: The name of job which build output data will be processed.
:param build: The build which output data will be processed.
+ :param get_timestamp: If True, timestamp is read form the xml source
+ file.
:type job: str
:type build: dict
+ :type get_timestamp: bool
:returns: JSON data structure.
:rtype: dict
"""
+ metadata = {
+ "job": job,
+ "build": build
+ }
+ if get_timestamp:
+ tree = ET.parse(build["file-name"])
+ root = tree.getroot()
+ metadata["generated"] = root.attrib["generated"]
+
with open(build["file-name"], 'r') as data_file:
- result = ExecutionResult(data_file)
- checker = ExecutionChecker(job=job, build=build)
+ try:
+ result = ExecutionResult(data_file)
+ except errors.DataError as err:
+ logging.error("Error occurred while parsing output.xml: {0}".
+ format(err))
+ return None
+ checker = ExecutionChecker(metadata)
result.visit(checker)
return checker.data
- def read_data(self):
- """Parse input data from input files and store in pandas' Series.
+ def download_and_parse_data(self, get_timestamp=False):
+ """Download the input data files, parse input data from input files and
+ store in pandas' Series.
+
+ :param get_timestamp: If True, timestamp is read form the xml source
+ file.
+ :type get_timestamp: bool
"""
- logging.info("Parsing input files ...")
+ logging.info("Downloading and parsing input files ...")
job_data = dict()
for job, builds in self._cfg.builds.items():
- logging.info(" Extracting data from the job '{0}' ...'".
+ logging.info(" Processing data from the job '{0}' ...'".
format(job))
builds_data = dict()
for build in builds:
- if build["status"] == "failed" \
- or build["status"] == "not found":
+ logging.info(" Processing the build '{0}'".
+ format(build["build"]))
+ self._cfg.set_input_state(job, build["build"], "failed")
+ if not download_and_unzip_data_file(self._cfg, job, build):
+ logging.error("It is not possible to download the input "
+ "data file from the job '{job}', build "
+ "'{build}', or it is damaged. Skipped.".
+ format(job=job, build=build["build"]))
continue
- logging.info(" Extracting data from the build '{0}'".
+
+ logging.info(" Processing data from the build '{0}' ...".
format(build["build"]))
- logging.info(" Processing the file '{0}'".
- format(build["file-name"]))
- data = InputData._parse_tests(job, build)
+ data = InputData._parse_tests(job, build,
+ get_timestamp=get_timestamp)
+ if data is None:
+ logging.error("Input data file from the job '{job}', build "
+ "'{build}' is damaged. Skipped.".
+ format(job=job, build=build["build"]))
+ continue
+
+ self._cfg.set_input_state(job, build["build"], "processed")
+
+ try:
+ remove(build["file-name"])
+ except OSError as err:
+ logging.error("Cannot remove the file '{0}': {1}".
+ format(build["file-name"], err))
build_data = pd.Series({
"metadata": pd.Series(data["metadata"].values(),
"suites": pd.Series(data["suites"].values(),
index=data["suites"].keys()),
"tests": pd.Series(data["tests"].values(),
- index=data["tests"].keys()),
- })
+ index=data["tests"].keys())})
builds_data[str(build["build"])] = build_data
+ build["status"] = "processed"
logging.info(" Done.")
job_data[job] = pd.Series(builds_data.values(),
index += 1
tag_filter = tag_filter[:index] + " in tags" + tag_filter[index:]
- def filter_data(self, element, params=None, data_set="tests"):
+ def filter_data(self, element, params=None, data_set="tests",
+ continue_on_error=False):
"""Filter required data from the given jobs and builds.
The output data structure is:
all parameters are included.
:param data_set: The set of data to be filtered: tests, suites,
metadata.
+ :param continue_on_error: Continue if there is error while reading the
+ data. The Item will be empty then
:type element: pandas.Series
:type params: list
:type data_set: str
+ :type continue_on_error: bool
:returns: Filtered data.
:rtype pandas.Series
"""
logging.info(" Creating the data set for the {0} '{1}'.".
- format(element["type"], element.get("title", "")))
+ format(element.get("type", ""), element.get("title", "")))
try:
if element["filter"] in ("all", "template"):
data[job] = pd.Series()
for build in builds:
data[job][str(build)] = pd.Series()
- for test_ID, test_data in \
- self.data[job][str(build)][data_set].iteritems():
+ try:
+ data_iter = self.data[job][str(build)][data_set].\
+ iteritems()
+ except KeyError:
+ if continue_on_error:
+ continue
+ else:
+ return None
+ for test_ID, test_data in data_iter:
if eval(cond, {"tags": test_data.get("tags", "")}):
data[job][str(build)][test_ID] = pd.Series()
if params is None:
except (KeyError, IndexError, ValueError) as err:
logging.error(" Missing mandatory parameter in the element "
- "specification.", err)
+ "specification: {0}".format(err))
return None
except AttributeError:
return None