PAL: Merge input data
[csit.git] / resources / tools / presentation / input_data_parser.py
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:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
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.
13
14 """Data pre-processing
15
16 - extract data from output.xml files generated by Jenkins jobs and store in
17   pandas' Series,
18 - provide access to the data.
19 """
20
21 import re
22 import pandas as pd
23 import logging
24
25 from robot.api import ExecutionResult, ResultVisitor
26 from collections import OrderedDict
27 from string import replace
28
29
30 class ExecutionChecker(ResultVisitor):
31     """Class to traverse through the test suite structure.
32
33     The functionality implemented in this class generates a json structure:
34
35     Performance tests:
36
37     {
38         "metadata": {  # Optional
39             "version": "VPP version",
40             "job": "Jenkins job name",
41             "build": "Information about the build"
42         },
43         "suites": {
44             "Suite name 1": {
45                 "doc": "Suite 1 documentation",
46                 "parent": "Suite 1 parent",
47                 "level": "Level of the suite in the suite hierarchy"
48             }
49             "Suite name N": {
50                 "doc": "Suite N documentation",
51                 "parent": "Suite 2 parent",
52                 "level": "Level of the suite in the suite hierarchy"
53             }
54         }
55         "tests": {
56             "ID": {
57                 "name": "Test name",
58                 "parent": "Name of the parent of the test",
59                 "doc": "Test documentation"
60                 "msg": "Test message"
61                 "tags": ["tag 1", "tag 2", "tag n"],
62                 "type": "PDR" | "NDR",
63                 "throughput": {
64                     "value": int,
65                     "unit": "pps" | "bps" | "percentage"
66                 },
67                 "latency": {
68                     "direction1": {
69                         "100": {
70                             "min": int,
71                             "avg": int,
72                             "max": int
73                         },
74                         "50": {  # Only for NDR
75                             "min": int,
76                             "avg": int,
77                             "max": int
78                         },
79                         "10": {  # Only for NDR
80                             "min": int,
81                             "avg": int,
82                             "max": int
83                         }
84                     },
85                     "direction2": {
86                         "100": {
87                             "min": int,
88                             "avg": int,
89                             "max": int
90                         },
91                         "50": {  # Only for NDR
92                             "min": int,
93                             "avg": int,
94                             "max": int
95                         },
96                         "10": {  # Only for NDR
97                             "min": int,
98                             "avg": int,
99                             "max": int
100                         }
101                     }
102                 },
103                 "lossTolerance": "lossTolerance",  # Only for PDR
104                 "vat-history": "DUT1 and DUT2 VAT History"
105                 },
106                 "show-run": "Show Run"
107             },
108             "ID" {
109                 # next test
110             }
111         }
112     }
113
114     Functional tests:
115
116
117     {
118         "metadata": {  # Optional
119             "version": "VPP version",
120             "job": "Jenkins job name",
121             "build": "Information about the build"
122         },
123         "suites": {
124             "Suite name 1": {
125                 "doc": "Suite 1 documentation",
126                 "parent": "Suite 1 parent",
127                 "level": "Level of the suite in the suite hierarchy"
128             }
129             "Suite name N": {
130                 "doc": "Suite N documentation",
131                 "parent": "Suite 2 parent",
132                 "level": "Level of the suite in the suite hierarchy"
133             }
134         }
135         "tests": {
136             "ID": {
137                 "name": "Test name",
138                 "parent": "Name of the parent of the test",
139                 "doc": "Test documentation"
140                 "msg": "Test message"
141                 "tags": ["tag 1", "tag 2", "tag n"],
142                 "vat-history": "DUT1 and DUT2 VAT History"
143                 "show-run": "Show Run"
144                 "status": "PASS" | "FAIL"
145             },
146             "ID" {
147                 # next test
148             }
149         }
150     }
151
152     .. note:: ID is the lowercase full path to the test.
153     """
154
155     REGEX_RATE = re.compile(r'^[\D\d]*FINAL_RATE:\s(\d+\.\d+)\s(\w+)')
156
157     REGEX_LAT_NDR = re.compile(r'^[\D\d]*'
158                                r'LAT_\d+%NDR:\s\[\'(-?\d+\/-?\d+/-?\d+)\','
159                                r'\s\'(-?\d+/-?\d+/-?\d+)\'\]\s\n'
160                                r'LAT_\d+%NDR:\s\[\'(-?\d+/-?\d+/-?\d+)\','
161                                r'\s\'(-?\d+/-?\d+/-?\d+)\'\]\s\n'
162                                r'LAT_\d+%NDR:\s\[\'(-?\d+/-?\d+/-?\d+)\','
163                                r'\s\'(-?\d+/-?\d+/-?\d+)\'\]')
164
165     REGEX_LAT_PDR = re.compile(r'^[\D\d]*'
166                                r'LAT_\d+%PDR:\s\[\'(-?\d+/-?\d+/-?\d+)\','
167                                r'\s\'(-?\d+/-?\d+/-?\d+)\'\][\D\d]*')
168
169     REGEX_TOLERANCE = re.compile(r'^[\D\d]*LOSS_ACCEPTANCE:\s(\d*\.\d*)\s'
170                                  r'[\D\d]*')
171
172     REGEX_VERSION = re.compile(r"(stdout: 'vat# vat# Version:)(\s*)(.*)")
173
174     def __init__(self, **metadata):
175         """Initialisation.
176
177         :param metadata: Key-value pairs to be included in "metadata" part of
178         JSON structure.
179         :type metadata: dict
180         """
181
182         # Type of message to parse out from the test messages
183         self._msg_type = None
184
185         # VPP version
186         self._version = None
187
188         # Number of VAT History messages found:
189         # 0 - no message
190         # 1 - VAT History of DUT1
191         # 2 - VAT History of DUT2
192         self._lookup_kw_nr = 0
193         self._vat_history_lookup_nr = 0
194
195         # Number of Show Running messages found
196         # 0 - no message
197         # 1 - Show run message found
198         self._show_run_lookup_nr = 0
199
200         # Test ID of currently processed test- the lowercase full path to the
201         # test
202         self._test_ID = None
203
204         # The main data structure
205         self._data = {
206             "metadata": OrderedDict(),
207             "suites": OrderedDict(),
208             "tests": OrderedDict()
209         }
210
211         # Save the provided metadata
212         for key, val in metadata.items():
213             self._data["metadata"][key] = val
214
215         # Dictionary defining the methods used to parse different types of
216         # messages
217         self.parse_msg = {
218             "setup-version": self._get_version,
219             "teardown-vat-history": self._get_vat_history,
220             "teardown-show-runtime": self._get_show_run
221         }
222
223     @property
224     def data(self):
225         """Getter - Data parsed from the XML file.
226
227         :returns: Data parsed from the XML file.
228         :rtype: dict
229         """
230         return self._data
231
232     def _get_version(self, msg):
233         """Called when extraction of VPP version is required.
234
235         :param msg: Message to process.
236         :type msg: Message
237         :returns: Nothing.
238         """
239
240         if msg.message.count("stdout: 'vat# vat# Version:"):
241             self._version = str(re.search(self.REGEX_VERSION, msg.message).
242                                 group(3))
243             self._data["metadata"]["version"] = self._version
244             self._msg_type = None
245
246             logging.debug("    VPP version: {0}".format(self._version))
247
248     def _get_vat_history(self, msg):
249         """Called when extraction of VAT command history is required.
250
251         :param msg: Message to process.
252         :type msg: Message
253         :returns: Nothing.
254         """
255         if msg.message.count("VAT command history:"):
256             self._vat_history_lookup_nr += 1
257             if self._vat_history_lookup_nr == 1:
258                 self._data["tests"][self._test_ID]["vat-history"] = str()
259             else:
260                 self._msg_type = None
261             text = re.sub("[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3} "
262                           "VAT command history:", "", msg.message, count=1). \
263                 replace("\n\n", "\n").replace('\n', ' |br| ').\
264                 replace('\r', '').replace('"', "'")
265
266             self._data["tests"][self._test_ID]["vat-history"] += " |br| "
267             self._data["tests"][self._test_ID]["vat-history"] += \
268                 "**DUT" + str(self._vat_history_lookup_nr) + ":** " + text
269
270     def _get_show_run(self, msg):
271         """Called when extraction of VPP operational data (output of CLI command
272         Show Runtime) is required.
273
274         :param msg: Message to process.
275         :type msg: Message
276         :returns: Nothing.
277         """
278         if msg.message.count("return STDOUT Thread "):
279             self._show_run_lookup_nr += 1
280             if self._lookup_kw_nr == 1 and self._show_run_lookup_nr == 1:
281                 self._data["tests"][self._test_ID]["show-run"] = str()
282             if self._lookup_kw_nr > 1:
283                 self._msg_type = None
284             if self._show_run_lookup_nr == 1:
285                 text = msg.message.replace("vat# ", "").\
286                     replace("return STDOUT ", "").replace("\n\n", "\n").\
287                     replace('\n', ' |br| ').\
288                     replace('\r', '').replace('"', "'")
289                 try:
290                     self._data["tests"][self._test_ID]["show-run"] += " |br| "
291                     self._data["tests"][self._test_ID]["show-run"] += \
292                         "**DUT" + str(self._lookup_kw_nr) + ":** |br| " + text
293                 except KeyError:
294                     pass
295
296     def _get_latency(self, msg, test_type):
297         """Get the latency data from the test message.
298
299         :param msg: Message to be parsed.
300         :param test_type: Type of the test - NDR or PDR.
301         :type msg: str
302         :type test_type: str
303         :returns: Latencies parsed from the message.
304         :rtype: dict
305         """
306
307         if test_type == "NDR":
308             groups = re.search(self.REGEX_LAT_NDR, msg)
309             groups_range = range(1, 7)
310         elif test_type == "PDR":
311             groups = re.search(self.REGEX_LAT_PDR, msg)
312             groups_range = range(1, 3)
313         else:
314             return {}
315
316         latencies = list()
317         for idx in groups_range:
318             try:
319                 lat = [int(item) for item in str(groups.group(idx)).split('/')]
320             except (AttributeError, ValueError):
321                 lat = [-1, -1, -1]
322             latencies.append(lat)
323
324         keys = ("min", "avg", "max")
325         latency = {
326             "direction1": {
327             },
328             "direction2": {
329             }
330         }
331
332         latency["direction1"]["100"] = dict(zip(keys, latencies[0]))
333         latency["direction2"]["100"] = dict(zip(keys, latencies[1]))
334         if test_type == "NDR":
335             latency["direction1"]["50"] = dict(zip(keys, latencies[2]))
336             latency["direction2"]["50"] = dict(zip(keys, latencies[3]))
337             latency["direction1"]["10"] = dict(zip(keys, latencies[4]))
338             latency["direction2"]["10"] = dict(zip(keys, latencies[5]))
339
340         return latency
341
342     def visit_suite(self, suite):
343         """Implements traversing through the suite and its direct children.
344
345         :param suite: Suite to process.
346         :type suite: Suite
347         :returns: Nothing.
348         """
349         if self.start_suite(suite) is not False:
350             suite.suites.visit(self)
351             suite.tests.visit(self)
352             self.end_suite(suite)
353
354     def start_suite(self, suite):
355         """Called when suite starts.
356
357         :param suite: Suite to process.
358         :type suite: Suite
359         :returns: Nothing.
360         """
361
362         try:
363             parent_name = suite.parent.name
364         except AttributeError:
365             return
366
367         doc_str = suite.doc.replace('"', "'").replace('\n', ' ').\
368             replace('\r', '').replace('*[', ' |br| *[').replace("*", "**")
369         doc_str = replace(doc_str, ' |br| *[', '*[', maxreplace=1)
370
371         self._data["suites"][suite.longname.lower().replace('"', "'").
372             replace(" ", "_")] = {
373             "name": suite.name.lower(),
374             "doc": doc_str,
375             "parent": parent_name,
376             "level": len(suite.longname.split("."))
377         }
378
379         suite.keywords.visit(self)
380
381     def end_suite(self, suite):
382         """Called when suite ends.
383
384         :param suite: Suite to process.
385         :type suite: Suite
386         :returns: Nothing.
387         """
388         pass
389
390     def visit_test(self, test):
391         """Implements traversing through the test.
392
393         :param test: Test to process.
394         :type test: Test
395         :returns: Nothing.
396         """
397         if self.start_test(test) is not False:
398             test.keywords.visit(self)
399             self.end_test(test)
400
401     def start_test(self, test):
402         """Called when test starts.
403
404         :param test: Test to process.
405         :type test: Test
406         :returns: Nothing.
407         """
408
409         tags = [str(tag) for tag in test.tags]
410         test_result = dict()
411         test_result["name"] = test.name.lower()
412         test_result["parent"] = test.parent.name.lower()
413         test_result["tags"] = tags
414         doc_str = test.doc.replace('"', "'").replace('\n', ' '). \
415             replace('\r', '').replace('[', ' |br| [')
416         test_result["doc"] =  replace(doc_str, ' |br| [', '[', maxreplace=1)
417         test_result["msg"] = test.message.replace('\n', ' |br| '). \
418             replace('\r', '').replace('"', "'")
419         if test.status == "PASS" and "NDRPDRDISC" in tags:
420
421             if "NDRDISC" in tags:
422                 test_type = "NDR"
423             elif "PDRDISC" in tags:
424                 test_type = "PDR"
425             else:
426                 return
427
428             try:
429                 rate_value = str(re.search(
430                     self.REGEX_RATE, test.message).group(1))
431             except AttributeError:
432                 rate_value = "-1"
433             try:
434                 rate_unit = str(re.search(
435                     self.REGEX_RATE, test.message).group(2))
436             except AttributeError:
437                 rate_unit = "-1"
438
439             test_result["type"] = test_type
440             test_result["throughput"] = dict()
441             test_result["throughput"]["value"] = int(rate_value.split('.')[0])
442             test_result["throughput"]["unit"] = rate_unit
443             test_result["latency"] = self._get_latency(test.message, test_type)
444             if test_type == "PDR":
445                 test_result["lossTolerance"] = str(re.search(
446                     self.REGEX_TOLERANCE, test.message).group(1))
447         else:
448             test_result["status"] = test.status
449
450         self._test_ID = test.longname.lower()
451         self._data["tests"][self._test_ID] = test_result
452
453     def end_test(self, test):
454         """Called when test ends.
455
456         :param test: Test to process.
457         :type test: Test
458         :returns: Nothing.
459         """
460         pass
461
462     def visit_keyword(self, keyword):
463         """Implements traversing through the keyword and its child keywords.
464
465         :param keyword: Keyword to process.
466         :type keyword: Keyword
467         :returns: Nothing.
468         """
469         if self.start_keyword(keyword) is not False:
470             self.end_keyword(keyword)
471
472     def start_keyword(self, keyword):
473         """Called when keyword starts. Default implementation does nothing.
474
475         :param keyword: Keyword to process.
476         :type keyword: Keyword
477         :returns: Nothing.
478         """
479         try:
480             if keyword.type == "setup":
481                 self.visit_setup_kw(keyword)
482             elif keyword.type == "teardown":
483                 self._lookup_kw_nr = 0
484                 self.visit_teardown_kw(keyword)
485         except AttributeError:
486             pass
487
488     def end_keyword(self, keyword):
489         """Called when keyword ends. Default implementation does nothing.
490
491         :param keyword: Keyword to process.
492         :type keyword: Keyword
493         :returns: Nothing.
494         """
495         pass
496
497     def visit_setup_kw(self, setup_kw):
498         """Implements traversing through the teardown keyword and its child
499         keywords.
500
501         :param setup_kw: Keyword to process.
502         :type setup_kw: Keyword
503         :returns: Nothing.
504         """
505         for keyword in setup_kw.keywords:
506             if self.start_setup_kw(keyword) is not False:
507                 self.visit_setup_kw(keyword)
508                 self.end_setup_kw(keyword)
509
510     def start_setup_kw(self, setup_kw):
511         """Called when teardown keyword starts. Default implementation does
512         nothing.
513
514         :param setup_kw: Keyword to process.
515         :type setup_kw: Keyword
516         :returns: Nothing.
517         """
518         if setup_kw.name.count("Vpp Show Version Verbose") \
519                 and not self._version:
520             self._msg_type = "setup-version"
521             setup_kw.messages.visit(self)
522
523     def end_setup_kw(self, setup_kw):
524         """Called when keyword ends. Default implementation does nothing.
525
526         :param setup_kw: Keyword to process.
527         :type setup_kw: Keyword
528         :returns: Nothing.
529         """
530         pass
531
532     def visit_teardown_kw(self, teardown_kw):
533         """Implements traversing through the teardown keyword and its child
534         keywords.
535
536         :param teardown_kw: Keyword to process.
537         :type teardown_kw: Keyword
538         :returns: Nothing.
539         """
540         for keyword in teardown_kw.keywords:
541             if self.start_teardown_kw(keyword) is not False:
542                 self.visit_teardown_kw(keyword)
543                 self.end_teardown_kw(keyword)
544
545     def start_teardown_kw(self, teardown_kw):
546         """Called when teardown keyword starts. Default implementation does
547         nothing.
548
549         :param teardown_kw: Keyword to process.
550         :type teardown_kw: Keyword
551         :returns: Nothing.
552         """
553
554         if teardown_kw.name.count("Show Vat History On All Duts"):
555             self._vat_history_lookup_nr = 0
556             self._msg_type = "teardown-vat-history"
557         elif teardown_kw.name.count("Show Statistics On All Duts"):
558             self._lookup_kw_nr += 1
559             self._show_run_lookup_nr = 0
560             self._msg_type = "teardown-show-runtime"
561
562         if self._msg_type:
563             teardown_kw.messages.visit(self)
564
565     def end_teardown_kw(self, teardown_kw):
566         """Called when keyword ends. Default implementation does nothing.
567
568         :param teardown_kw: Keyword to process.
569         :type teardown_kw: Keyword
570         :returns: Nothing.
571         """
572         pass
573
574     def visit_message(self, msg):
575         """Implements visiting the message.
576
577         :param msg: Message to process.
578         :type msg: Message
579         :returns: Nothing.
580         """
581         if self.start_message(msg) is not False:
582             self.end_message(msg)
583
584     def start_message(self, msg):
585         """Called when message starts. Get required information from messages:
586         - VPP version.
587
588         :param msg: Message to process.
589         :type msg: Message
590         :returns: Nothing.
591         """
592
593         if self._msg_type:
594             self.parse_msg[self._msg_type](msg)
595
596     def end_message(self, msg):
597         """Called when message ends. Default implementation does nothing.
598
599         :param msg: Message to process.
600         :type msg: Message
601         :returns: Nothing.
602         """
603         pass
604
605
606 class InputData(object):
607     """Input data
608
609     The data is extracted from output.xml files generated by Jenkins jobs and
610     stored in pandas' DataFrames.
611
612     The data structure:
613     - job name
614       - build number
615         - metadata
616           - job
617           - build
618           - vpp version
619         - suites
620         - tests
621           - ID: test data (as described in ExecutionChecker documentation)
622     """
623
624     def __init__(self, spec):
625         """Initialization.
626
627         :param spec: Specification.
628         :type spec: Specification
629         """
630
631         # Specification:
632         self._cfg = spec
633
634         # Data store:
635         self._input_data = None
636
637     @property
638     def data(self):
639         """Getter - Input data.
640
641         :returns: Input data
642         :rtype: pandas.Series
643         """
644         return self._input_data
645
646     def metadata(self, job, build):
647         """Getter - metadata
648
649         :param job: Job which metadata we want.
650         :param build: Build which metadata we want.
651         :type job: str
652         :type build: str
653         :returns: Metadata
654         :rtype: pandas.Series
655         """
656
657         return self.data[job][build]["metadata"]
658
659     def suites(self, job, build):
660         """Getter - suites
661
662         :param job: Job which suites we want.
663         :param build: Build which suites we want.
664         :type job: str
665         :type build: str
666         :returns: Suites.
667         :rtype: pandas.Series
668         """
669
670         return self.data[job][str(build)]["suites"]
671
672     def tests(self, job, build):
673         """Getter - tests
674
675         :param job: Job which tests we want.
676         :param build: Build which tests we want.
677         :type job: str
678         :type build: str
679         :returns: Tests.
680         :rtype: pandas.Series
681         """
682
683         return self.data[job][build]["tests"]
684
685     @staticmethod
686     def _parse_tests(job, build):
687         """Process data from robot output.xml file and return JSON structured
688         data.
689
690         :param job: The name of job which build output data will be processed.
691         :param build: The build which output data will be processed.
692         :type job: str
693         :type build: dict
694         :returns: JSON data structure.
695         :rtype: dict
696         """
697
698         with open(build["file-name"], 'r') as data_file:
699             result = ExecutionResult(data_file)
700         checker = ExecutionChecker(job=job, build=build)
701         result.visit(checker)
702
703         return checker.data
704
705     def read_data(self):
706         """Parse input data from input files and store in pandas' Series.
707         """
708
709         logging.info("Parsing input files ...")
710
711         job_data = dict()
712         for job, builds in self._cfg.builds.items():
713             logging.info("  Extracting data from the job '{0}' ...'".
714                          format(job))
715             builds_data = dict()
716             for build in builds:
717                 if build["status"] == "failed" \
718                         or build["status"] == "not found":
719                     continue
720                 logging.info("    Extracting data from the build '{0}'".
721                              format(build["build"]))
722                 logging.info("    Processing the file '{0}'".
723                              format(build["file-name"]))
724                 data = InputData._parse_tests(job, build)
725
726                 build_data = pd.Series({
727                     "metadata": pd.Series(data["metadata"].values(),
728                                           index=data["metadata"].keys()),
729                     "suites": pd.Series(data["suites"].values(),
730                                         index=data["suites"].keys()),
731                     "tests": pd.Series(data["tests"].values(),
732                                        index=data["tests"].keys()),
733                     })
734                 builds_data[str(build["build"])] = build_data
735                 logging.info("    Done.")
736
737             job_data[job] = pd.Series(builds_data.values(),
738                                       index=builds_data.keys())
739             logging.info("  Done.")
740
741         self._input_data = pd.Series(job_data.values(), index=job_data.keys())
742         logging.info("Done.")
743
744     @staticmethod
745     def _end_of_tag(tag_filter, start=0, closer="'"):
746         """Return the index of character in the string which is the end of tag.
747
748         :param tag_filter: The string where the end of tag is being searched.
749         :param start: The index where the searching is stated.
750         :param closer: The character which is the tag closer.
751         :type tag_filter: str
752         :type start: int
753         :type closer: str
754         :returns: The index of the tag closer.
755         :rtype: int
756         """
757
758         try:
759             idx_opener = tag_filter.index(closer, start)
760             return tag_filter.index(closer, idx_opener + 1)
761         except ValueError:
762             return None
763
764     @staticmethod
765     def _condition(tag_filter):
766         """Create a conditional statement from the given tag filter.
767
768         :param tag_filter: Filter based on tags from the element specification.
769         :type tag_filter: str
770         :returns: Conditional statement which can be evaluated.
771         :rtype: str
772         """
773
774         index = 0
775         while True:
776             index = InputData._end_of_tag(tag_filter, index)
777             if index is None:
778                 return tag_filter
779             index += 1
780             tag_filter = tag_filter[:index] + " in tags" + tag_filter[index:]
781
782     def filter_data(self, element, params=None, data_set="tests"):
783         """Filter required data from the given jobs and builds.
784
785         The output data structure is:
786
787         - job 1
788           - build 1
789             - test (suite) 1 ID:
790               - param 1
791               - param 2
792               ...
793               - param n
794             ...
795             - test (suite) n ID:
796             ...
797           ...
798           - build n
799         ...
800         - job n
801
802         :param element: Element which will use the filtered data.
803         :param params: Parameters which will be included in the output. If None,
804         all parameters are included.
805         :param data_set: The set of data to be filtered: tests, suites,
806         metadata.
807         :type element: pandas.Series
808         :type params: list
809         :type data_set: str
810         :returns: Filtered data.
811         :rtype pandas.Series
812         """
813
814         logging.info("    Creating the data set for the {0} '{1}'.".
815                      format(element["type"], element.get("title", "")))
816
817         try:
818             if element["filter"] in ("all", "template"):
819                 cond = "True"
820             else:
821                 cond = InputData._condition(element["filter"])
822             logging.debug("   Filter: {0}".format(cond))
823         except KeyError:
824             logging.error("  No filter defined.")
825             return None
826
827         if params is None:
828             params = element.get("parameters", None)
829
830         data = pd.Series()
831         try:
832             for job, builds in element["data"].items():
833                 data[job] = pd.Series()
834                 for build in builds:
835                     data[job][str(build)] = pd.Series()
836                     for test_ID, test_data in \
837                             self.data[job][str(build)][data_set].iteritems():
838                         if eval(cond, {"tags": test_data.get("tags", "")}):
839                             data[job][str(build)][test_ID] = pd.Series()
840                             if params is None:
841                                 for param, val in test_data.items():
842                                     data[job][str(build)][test_ID][param] = val
843                             else:
844                                 for param in params:
845                                     try:
846                                         data[job][str(build)][test_ID][param] =\
847                                             test_data[param]
848                                     except KeyError:
849                                         data[job][str(build)][test_ID][param] =\
850                                             "No Data"
851             return data
852
853         except (KeyError, IndexError, ValueError) as err:
854             logging.error("   Missing mandatory parameter in the element "
855                           "specification.", err)
856             return None
857         except SyntaxError:
858             logging.error("   The filter '{0}' is not correct. Check if all "
859                           "tags are enclosed by apostrophes.".format(cond))
860             return None
861
862     @staticmethod
863     def merge_data(data):
864         """Merge data from more jobs and builds to a simple data structure.
865
866         The output data structure is:
867
868         - test (suite) 1 ID:
869           - param 1
870           - param 2
871           ...
872           - param n
873         ...
874         - test (suite) n ID:
875         ...
876
877         :param data: Data to merge.
878         :type data: pandas.Series
879         :returns: Merged data.
880         :rtype: pandas.Series
881         """
882
883         logging.info("    Merging data ...")
884
885         merged_data = pd.Series()
886         for _, builds in data.iteritems():
887             for _, item in builds.iteritems():
888                 for ID, item_data in item.iteritems():
889                     merged_data[ID] = item_data
890
891         return merged_data