FIX: Remove old restart sequence - Honeycomb
[csit.git] / resources / tools / presentation / utils.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 """General purpose utilities.
15 """
16
17 import multiprocessing
18 import subprocess
19 import numpy as np
20 import logging
21 import csv
22 import prettytable
23
24 from os import walk, makedirs, environ
25 from os.path import join, isdir
26 from shutil import move, Error
27 from math import sqrt
28 from datetime import datetime
29
30 from errors import PresentationError
31 from jumpavg.BitCountingClassifier import BitCountingClassifier
32
33
34 def mean(items):
35     """Calculate mean value from the items.
36
37     :param items: Mean value is calculated from these items.
38     :type items: list
39     :returns: MEan value.
40     :rtype: float
41     """
42
43     return float(sum(items)) / len(items)
44
45
46 def stdev(items):
47     """Calculate stdev from the items.
48
49     :param items: Stdev is calculated from these items.
50     :type items: list
51     :returns: Stdev.
52     :rtype: float
53     """
54
55     avg = mean(items)
56     variance = [(x - avg) ** 2 for x in items]
57     stddev = sqrt(mean(variance))
58     return stddev
59
60
61 def relative_change(nr1, nr2):
62     """Compute relative change of two values.
63
64     :param nr1: The first number.
65     :param nr2: The second number.
66     :type nr1: float
67     :type nr2: float
68     :returns: Relative change of nr1.
69     :rtype: float
70     """
71
72     return float(((nr2 - nr1) / nr1) * 100)
73
74
75 def get_files(path, extension=None, full_path=True):
76     """Generates the list of files to process.
77
78     :param path: Path to files.
79     :param extension: Extension of files to process. If it is the empty string,
80         all files will be processed.
81     :param full_path: If True, the files with full path are generated.
82     :type path: str
83     :type extension: str
84     :type full_path: bool
85     :returns: List of files to process.
86     :rtype: list
87     """
88
89     file_list = list()
90     for root, _, files in walk(path):
91         for filename in files:
92             if extension:
93                 if filename.endswith(extension):
94                     if full_path:
95                         file_list.append(join(root, filename))
96                     else:
97                         file_list.append(filename)
98             else:
99                 file_list.append(join(root, filename))
100
101     return file_list
102
103
104 def get_rst_title_char(level):
105     """Return character used for the given title level in rst files.
106
107     :param level: Level of the title.
108     :type: int
109     :returns: Character used for the given title level in rst files.
110     :rtype: str
111     """
112     chars = ('=', '-', '`', "'", '.', '~', '*', '+', '^')
113     if level < len(chars):
114         return chars[level]
115     else:
116         return chars[-1]
117
118
119 def execute_command(cmd):
120     """Execute the command in a subprocess and log the stdout and stderr.
121
122     :param cmd: Command to execute.
123     :type cmd: str
124     :returns: Return code of the executed command, stdout and stderr.
125     :rtype: tuple(int, str, str)
126     """
127
128     env = environ.copy()
129     proc = subprocess.Popen(
130         [cmd],
131         stdout=subprocess.PIPE,
132         stderr=subprocess.PIPE,
133         shell=True,
134         env=env)
135
136     stdout, stderr = proc.communicate()
137
138     if stdout:
139         logging.info(stdout)
140     if stderr:
141         logging.info(stderr)
142
143     if proc.returncode != 0:
144         logging.error("    Command execution failed.")
145     return proc.returncode, stdout, stderr
146
147
148 def get_last_successful_build_number(jenkins_url, job_name):
149     """Get the number of the last successful build of the given job.
150
151     :param jenkins_url: Jenkins URL.
152     :param job_name: Job name.
153     :type jenkins_url: str
154     :type job_name: str
155     :returns: The build number as a string.
156     :rtype: str
157     """
158
159     url = "{}/{}/lastSuccessfulBuild/buildNumber".format(jenkins_url, job_name)
160     cmd = "wget -qO- {url}".format(url=url)
161
162     return execute_command(cmd)
163
164
165 def get_last_completed_build_number(jenkins_url, job_name):
166     """Get the number of the last completed build of the given job.
167
168     :param jenkins_url: Jenkins URL.
169     :param job_name: Job name.
170     :type jenkins_url: str
171     :type job_name: str
172     :returns: The build number as a string.
173     :rtype: str
174     """
175
176     url = "{}/{}/lastCompletedBuild/buildNumber".format(jenkins_url, job_name)
177     cmd = "wget -qO- {url}".format(url=url)
178
179     return execute_command(cmd)
180
181
182 def get_build_timestamp(jenkins_url, job_name, build_nr):
183     """Get the timestamp of the build of the given job.
184
185     :param jenkins_url: Jenkins URL.
186     :param job_name: Job name.
187     :param build_nr: Build number.
188     :type jenkins_url: str
189     :type job_name: str
190     :type build_nr: int
191     :returns: The timestamp.
192     :rtype: datetime.datetime
193     """
194
195     url = "{jenkins_url}/{job_name}/{build_nr}".format(jenkins_url=jenkins_url,
196                                                        job_name=job_name,
197                                                        build_nr=build_nr)
198     cmd = "wget -qO- {url}".format(url=url)
199
200     timestamp = execute_command(cmd)
201
202     return datetime.fromtimestamp(timestamp/1000)
203
204
205 def archive_input_data(spec):
206     """Archive the report.
207
208     :param spec: Specification read from the specification file.
209     :type spec: Specification
210     :raises PresentationError: If it is not possible to archive the input data.
211     """
212
213     logging.info("    Archiving the input data files ...")
214
215     extension = spec.input["file-format"]
216     data_files = get_files(spec.environment["paths"]["DIR[WORKING,DATA]"],
217                            extension=extension)
218     dst = spec.environment["paths"]["DIR[STATIC,ARCH]"]
219     logging.info("      Destination: {0}".format(dst))
220
221     try:
222         if not isdir(dst):
223             makedirs(dst)
224
225         for data_file in data_files:
226             logging.info("      Moving the file: {0} ...".format(data_file))
227             move(data_file, dst)
228
229     except (Error, OSError) as err:
230         raise PresentationError("Not possible to archive the input data.",
231                                 str(err))
232
233     logging.info("    Done.")
234
235
236 def classify_anomalies(data):
237     """Process the data and return anomalies and trending values.
238
239     Gather data into groups with average as trend value.
240     Decorate values within groups to be normal,
241     the first value of changed average as a regression, or a progression.
242
243     :param data: Full data set with unavailable samples replaced by nan.
244     :type data: OrderedDict
245     :returns: Classification and trend values
246     :rtype: 2-tuple, list of strings and list of floats
247     """
248     # Nan mean something went wrong.
249     # Use 0.0 to cause that being reported as a severe regression.
250     bare_data = [0.0 if np.isnan(sample.avg) else sample
251                  for _, sample in data.iteritems()]
252     # TODO: Put analogous iterator into jumpavg library.
253     groups = BitCountingClassifier().classify(bare_data)
254     groups.reverse()  # Just to use .pop() for FIFO.
255     classification = []
256     avgs = []
257     active_group = None
258     values_left = 0
259     avg = 0.0
260     for _, sample in data.iteritems():
261         if np.isnan(sample.avg):
262             classification.append("outlier")
263             avgs.append(sample.avg)
264             continue
265         if values_left < 1 or active_group is None:
266             values_left = 0
267             while values_left < 1:  # Ignore empty groups (should not happen).
268                 active_group = groups.pop()
269                 values_left = len(active_group.values)
270             avg = active_group.metadata.avg
271             classification.append(active_group.metadata.classification)
272             avgs.append(avg)
273             values_left -= 1
274             continue
275         classification.append("normal")
276         avgs.append(avg)
277         values_left -= 1
278     return classification, avgs
279
280
281 def convert_csv_to_pretty_txt(csv_file, txt_file):
282     """Convert the given csv table to pretty text table.
283
284     :param csv_file: The path to the input csv file.
285     :param txt_file: The path to the output pretty text file.
286     :type csv_file: str
287     :type txt_file: str
288     """
289
290     txt_table = None
291     with open(csv_file, 'rb') as csv_file:
292         csv_content = csv.reader(csv_file, delimiter=',', quotechar='"')
293         for row in csv_content:
294             if txt_table is None:
295                 txt_table = prettytable.PrettyTable(row)
296             else:
297                 txt_table.add_row(row)
298         txt_table.align["Test case"] = "l"
299     if txt_table:
300         with open(txt_file, "w") as txt_file:
301             txt_file.write(str(txt_table))
302
303
304 class Worker(multiprocessing.Process):
305     """Worker class used to process tasks in separate parallel processes.
306     """
307
308     def __init__(self, work_queue, data_queue, func):
309         """Initialization.
310
311         :param work_queue: Queue with items to process.
312         :param data_queue: Shared memory between processes. Queue which keeps
313             the result data. This data is then read by the main process and used
314             in further processing.
315         :param func: Function which is executed by the worker.
316         :type work_queue: multiprocessing.JoinableQueue
317         :type data_queue: multiprocessing.Manager().Queue()
318         :type func: Callable object
319         """
320         super(Worker, self).__init__()
321         self._work_queue = work_queue
322         self._data_queue = data_queue
323         self._func = func
324
325     def run(self):
326         """Method representing the process's activity.
327         """
328
329         while True:
330             try:
331                 self.process(self._work_queue.get())
332             finally:
333                 self._work_queue.task_done()
334
335     def process(self, item_to_process):
336         """Method executed by the runner.
337
338         :param item_to_process: Data to be processed by the function.
339         :type item_to_process: tuple
340         """
341         self._func(self.pid, self._data_queue, *item_to_process)