Fix conversion to JS for HTML reports
[yardstick.git] / yardstick / benchmark / core / report.py
1 ##############################################################################
2 # Copyright (c) 2017 Rajesh Kudaka <4k.rajesh@gmail.com>
3 # Copyright (c) 2018 Intel Corporation.
4 #
5 # All rights reserved. This program and the accompanying materials
6 # are made available under the terms of the Apache License, Version 2.0
7 # which accompanies this distribution, and is available at
8 # http://www.apache.org/licenses/LICENSE-2.0
9 ##############################################################################
10
11 """ Handler for yardstick command 'report' """
12
13 import re
14 import six
15 import uuid
16
17 import jinja2
18 from api.utils import influx
19 from oslo_utils import uuidutils
20 from yardstick.common import constants as consts
21 from yardstick.common.utils import cliargs
22
23
24 class JSTree(object):
25     """Data structure to parse data for use with the JS library jsTree"""
26     def __init__(self):
27         self._created_nodes = ['#']
28         self.jstree_data = []
29
30     def _create_node(self, _id):
31         """Helper method for format_for_jstree to create each node.
32
33         Creates the node (and any required parents) and keeps track
34         of the created nodes.
35
36         :param _id: (string) id of the node to be created
37         :return: None
38         """
39         components = _id.split(".")
40
41         if len(components) == 1:
42             text = components[0]
43             parent_id = "#"
44         else:
45             text = components[-1]
46             parent_id = ".".join(components[:-1])
47             # make sure the parent has been created
48             if not parent_id in self._created_nodes:
49                 self._create_node(parent_id)
50
51         self.jstree_data.append({"id": _id, "text": text, "parent": parent_id})
52         self._created_nodes.append(_id)
53
54     def format_for_jstree(self, data):
55         """Format the data into the required format for jsTree.
56
57         The data format expected is a list of key-value pairs which represent
58         the data and label for each metric e.g.:
59
60             [{'data': [0, ], 'label': 'tg__0.DropPackets'},
61              {'data': [548, ], 'label': 'tg__0.LatencyAvg.5'},]
62
63         This data is converted into the format required for jsTree to group and
64         display the metrics in a hierarchial fashion, including creating a
65         number of parent nodes e.g.::
66
67             [{"id": "tg__0", "text": "tg__0", "parent": "#"},
68              {"id": "tg__0.DropPackets", "text": "DropPackets", "parent": "tg__0"},
69              {"id": "tg__0.LatencyAvg", "text": "LatencyAvg", "parent": "tg__0"},
70              {"id": "tg__0.LatencyAvg.5", "text": "5", "parent": "tg__0.LatencyAvg"},]
71
72         :param data: (list) data to be converted
73         :return: list
74         """
75         self._created_nodes = ['#']
76         self.jstree_data = []
77
78         for item in data:
79             self._create_node(item["label"])
80
81         return self.jstree_data
82
83
84 class Report(object):
85     """Report commands.
86
87     Set of commands to manage reports.
88     """
89
90     def __init__(self):
91         self.Timestamp = []
92         self.yaml_name = ""
93         self.task_id = ""
94
95     def _validate(self, yaml_name, task_id):
96         if re.match(r"^[\w-]+$", yaml_name):
97             self.yaml_name = yaml_name
98         else:
99             raise ValueError("invalid yaml_name", yaml_name)
100
101         if uuidutils.is_uuid_like(task_id):
102             task_id = '{' + task_id + '}'
103             task_uuid = (uuid.UUID(task_id))
104             self.task_id = task_uuid
105         else:
106             raise ValueError("invalid task_id", task_id)
107
108     def _get_fieldkeys(self):
109         fieldkeys_cmd = "show field keys from \"%s\""
110         fieldkeys_query = fieldkeys_cmd % (self.yaml_name)
111         query_exec = influx.query(fieldkeys_query)
112         if query_exec:
113             return query_exec
114         else:
115             raise KeyError("Test case not found.")
116
117     def _get_metrics(self):
118         metrics_cmd = "select * from \"%s\" where task_id = '%s'"
119         metrics_query = metrics_cmd % (self.yaml_name, self.task_id)
120         query_exec = influx.query(metrics_query)
121         if query_exec:
122             return query_exec
123         else:
124             raise KeyError("Task ID or Test case not found.")
125
126     def _generate_common(self, args):
127         """Actions that are common to both report formats.
128
129         Create the necessary data structure for rendering
130         the report templates.
131         """
132         self._validate(args.yaml_name[0], args.task_id[0])
133
134         db_fieldkeys = self._get_fieldkeys()
135         # list of dicts of:
136         # - PY2: unicode key and unicode value
137         # - PY3: str key and str value
138
139         db_metrics = self._get_metrics()
140         # list of dicts of:
141         # - PY2: unicode key and { None | unicode | float | long | int } value
142         # - PY3: str key and { None | str | float | int } value
143
144         # extract fieldKey entries, and convert them to str where needed
145         field_keys = [key if isinstance(key, str)       # PY3: already str
146                           else key.encode('utf8')       # PY2: unicode to str
147                       for key in
148                           [field['fieldKey']
149                            for field in db_fieldkeys]]
150
151         # extract timestamps
152         self.Timestamp = []
153         for metric in db_metrics:
154             metric_time = metric['time']                    # in RFC3339 format
155             if not isinstance(metric_time, str):
156                 metric_time = metric_time.encode('utf8')    # PY2: unicode to str
157             metric_time = metric_time[11:]                  # skip date, keep time
158             head, _, tail = metric_time.partition('.')      # split HH:MM:SS and nsZ
159             metric_time = head + '.' + tail[:6]             # join HH:MM:SS and .us
160             self.Timestamp.append(metric_time)              # HH:MM:SS.micros
161
162         # prepare return values
163         datasets = []
164         table_vals = {'Timestamp': self.Timestamp}
165
166         # extract and convert field values
167         for key in field_keys:
168             values = []
169             for metric in db_metrics:
170                 val = metric.get(key, None)
171                 if val is None:
172                     # keep explicit None or missing entry as is
173                     pass
174                 elif isinstance(val, (int, float)):
175                     # keep plain int or float as is
176                     pass
177                 elif six.PY2 and isinstance(val,
178                             long):  # pylint: disable=undefined-variable
179                     # PY2: long value would be rendered with trailing L,
180                     # which JS does not support, so convert it to float
181                     val = float(val)
182                 elif isinstance(val, six.string_types):
183                     s = val
184                     if not isinstance(s, str):
185                         s = s.encode('utf8')            # PY2: unicode to str
186                     try:
187                         # convert until failure
188                         val = s
189                         val = float(s)
190                         val = int(s)
191                         if six.PY2 and isinstance(val,
192                                     long):  # pylint: disable=undefined-variable
193                             val = float(val)            # PY2: long to float
194                     except ValueError:
195                         pass
196                 else:
197                     raise ValueError("Cannot convert %r" % val)
198                 values.append(val)
199             datasets.append({'label': key, 'data': values})
200             table_vals[key] = values
201
202         return datasets, table_vals
203
204     @cliargs("task_id", type=str, help=" task id", nargs=1)
205     @cliargs("yaml_name", type=str, help=" Yaml file Name", nargs=1)
206     def generate(self, args):
207         """Start report generation."""
208         datasets, table_vals = self._generate_common(args)
209
210         template_dir = consts.YARDSTICK_ROOT_PATH + "yardstick/common"
211         template_environment = jinja2.Environment(
212             autoescape=False,
213             loader=jinja2.FileSystemLoader(template_dir))
214
215         context = {
216             "datasets": datasets,
217             "Timestamps": self.Timestamp,
218             "task_id": self.task_id,
219             "table": table_vals,
220         }
221
222         template_html = template_environment.get_template("report.html.j2")
223
224         with open(consts.DEFAULT_HTML_FILE, "w") as file_open:
225             file_open.write(template_html.render(context))
226
227         print("Report generated. View %s" % consts.DEFAULT_HTML_FILE)
228
229     @cliargs("task_id", type=str, help=" task id", nargs=1)
230     @cliargs("yaml_name", type=str, help=" Yaml file Name", nargs=1)
231     def generate_nsb(self, args):
232         """Start NSB report generation."""
233         datasets, table_vals = self._generate_common(args)
234         jstree_data = JSTree().format_for_jstree(datasets)
235
236         template_dir = consts.YARDSTICK_ROOT_PATH + "yardstick/common"
237         template_environment = jinja2.Environment(
238             autoescape=False,
239             loader=jinja2.FileSystemLoader(template_dir),
240             lstrip_blocks=True)
241
242         context = {
243             "Timestamps": self.Timestamp,
244             "task_id": self.task_id,
245             "table": table_vals,
246             "jstree_nodes": jstree_data,
247         }
248
249         template_html = template_environment.get_template("nsb_report.html.j2")
250
251         with open(consts.DEFAULT_HTML_FILE, "w") as file_open:
252             file_open.write(template_html.render(context))
253
254         print("Report generated. View %s" % consts.DEFAULT_HTML_FILE)