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