Report: Detailed test results
[csit.git] / resources / tools / presentation / specification_parser.py
1 # Copyright (c) 2020 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 """Specification
15
16 Parsing of the specification YAML file.
17 """
18
19
20 import logging
21 from pprint import pformat
22
23 from yaml import load, FullLoader, YAMLError
24
25 from pal_errors import PresentationError
26 from pal_utils import (
27     get_last_successful_build_nr, get_last_completed_build_number
28 )
29
30
31 class Specification:
32     """Specification of Presentation and analytics layer.
33
34     - based on specification specified in the specification YAML file
35     - presentation and analytics layer is model driven
36     """
37
38     # Tags are used in specification YAML file and replaced while the file is
39     # parsed.
40     TAG_OPENER = u"{"
41     TAG_CLOSER = u"}"
42
43     def __init__(self, cfg_file):
44         """Initialization.
45
46         :param cfg_file: File handler for the specification YAML file.
47         :type cfg_file: BinaryIO
48         """
49         self._cfg_file = cfg_file
50         self._cfg_yaml = None
51
52         self._specification = {
53             u"environment": dict(),
54             u"configuration": dict(),
55             u"static": dict(),
56             u"input": dict(),
57             u"output": dict(),
58             u"tables": list(),
59             u"plots": list(),
60             u"files": list(),
61             u"cpta": dict()
62         }
63
64     @property
65     def specification(self):
66         """Getter - specification.
67
68         :returns: Specification.
69         :rtype: dict
70         """
71         return self._specification
72
73     @property
74     def environment(self):
75         """Getter - environment.
76
77         :returns: Environment specification.
78         :rtype: dict
79         """
80         return self._specification[u"environment"]
81
82     @property
83     def configuration(self):
84         """Getter - configuration.
85
86         :returns: Configuration of PAL.
87         :rtype: dict
88         """
89         return self._specification[u"configuration"]
90
91     @property
92     def static(self):
93         """Getter - static content.
94
95         :returns: Static content specification.
96         :rtype: dict
97         """
98         return self._specification[u"static"]
99
100     @property
101     def mapping(self):
102         """Getter - Mapping.
103
104         :returns: Mapping of the old names of test cases to the new (actual)
105             one.
106         :rtype: dict
107         """
108         return self._specification[u"configuration"][u"mapping"]
109
110     @property
111     def ignore(self):
112         """Getter - Ignore list.
113
114         :returns: List of ignored test cases.
115         :rtype: list
116         """
117         return self._specification[u"configuration"][u"ignore"]
118
119     @property
120     def alerting(self):
121         """Getter - Alerting.
122
123         :returns: Specification of alerts.
124         :rtype: dict
125         """
126         return self._specification[u"configuration"][u"alerting"]
127
128     @property
129     def input(self):
130         """Getter - specification - inputs.
131         - jobs and builds.
132
133         :returns: Inputs.
134         :rtype: dict
135         """
136         return self._specification[u"input"]
137
138     @property
139     def builds(self):
140         """Getter - builds defined in specification.
141
142         :returns: Builds defined in the specification.
143         :rtype: dict
144         """
145         return self.input[u"builds"]
146
147     @property
148     def output(self):
149         """Getter - specification - output formats and versions to be generated.
150         - formats: html, pdf
151         - versions: full, ...
152
153         :returns: Outputs to be generated.
154         :rtype: dict
155         """
156         return self._specification[u"output"]
157
158     @property
159     def tables(self):
160         """Getter - tables to be generated.
161
162         :returns: List of specifications of tables to be generated.
163         :rtype: list
164         """
165         return self._specification[u"tables"]
166
167     @property
168     def plots(self):
169         """Getter - plots to be generated.
170
171         :returns: List of specifications of plots to be generated.
172         :rtype: list
173         """
174         return self._specification[u"plots"]
175
176     @property
177     def files(self):
178         """Getter - files to be generated.
179
180         :returns: List of specifications of files to be generated.
181         :rtype: list
182         """
183         return self._specification[u"files"]
184
185     @property
186     def cpta(self):
187         """Getter - Continuous Performance Trending and Analysis to be
188         generated.
189
190         :returns: List of specifications of Continuous Performance Trending and
191             Analysis to be generated.
192         :rtype: list
193         """
194         return self._specification[u"cpta"]
195
196     def set_input_state(self, job, build_nr, state):
197         """Set the state of input
198
199         :param job: Job name.
200         :param build_nr: Build number.
201         :param state: The new input state.
202         :type job: str
203         :type build_nr: int
204         :type state: str
205         :raises: PresentationError if wrong job and/or build is provided.
206         """
207
208         try:
209             for build in self._specification[u"input"][u"builds"][job]:
210                 if build[u"build"] == build_nr:
211                     build[u"status"] = state
212                     break
213             else:
214                 raise PresentationError(
215                     f"Build {build_nr} is not defined for job {job} in "
216                     f"specification file."
217                 )
218         except KeyError:
219             raise PresentationError(
220                 f"Job {job} and build {build_nr} is not defined in "
221                 f"specification file."
222             )
223
224     def set_input_file_name(self, job, build_nr, file_name):
225         """Set the state of input
226
227         :param job: Job name.
228         :param build_nr: Build number.
229         :param file_name: The new file name.
230         :type job: str
231         :type build_nr: int
232         :type file_name: str
233         :raises: PresentationError if wrong job and/or build is provided.
234         """
235
236         try:
237             for build in self._specification[u"input"][u"builds"][job]:
238                 if build[u"build"] == build_nr:
239                     build[u"file-name"] = file_name
240                     break
241             else:
242                 raise PresentationError(
243                     f"Build {build_nr} is not defined for job {job} in "
244                     f"specification file."
245                 )
246         except KeyError:
247             raise PresentationError(
248                 f"Job {job} and build {build_nr} is not defined in "
249                 f"specification file."
250             )
251
252     def _get_build_number(self, job, build_type):
253         """Get the number of the job defined by its name:
254          - lastSuccessfulBuild
255          - lastCompletedBuild
256
257         :param job: Job name.
258         :param build_type: Build type:
259          - lastSuccessfulBuild
260          - lastCompletedBuild
261         :type job" str
262         :raises PresentationError: If it is not possible to get the build
263             number.
264         :returns: The build number.
265         :rtype: int
266         """
267
268         # defined as a range <start, end>
269         if build_type == u"lastSuccessfulBuild":
270             # defined as a range <start, lastSuccessfulBuild>
271             ret_code, build_nr, _ = get_last_successful_build_nr(
272                 self.environment[u"urls"][u"URL[JENKINS,CSIT]"], job)
273         elif build_type == u"lastCompletedBuild":
274             # defined as a range <start, lastCompletedBuild>
275             ret_code, build_nr, _ = get_last_completed_build_number(
276                 self.environment[u"urls"][u"URL[JENKINS,CSIT]"], job)
277         else:
278             raise PresentationError(f"Not supported build type: {build_type}")
279         if ret_code != 0:
280             raise PresentationError(u"Not possible to get the number of the "
281                                     u"build number.")
282         try:
283             build_nr = int(build_nr)
284             return build_nr
285         except ValueError as err:
286             raise PresentationError(
287                 f"Not possible to get the number of the build number. Reason:\n"
288                 f"{repr(err)}"
289             )
290
291     def _get_type_index(self, item_type):
292         """Get index of item type (environment, input, output, ...) in
293         specification YAML file.
294
295         :param item_type: Item type: Top level items in specification YAML file,
296             e.g.: environment, input, output.
297         :type item_type: str
298         :returns: Index of the given item type.
299         :rtype: int
300         """
301
302         index = 0
303         for item in self._cfg_yaml:
304             if item[u"type"] == item_type:
305                 return index
306             index += 1
307         return None
308
309     def _find_tag(self, text):
310         """Find the first tag in the given text. The tag is enclosed by the
311         TAG_OPENER and TAG_CLOSER.
312
313         :param text: Text to be searched.
314         :type text: str
315         :returns: The tag, or None if not found.
316         :rtype: str
317         """
318         try:
319             start = text.index(self.TAG_OPENER)
320             end = text.index(self.TAG_CLOSER, start + 1) + 1
321             return text[start:end]
322         except ValueError:
323             return None
324
325     def _replace_tags(self, data, src_data=None):
326         """Replace tag(s) in the data by their values.
327
328         :param data: The data where the tags will be replaced by their values.
329         :param src_data: Data where the tags are defined. It is dictionary where
330             the key is the tag and the value is the tag value. If not given,
331             'data' is used instead.
332         :type data: str, list or dict
333         :type src_data: dict
334         :returns: Data with the tags replaced.
335         :rtype: str, list or dict
336         :raises: PresentationError if it is not possible to replace the tag or
337             the data is not the supported data type (str, list or dict).
338         """
339
340         if src_data is None:
341             src_data = data
342
343         if isinstance(data, str):
344             tag = self._find_tag(data)
345             if tag is not None:
346                 data = data.replace(tag, src_data[tag[1:-1]])
347             return data
348
349         if isinstance(data, list):
350             new_list = list()
351             for item in data:
352                 new_list.append(self._replace_tags(item, src_data))
353             return new_list
354
355         if isinstance(data, dict):
356             counter = 0
357             for key, value in data.items():
358                 tag = self._find_tag(value)
359                 if tag is not None:
360                     try:
361                         data[key] = value.replace(tag, src_data[tag[1:-1]])
362                         counter += 1
363                     except KeyError:
364                         raise PresentationError(
365                             f"Not possible to replace the tag {tag}"
366                         )
367             if counter:
368                 self._replace_tags(data, src_data)
369             return data
370
371         raise PresentationError(u"Replace tags: Not supported data type.")
372
373     def _parse_env(self):
374         """Parse environment specification in the specification YAML file.
375         """
376
377         logging.info(u"Parsing specification file: environment ...")
378
379         idx = self._get_type_index(u"environment")
380         if idx is None:
381             return
382
383         try:
384             self._specification[u"environment"][u"configuration"] = \
385                 self._cfg_yaml[idx][u"configuration"]
386         except KeyError:
387             self._specification[u"environment"][u"configuration"] = None
388
389         try:
390             self._specification[u"environment"][u"paths"] = \
391                 self._replace_tags(self._cfg_yaml[idx][u"paths"])
392         except KeyError:
393             self._specification[u"environment"][u"paths"] = None
394
395         try:
396             self._specification[u"environment"][u"urls"] = \
397                 self._cfg_yaml[idx][u"urls"]
398         except KeyError:
399             self._specification[u"environment"][u"urls"] = None
400
401         try:
402             self._specification[u"environment"][u"make-dirs"] = \
403                 self._cfg_yaml[idx][u"make-dirs"]
404         except KeyError:
405             self._specification[u"environment"][u"make-dirs"] = None
406
407         try:
408             self._specification[u"environment"][u"remove-dirs"] = \
409                 self._cfg_yaml[idx][u"remove-dirs"]
410         except KeyError:
411             self._specification[u"environment"][u"remove-dirs"] = None
412
413         try:
414             self._specification[u"environment"][u"build-dirs"] = \
415                 self._cfg_yaml[idx][u"build-dirs"]
416         except KeyError:
417             self._specification[u"environment"][u"build-dirs"] = None
418
419         try:
420             self._specification[u"environment"][u"testbeds"] = \
421                 self._cfg_yaml[idx][u"testbeds"]
422         except KeyError:
423             self._specification[u"environment"][u"testbeds"] = None
424
425         logging.info(u"Done.")
426
427     def _load_mapping_table(self):
428         """Load a mapping table if it is specified. If not, use empty list.
429         """
430
431         mapping_file_name = self._specification[u"configuration"].\
432             get(u"mapping-file", None)
433         if mapping_file_name:
434             try:
435                 with open(mapping_file_name, u'r') as mfile:
436                     mapping = load(mfile, Loader=FullLoader)
437                     # Make sure everything is lowercase
438                     self._specification[u"configuration"][u"mapping"] = \
439                         {key.lower(): val.lower() for key, val in
440                          mapping.items()}
441                 logging.debug(f"Loaded mapping table:\n{mapping}")
442             except (YAMLError, IOError) as err:
443                 raise PresentationError(
444                     msg=f"An error occurred while parsing the mapping file "
445                         f"{mapping_file_name}",
446                     details=repr(err)
447                 )
448         else:
449             self._specification[u"configuration"][u"mapping"] = dict()
450
451     def _load_ignore_list(self):
452         """Load an ignore list if it is specified. If not, use empty list.
453         """
454
455         ignore_list_name = self._specification[u"configuration"].\
456             get(u"ignore-list", None)
457         if ignore_list_name:
458             try:
459                 with open(ignore_list_name, u'r') as ifile:
460                     ignore = load(ifile, Loader=FullLoader)
461                     # Make sure everything is lowercase
462                     self._specification[u"configuration"][u"ignore"] = \
463                         [item.lower() for item in ignore]
464                 logging.debug(f"Loaded ignore list:\n{ignore}")
465             except (YAMLError, IOError) as err:
466                 raise PresentationError(
467                     msg=f"An error occurred while parsing the ignore list file "
468                         f"{ignore_list_name}.",
469                     details=repr(err)
470                 )
471         else:
472             self._specification[u"configuration"][u"ignore"] = list()
473
474     def _parse_configuration(self):
475         """Parse configuration of PAL in the specification YAML file.
476         """
477
478         logging.info(u"Parsing specification file: configuration ...")
479
480         idx = self._get_type_index("configuration")
481         if idx is None:
482             logging.warning(
483                 u"No configuration information in the specification file."
484             )
485             return
486
487         try:
488             self._specification[u"configuration"] = self._cfg_yaml[idx]
489         except KeyError:
490             raise PresentationError(u"No configuration defined.")
491
492         # Data sets: Replace ranges by lists
493         for set_name, data_set in self.configuration[u"data-sets"].items():
494             if not isinstance(data_set, dict):
495                 continue
496             for job, builds in data_set.items():
497                 if not builds:
498                     continue
499                 if isinstance(builds, dict):
500                     build_end = builds.get(u"end", None)
501                     try:
502                         build_end = int(build_end)
503                     except ValueError:
504                         # defined as a range <start, build_type>
505                         build_end = self._get_build_number(job, build_end)
506                     builds = [x for x in range(builds[u"start"],
507                                                build_end + 1)
508                               if x not in builds.get(u"skip", list())]
509                     self.configuration[u"data-sets"][set_name][job] = builds
510                 elif isinstance(builds, list):
511                     for idx, item in enumerate(builds):
512                         try:
513                             builds[idx] = int(item)
514                         except ValueError:
515                             # defined as a range <build_type>
516                             builds[idx] = self._get_build_number(job, item)
517
518         # Data sets: add sub-sets to sets (only one level):
519         for set_name, data_set in self.configuration[u"data-sets"].items():
520             if isinstance(data_set, list):
521                 new_set = dict()
522                 for item in data_set:
523                     try:
524                         for key, val in self.configuration[u"data-sets"][item].\
525                                 items():
526                             new_set[key] = val
527                     except KeyError:
528                         raise PresentationError(
529                             f"Data set {item} is not defined in "
530                             f"the configuration section."
531                         )
532                 self.configuration[u"data-sets"][set_name] = new_set
533
534         # Mapping table:
535         self._load_mapping_table()
536
537         # Ignore list:
538         self._load_ignore_list()
539
540         logging.info(u"Done.")
541
542     def _parse_input(self):
543         """Parse input specification in the specification YAML file.
544
545         :raises: PresentationError if there are no data to process.
546         """
547
548         logging.info(u"Parsing specification file: input ...")
549
550         idx = self._get_type_index(u"input")
551         if idx is None:
552             raise PresentationError(u"No data to process.")
553
554         try:
555             for key, value in self._cfg_yaml[idx][u"general"].items():
556                 self._specification[u"input"][key] = value
557             self._specification[u"input"][u"builds"] = dict()
558
559             for job, builds in self._cfg_yaml[idx][u"builds"].items():
560                 if builds:
561                     if isinstance(builds, dict):
562                         build_end = builds.get(u"end", None)
563                         try:
564                             build_end = int(build_end)
565                         except ValueError:
566                             # defined as a range <start, build_type>
567                             build_end = self._get_build_number(job, build_end)
568                         builds = [x for x in range(builds[u"start"],
569                                                    build_end + 1)
570                                   if x not in builds.get(u"skip", list())]
571                     self._specification[u"input"][u"builds"][job] = list()
572                     for build in builds:
573                         self._specification[u"input"][u"builds"][job]. \
574                             append({u"build": build, u"status": None})
575
576                 else:
577                     logging.warning(
578                         f"No build is defined for the job {job}. Trying to "
579                         f"continue without it."
580                     )
581         except KeyError:
582             raise PresentationError(u"No data to process.")
583
584         logging.info(u"Done.")
585
586     def _parse_output(self):
587         """Parse output specification in the specification YAML file.
588
589         :raises: PresentationError if there is no output defined.
590         """
591
592         logging.info(u"Parsing specification file: output ...")
593
594         idx = self._get_type_index(u"output")
595         if idx is None:
596             raise PresentationError(u"No output defined.")
597
598         try:
599             self._specification[u"output"] = self._cfg_yaml[idx]
600         except (KeyError, IndexError):
601             raise PresentationError(u"No output defined.")
602
603         logging.info(u"Done.")
604
605     def _parse_static(self):
606         """Parse specification of the static content in the specification YAML
607         file.
608         """
609
610         logging.info(u"Parsing specification file: static content ...")
611
612         idx = self._get_type_index(u"static")
613         if idx is None:
614             logging.warning(u"No static content specified.")
615
616         for key, value in self._cfg_yaml[idx].items():
617             if isinstance(value, str):
618                 try:
619                     self._cfg_yaml[idx][key] = self._replace_tags(
620                         value, self._specification[u"environment"][u"paths"])
621                 except KeyError:
622                     pass
623
624         self._specification[u"static"] = self._cfg_yaml[idx]
625
626         logging.info(u"Done.")
627
628     def _parse_elements_tables(self, table):
629         """Parse tables from the specification YAML file.
630
631         :param table: Table to be parsed from the specification file.
632         :type table: dict
633         :raises PresentationError: If wrong data set is used.
634         """
635
636         try:
637             table[u"template"] = self._replace_tags(
638                 table[u"template"],
639                 self._specification[u"environment"][u"paths"])
640         except KeyError:
641             pass
642
643         # Add data sets
644         try:
645             for item in (u"reference", u"compare"):
646                 if table.get(item, None):
647                     data_set = table[item].get(u"data", None)
648                     if isinstance(data_set, str):
649                         table[item][u"data"] = \
650                             self.configuration[u"data-sets"][data_set]
651                     data_set = table[item].get(u"data-replacement", None)
652                     if isinstance(data_set, str):
653                         table[item][u"data-replacement"] = \
654                             self.configuration[u"data-sets"][data_set]
655
656             if table.get(u"history", None):
657                 for i in range(len(table[u"history"])):
658                     data_set = table[u"history"][i].get(u"data", None)
659                     if isinstance(data_set, str):
660                         table[u"history"][i][u"data"] = \
661                             self.configuration[u"data-sets"][data_set]
662                     data_set = table[u"history"][i].get(
663                         u"data-replacement", None)
664                     if isinstance(data_set, str):
665                         table[u"history"][i][u"data-replacement"] = \
666                             self.configuration[u"data-sets"][data_set]
667         except KeyError:
668             raise PresentationError(
669                 f"Wrong data set used in {table.get(u'title', u'')}."
670             )
671
672         self._specification[u"tables"].append(table)
673
674     def _parse_elements_plots(self, plot):
675         """Parse plots from the specification YAML file.
676
677         :param plot: Plot to be parsed from the specification file.
678         :type plot: dict
679         :raises PresentationError: If plot layout is not defined.
680         """
681
682         # Add layout to the plots:
683         layout = plot[u"layout"].get(u"layout", None)
684         if layout is not None:
685             plot[u"layout"].pop(u"layout")
686             try:
687                 for key, val in (self.configuration[u"plot-layouts"]
688                                  [layout].items()):
689                     plot[u"layout"][key] = val
690             except KeyError:
691                 raise PresentationError(
692                     f"Layout {layout} is not defined in the "
693                     f"configuration section."
694                 )
695         self._specification[u"plots"].append(plot)
696
697     def _parse_elements_files(self, file):
698         """Parse files from the specification YAML file.
699
700         :param file: File to be parsed from the specification file.
701         :type file: dict
702         """
703
704         try:
705             file[u"dir-tables"] = self._replace_tags(
706                 file[u"dir-tables"],
707                 self._specification[u"environment"][u"paths"])
708         except KeyError:
709             pass
710         self._specification[u"files"].append(file)
711
712     def _parse_elements_cpta(self, cpta):
713         """Parse cpta from the specification YAML file.
714
715         :param cpta: cpta to be parsed from the specification file.
716         :type cpta: dict
717         :raises PresentationError: If wrong data set is used or if plot layout
718             is not defined.
719         """
720
721         for plot in cpta[u"plots"]:
722             # Add layout to the plots:
723             layout = plot.get(u"layout", None)
724             if layout is not None:
725                 try:
726                     plot[u"layout"] = \
727                         self.configuration[u"plot-layouts"][layout]
728                 except KeyError:
729                     raise PresentationError(
730                         f"Layout {layout} is not defined in the "
731                         f"configuration section."
732                     )
733             # Add data sets:
734             if isinstance(plot.get(u"data", None), str):
735                 data_set = plot[u"data"]
736                 try:
737                     plot[u"data"] = \
738                         self.configuration[u"data-sets"][data_set]
739                 except KeyError:
740                     raise PresentationError(
741                         f"Data set {data_set} is not defined in "
742                         f"the configuration section."
743                     )
744         self._specification[u"cpta"] = cpta
745
746     def _parse_elements(self):
747         """Parse elements (tables, plots, ..) specification in the specification
748         YAML file.
749         """
750
751         logging.info(u"Parsing specification file: elements ...")
752
753         count = 1
754         for element in self._cfg_yaml:
755
756             # Replace tags:
757             try:
758                 element[u"output-file"] = self._replace_tags(
759                     element[u"output-file"],
760                     self._specification[u"environment"][u"paths"])
761             except KeyError:
762                 pass
763
764             try:
765                 element[u"input-file"] = self._replace_tags(
766                     element[u"input-file"],
767                     self._specification[u"environment"][u"paths"])
768             except KeyError:
769                 pass
770
771             try:
772                 element[u"output-file-links"] = self._replace_tags(
773                     element[u"output-file-links"],
774                     self._specification[u"environment"][u"paths"])
775             except KeyError:
776                 pass
777
778             # Add data sets to the elements:
779             if isinstance(element.get(u"data", None), str):
780                 data_set = element[u"data"]
781                 try:
782                     element[u"data"] = \
783                         self.configuration[u"data-sets"][data_set]
784                 except KeyError:
785                     raise PresentationError(
786                         f"Data set {data_set} is not defined in the "
787                         f"configuration section."
788                     )
789             elif isinstance(element.get(u"data", None), list):
790                 new_list = list()
791                 for item in element[u"data"]:
792                     try:
793                         new_list.append(
794                             self.configuration[u"data-sets"][item]
795                         )
796                     except KeyError:
797                         raise PresentationError(
798                             f"Data set {item} is not defined in the "
799                             f"configuration section."
800                         )
801                 element[u"data"] = new_list
802
803             # Parse elements:
804             if element[u"type"] == u"table":
805
806                 logging.info(f"  {count:3d} Processing a table ...")
807                 self._parse_elements_tables(element)
808                 count += 1
809
810             elif element[u"type"] == u"plot":
811
812                 logging.info(f"  {count:3d} Processing a plot ...")
813                 self._parse_elements_plots(element)
814                 count += 1
815
816             elif element[u"type"] == u"file":
817
818                 logging.info(f"  {count:3d} Processing a file ...")
819                 self._parse_elements_files(element)
820                 count += 1
821
822             elif element[u"type"] == u"cpta":
823
824                 logging.info(
825                     f"  {count:3d} Processing Continuous Performance Trending "
826                     f"and Analysis ..."
827                 )
828                 self._parse_elements_cpta(element)
829                 count += 1
830
831         logging.info(u"Done.")
832
833     def read_specification(self):
834         """Parse specification in the specification YAML file.
835
836         :raises: PresentationError if an error occurred while parsing the
837             specification file.
838         """
839         try:
840             self._cfg_yaml = load(self._cfg_file, Loader=FullLoader)
841         except YAMLError as err:
842             raise PresentationError(msg=u"An error occurred while parsing the "
843                                         u"specification file.",
844                                     details=repr(err))
845
846         self._parse_env()
847         self._parse_configuration()
848         self._parse_input()
849         self._parse_output()
850         self._parse_static()
851         self._parse_elements()
852
853         logging.debug(f"Specification: \n{pformat(self._specification)}")