1 # Copyright (c) 2017 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
6 # http://www.apache.org/licenses/LICENSE-2.0
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
14 """Data pre-processing
16 - extract data from output.xml files generated by Jenkins jobs and store in
18 - provide access to the data.
25 from robot.api import ExecutionResult, ResultVisitor
27 from errors import PresentationError
30 class ExecutionChecker(ResultVisitor):
31 """Class to traverse through the test suite structure.
33 The functionality implemented in this class generates a json structure:
36 "metadata": { # Optional
37 "version": "VPP version",
38 "job": "Jenkins job name"
39 "build": "Information about the build"
43 "doc": "Suite 1 documentation"
46 "doc": "Suite N documentation"
52 "parent": "Name of the parent of the test",
53 "tags": ["tag 1", "tag 2", "tag n"],
54 "type": "PDR" | "NDR",
57 "unit": "pps" | "bps" | "percentage"
66 "50": { # Only for NDR
71 "10": { # Only for NDR
83 "50": { # Only for NDR
88 "10": { # Only for NDR
95 "lossTolerance": "lossTolerance", # Only for PDR
97 "DUT1": " DUT1 VAT History",
98 "DUT2": " DUT2 VAT History"
100 "show-run": "Show Run"
108 .. note:: ID is the lowercase full path to the test.
111 REGEX_RATE = re.compile(r'^[\D\d]*FINAL_RATE:\s(\d+\.\d+)\s(\w+)')
113 REGEX_LAT_NDR = re.compile(r'^[\D\d]*'
114 r'LAT_\d+%NDR:\s\[\'(-?\d+\/-?\d+/-?\d+)\','
115 r'\s\'(-?\d+/-?\d+/-?\d+)\'\]\s\n'
116 r'LAT_\d+%NDR:\s\[\'(-?\d+/-?\d+/-?\d+)\','
117 r'\s\'(-?\d+/-?\d+/-?\d+)\'\]\s\n'
118 r'LAT_\d+%NDR:\s\[\'(-?\d+/-?\d+/-?\d+)\','
119 r'\s\'(-?\d+/-?\d+/-?\d+)\'\]')
121 REGEX_LAT_PDR = re.compile(r'^[\D\d]*'
122 r'LAT_\d+%PDR:\s\[\'(-?\d+/-?\d+/-?\d+)\','
123 r'\s\'(-?\d+/-?\d+/-?\d+)\'\][\D\d]*')
125 REGEX_TOLERANCE = re.compile(r'^[\D\d]*LOSS_ACCEPTANCE:\s(\d*\.\d*)\s'
128 REGEX_VERSION = re.compile(r"(stdout: 'vat# vat# Version:)(\s*)(.*)")
130 def __init__(self, **metadata):
133 :param metadata: Key-value pairs to be included to "metadata" part of
138 # Type of message to parse out from the test messages
139 self._msg_type = None
144 # Number of VAT History messages found:
146 # 1 - VAT History of DUT1
147 # 2 - VAT History of DUT2
148 self._vat_history_lookup_nr = 0
150 # Number of Show Running messages found
152 # 1 - Show run message found
153 self._show_run_lookup_nr = 0
155 # Test ID of currently processed test- the lowercase full path to the
159 # The main data structure
169 # Save the provided metadata
170 for key, val in metadata.items():
171 self._data["metadata"][key] = val
173 # Dictionary defining the methods used to parse different types of
176 "setup-version": self._get_version,
177 "teardown-vat-history": self._get_vat_history,
178 "teardown-show-runtime": self._get_show_run
183 """Getter - Data parsed from the XML file.
185 :returns: Data parsed from the XML file.
190 def _get_version(self, msg):
191 """Called when extraction of VPP version is required.
193 :param msg: Message to process.
198 if msg.message.count("stdout: 'vat# vat# Version:"):
199 self._version = str(re.search(self.REGEX_VERSION, msg.message).
201 self._data["metadata"]["version"] = self._version
202 self._msg_type = None
204 logging.debug(" VPP version: {0}".format(self._version))
206 def _get_vat_history(self, msg):
207 """Called when extraction of VAT command history is required.
209 :param msg: Message to process.
213 if msg.message.count("VAT command history:"):
214 self._vat_history_lookup_nr += 1
215 text = re.sub("[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3} "
216 "VAT command history:", "", msg.message, count=1).\
219 if self._vat_history_lookup_nr == 1:
220 self._data["tests"][self._test_ID]["vat-history"] = dict()
221 self._data["tests"][self._test_ID]["vat-history"]["DUT1"] = text
222 elif self._vat_history_lookup_nr == 2:
223 self._data["tests"][self._test_ID]["vat-history"]["DUT2"] = text
224 self._msg_type = None
226 def _get_show_run(self, msg):
227 """Called when extraction of VPP operational data (output of CLI command
228 Show Runtime) is required.
230 :param msg: Message to process.
234 if msg.message.count("vat# Thread "):
235 self._show_run_lookup_nr += 1
236 text = msg.message.replace("vat# ", "").\
237 replace("return STDOUT ", "").replace('"', "'")
238 if self._show_run_lookup_nr == 1:
239 self._data["tests"][self._test_ID]["show-run"] = text
240 self._msg_type = None
242 def _get_latency(self, msg, test_type):
243 """Get the latency data from the test message.
245 :param msg: Message to be parsed.
246 :param test_type: Type of the test - NDR or PDR.
249 :returns: Latencies parsed from the message.
253 if test_type == "NDR":
254 groups = re.search(self.REGEX_LAT_NDR, msg)
255 groups_range = range(1, 7)
256 elif test_type == "PDR":
257 groups = re.search(self.REGEX_LAT_PDR, msg)
258 groups_range = range(1, 3)
263 for idx in groups_range:
265 lat = [int(item) for item in str(groups.group(idx)).split('/')]
266 except (AttributeError, ValueError):
268 latencies.append(lat)
270 keys = ("min", "avg", "max")
278 latency["direction1"]["100"] = dict(zip(keys, latencies[0]))
279 latency["direction2"]["100"] = dict(zip(keys, latencies[1]))
280 if test_type == "NDR":
281 latency["direction1"]["50"] = dict(zip(keys, latencies[2]))
282 latency["direction2"]["50"] = dict(zip(keys, latencies[3]))
283 latency["direction1"]["10"] = dict(zip(keys, latencies[4]))
284 latency["direction2"]["10"] = dict(zip(keys, latencies[5]))
288 def visit_suite(self, suite):
289 """Implements traversing through the suite and its direct children.
291 :param suite: Suite to process.
295 if self.start_suite(suite) is not False:
296 suite.suites.visit(self)
297 suite.tests.visit(self)
298 self.end_suite(suite)
300 def start_suite(self, suite):
301 """Called when suite starts.
303 :param suite: Suite to process.
308 suite_name = suite.name.lower().replace('"', "'")
309 self._data["suites"][suite_name] = \
310 {"doc": suite.doc.replace('"', "'").replace('\n', ' ').
311 replace('\r', '').replace('*[', '\n *[')}
313 suite.keywords.visit(self)
315 def end_suite(self, suite):
316 """Called when suite ends.
318 :param suite: Suite to process.
324 def visit_test(self, test):
325 """Implements traversing through the test.
327 :param test: Test to process.
331 if self.start_test(test) is not False:
332 test.keywords.visit(self)
335 def start_test(self, test):
336 """Called when test starts.
338 :param test: Test to process.
343 tags = [str(tag) for tag in test.tags]
344 if test.status == "PASS" and "NDRPDRDISC" in tags:
346 if "NDRDISC" in tags:
348 elif "PDRDISC" in tags:
354 rate_value = str(re.search(
355 self.REGEX_RATE, test.message).group(1))
356 except AttributeError:
359 rate_unit = str(re.search(
360 self.REGEX_RATE, test.message).group(2))
361 except AttributeError:
365 test_result["name"] = test.name.lower()
366 test_result["parent"] = test.parent.name.lower()
367 test_result["tags"] = tags
368 test_result["type"] = test_type
369 test_result["throughput"] = dict()
370 test_result["throughput"]["value"] = int(rate_value.split('.')[0])
371 test_result["throughput"]["unit"] = rate_unit
372 test_result["latency"] = self._get_latency(test.message, test_type)
373 if test_type == "PDR":
374 test_result["lossTolerance"] = str(re.search(
375 self.REGEX_TOLERANCE, test.message).group(1))
377 self._test_ID = test.longname.lower()
379 self._data["tests"][self._test_ID] = test_result
381 def end_test(self, test):
382 """Called when test ends.
384 :param test: Test to process.
390 def visit_keyword(self, keyword):
391 """Implements traversing through the keyword and its child keywords.
393 :param keyword: Keyword to process.
394 :type keyword: Keyword
397 if self.start_keyword(keyword) is not False:
398 self.end_keyword(keyword)
400 def start_keyword(self, keyword):
401 """Called when keyword starts. Default implementation does nothing.
403 :param keyword: Keyword to process.
404 :type keyword: Keyword
408 if keyword.type == "setup":
409 self.visit_setup_kw(keyword)
410 elif keyword.type == "teardown":
411 self.visit_teardown_kw(keyword)
412 except AttributeError:
415 def end_keyword(self, keyword):
416 """Called when keyword ends. Default implementation does nothing.
418 :param keyword: Keyword to process.
419 :type keyword: Keyword
424 def visit_setup_kw(self, setup_kw):
425 """Implements traversing through the teardown keyword and its child
428 :param setup_kw: Keyword to process.
429 :type setup_kw: Keyword
432 for keyword in setup_kw.keywords:
433 if self.start_setup_kw(keyword) is not False:
434 self.visit_setup_kw(keyword)
435 self.end_setup_kw(keyword)
437 def start_setup_kw(self, setup_kw):
438 """Called when teardown keyword starts. Default implementation does
441 :param setup_kw: Keyword to process.
442 :type setup_kw: Keyword
445 if setup_kw.name.count("Vpp Show Version Verbose") \
446 and not self._version:
447 self._msg_type = "setup-version"
448 setup_kw.messages.visit(self)
450 def end_setup_kw(self, setup_kw):
451 """Called when keyword ends. Default implementation does nothing.
453 :param setup_kw: Keyword to process.
454 :type setup_kw: Keyword
459 def visit_teardown_kw(self, teardown_kw):
460 """Implements traversing through the teardown keyword and its child
463 :param teardown_kw: Keyword to process.
464 :type teardown_kw: Keyword
467 for keyword in teardown_kw.keywords:
468 if self.start_teardown_kw(keyword) is not False:
469 self.visit_teardown_kw(keyword)
470 self.end_teardown_kw(keyword)
472 def start_teardown_kw(self, teardown_kw):
473 """Called when teardown keyword starts. Default implementation does
476 :param teardown_kw: Keyword to process.
477 :type teardown_kw: Keyword
481 if teardown_kw.name.count("Show Vat History On All Duts"):
482 self._vat_history_lookup_nr = 0
483 self._msg_type = "teardown-vat-history"
484 elif teardown_kw.name.count("Vpp Show Runtime"):
485 self._show_run_lookup_nr = 0
486 self._msg_type = "teardown-show-runtime"
489 teardown_kw.messages.visit(self)
491 def end_teardown_kw(self, teardown_kw):
492 """Called when keyword ends. Default implementation does nothing.
494 :param teardown_kw: Keyword to process.
495 :type teardown_kw: Keyword
500 def visit_message(self, msg):
501 """Implements visiting the message.
503 :param msg: Message to process.
507 if self.start_message(msg) is not False:
508 self.end_message(msg)
510 def start_message(self, msg):
511 """Called when message starts. Get required information from messages:
514 :param msg: Message to process.
520 self.parse_msg[self._msg_type](msg)
522 def end_message(self, msg):
523 """Called when message ends. Default implementation does nothing.
525 :param msg: Message to process.
532 class InputData(object):
535 The data is extracted from output.xml files generated by Jenkins jobs and
536 stored in pandas' DataFrames.
547 - ID: test data (as described in ExecutionChecker documentation)
550 def __init__(self, config):
558 self._input_data = None
562 """Getter - Input data.
565 :rtype: pandas.Series
567 return self._input_data
569 def metadata(self, job, build):
572 :param job: Job which metadata we want.
573 :param build: Build which metadata we want.
577 :rtype: pandas.Series
580 return self.data[job][build]["metadata"]
582 def suites(self, job, build):
585 :param job: Job which suites we want.
586 :param build: Build which suites we want.
590 :rtype: pandas.Series
593 return self.data[job][build]["suites"]
595 def tests(self, job, build):
598 :param job: Job which tests we want.
599 :param build: Build which tests we want.
603 :rtype: pandas.Series
606 return self.data[job][build]["tests"]
609 def _parse_tests(job, build):
610 """Process data from robot output.xml file and return JSON structured
613 :param job: The name of job which build output data will be processed.
614 :param build: The build which output data will be processed.
617 :returns: JSON data structure.
621 with open(build["file-name"], 'r') as data_file:
622 result = ExecutionResult(data_file)
623 checker = ExecutionChecker(job=job, build=build)
624 result.visit(checker)
628 def parse_input_data(self):
629 """Parse input data from input files and store in pandas' Series.
632 logging.info("Parsing input files ...")
635 for job, builds in self._cfg.builds.items():
636 logging.info(" Extracting data from the job '{0}' ...'".
640 logging.info(" Extracting data from the build '{0}'".
641 format(build["build"]))
642 logging.info(" Processing the file '{0}'".
643 format(build["file-name"]))
644 data = InputData._parse_tests(job, build)
646 build_data = pd.Series({
647 "metadata": pd.Series(data["metadata"].values(),
648 index=data["metadata"].keys()),
649 "suites": pd.Series(data["suites"].values(),
650 index=data["suites"].keys()),
651 "tests": pd.Series(data["tests"].values(),
652 index=data["tests"].keys()),
654 builds_data[str(build["build"])] = build_data
655 logging.info(" Done.")
657 job_data[job] = pd.Series(builds_data.values(),
658 index=builds_data.keys())
659 logging.info(" Done.")
661 self._input_data = pd.Series(job_data.values(), index=job_data.keys())
662 logging.info("Done.")
665 def _end_of_tag(tag_filter, start=0, closer="'"):
666 """Return the index of character in the string which is the end of tag.
668 :param tag_filter: The string where the end of tag is being searched.
669 :param start: The index where the searching is stated.
670 :param closer: The character which is the tag closer.
671 :type tag_filter: str
674 :returns: The index of the tag closer.
679 idx_opener = tag_filter.index(closer, start)
680 return tag_filter.index(closer, idx_opener + 1)
685 def _condition(tag_filter):
686 """Create a conditional statement from the given tag filter.
688 :param tag_filter: Filter based on tags from the element specification.
689 :type tag_filter: str
690 :returns: Conditional statement which can be evaluated.
696 index = InputData._end_of_tag(tag_filter, index)
700 tag_filter = tag_filter[:index] + " in tags" + tag_filter[index:]
702 def filter_tests_data(self, element, params=None):
703 """Filter required data from the given jobs and builds.
705 The output data structure is:
722 :param element: Element which will use the filtered data.
723 :param params: Parameters which will be included in the output.
724 :type element: pandas.Series
726 :returns: Filtered data.
730 logging.info(" Creating the data set for the {0} '{1}'.".
731 format(element["type"], element.get("title", "")))
733 cond = InputData._condition(element.get("filter", ""))
735 logging.debug(" Filter: {0}".format(cond))
737 logging.error(" No filter defined.")
742 params = element["parameters"]
748 for job, builds in element["data"].items():
749 data[job] = pd.Series()
751 data[job][str(build)] = pd.Series()
753 for test_ID, test_data in self.tests(job, str(build)).\
755 if eval(cond, {"tags": test_data["tags"]}):
756 data[job][str(build)][test_ID] = pd.Series()
758 for param, val in test_data.items():
759 data[job][str(build)][test_ID][param] = val
762 data[job][str(build)][test_ID][param] = \
766 except (KeyError, IndexError, ValueError) as err:
767 raise PresentationError("Missing mandatory parameter in the "
768 "element specification.", err)