Report: fix performance comparision tables
[csit.git] / resources / tools / presentation / utils.py
1 # Copyright (c) 2017 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """General purpose utilities.
15 """
16
17 import subprocess
18 import numpy as np
19 import pandas as pd
20 import logging
21
22 from os import walk, makedirs, environ
23 from os.path import join, isdir
24 from shutil import copy, Error
25 from math import sqrt
26
27 from errors import PresentationError
28
29
30 def mean(items):
31     """Calculate mean value from the items.
32
33     :param items: Mean value is calculated from these items.
34     :type items: list
35     :returns: MEan value.
36     :rtype: float
37     """
38
39     return float(sum(items)) / len(items)
40
41
42 def stdev(items):
43     """Calculate stdev from the items.
44
45     :param items: Stdev is calculated from these items.
46     :type items: list
47     :returns: Stdev.
48     :rtype: float
49     """
50
51     avg = mean(items)
52     variance = [(x - avg) ** 2 for x in items]
53     stddev = sqrt(mean(variance))
54     return stddev
55
56
57 def relative_change(nr1, nr2):
58     """Compute relative change of two values.
59
60     :param nr1: The first number.
61     :param nr2: The second number.
62     :type nr1: float
63     :type nr2: float
64     :returns: Relative change of nr1.
65     :rtype: float
66     """
67
68     return float(((nr2 - nr1) / nr1) * 100)
69
70
71 def remove_outliers(input_list, outlier_const=1.5, window=14):
72     """Return list with outliers removed, using split_outliers.
73
74     :param input_list: Data from which the outliers will be removed.
75     :param outlier_const: Outlier constant.
76     :param window: How many preceding values to take into account.
77     :type input_list: list of floats
78     :type outlier_const: float
79     :type window: int
80     :returns: The input list without outliers.
81     :rtype: list of floats
82     """
83
84     input_series = pd.Series()
85     for index, value in enumerate(input_list):
86         item_pd = pd.Series([value, ], index=[index, ])
87         input_series.append(item_pd)
88     output_series, _ = split_outliers(input_series, outlier_const=outlier_const,
89                                       window=window)
90     output_list = [y for x, y in output_series.items() if not np.isnan(y)]
91
92     return output_list
93
94
95 def split_outliers(input_series, outlier_const=1.5, window=14):
96     """Go through the input data and generate two pandas series:
97     - input data with outliers replaced by NAN
98     - outliers.
99     The function uses IQR to detect outliers.
100
101     :param input_series: Data to be examined for outliers.
102     :param outlier_const: Outlier constant.
103     :param window: How many preceding values to take into account.
104     :type input_series: pandas.Series
105     :type outlier_const: float
106     :type window: int
107     :returns: Input data with NAN outliers and Outliers.
108     :rtype: (pandas.Series, pandas.Series)
109     """
110
111     list_data = list(input_series.items())
112     head_size = min(window, len(list_data))
113     head_list = list_data[:head_size]
114     trimmed_data = pd.Series()
115     outliers = pd.Series()
116     for item_x, item_y in head_list:
117         item_pd = pd.Series([item_y, ], index=[item_x, ])
118         trimmed_data = trimmed_data.append(item_pd)
119     for index, (item_x, item_y) in list(enumerate(list_data))[head_size:]:
120         y_rolling_list = [y for (x, y) in list_data[index - head_size:index]]
121         y_rolling_array = np.array(y_rolling_list)
122         q1 = np.percentile(y_rolling_array, 25)
123         q3 = np.percentile(y_rolling_array, 75)
124         iqr = (q3 - q1) * outlier_const
125         low, high = q1 - iqr, q3 + iqr
126         item_pd = pd.Series([item_y, ], index=[item_x, ])
127         if low <= item_y <= high:
128             trimmed_data = trimmed_data.append(item_pd)
129         else:
130             outliers = outliers.append(item_pd)
131             nan_pd = pd.Series([np.nan, ], index=[item_x, ])
132             trimmed_data = trimmed_data.append(nan_pd)
133
134     return trimmed_data, outliers
135
136
137 def get_files(path, extension=None, full_path=True):
138     """Generates the list of files to process.
139
140     :param path: Path to files.
141     :param extension: Extension of files to process. If it is the empty string,
142     all files will be processed.
143     :param full_path: If True, the files with full path are generated.
144     :type path: str
145     :type extension: str
146     :type full_path: bool
147     :returns: List of files to process.
148     :rtype: list
149     """
150
151     file_list = list()
152     for root, _, files in walk(path):
153         for filename in files:
154             if extension:
155                 if filename.endswith(extension):
156                     if full_path:
157                         file_list.append(join(root, filename))
158                     else:
159                         file_list.append(filename)
160             else:
161                 file_list.append(join(root, filename))
162
163     return file_list
164
165
166 def get_rst_title_char(level):
167     """Return character used for the given title level in rst files.
168
169     :param level: Level of the title.
170     :type: int
171     :returns: Character used for the given title level in rst files.
172     :rtype: str
173     """
174     chars = ('=', '-', '`', "'", '.', '~', '*', '+', '^')
175     if level < len(chars):
176         return chars[level]
177     else:
178         return chars[-1]
179
180
181 def execute_command(cmd):
182     """Execute the command in a subprocess and log the stdout and stderr.
183
184     :param cmd: Command to execute.
185     :type cmd: str
186     :returns: Return code of the executed command.
187     :rtype: int
188     """
189
190     env = environ.copy()
191     proc = subprocess.Popen(
192         [cmd],
193         stdout=subprocess.PIPE,
194         stderr=subprocess.PIPE,
195         shell=True,
196         env=env)
197
198     stdout, stderr = proc.communicate()
199
200     logging.info(stdout)
201     logging.info(stderr)
202
203     if proc.returncode != 0:
204         logging.error("    Command execution failed.")
205     return proc.returncode, stdout, stderr
206
207
208 def get_last_successful_build_number(jenkins_url, job_name):
209     """Get the number of the last successful build of the given job.
210
211     :param jenkins_url: Jenkins URL.
212     :param job_name: Job name.
213     :type jenkins_url: str
214     :type job_name: str
215     :returns: The build number as a string.
216     :rtype: str
217     """
218
219     url = "{}/{}/lastSuccessfulBuild/buildNumber".format(jenkins_url, job_name)
220     cmd = "wget -qO- {url}".format(url=url)
221
222     return execute_command(cmd)
223
224
225 def get_last_completed_build_number(jenkins_url, job_name):
226     """Get the number of the last completed build of the given job.
227
228     :param jenkins_url: Jenkins URL.
229     :param job_name: Job name.
230     :type jenkins_url: str
231     :type job_name: str
232     :returns: The build number as a string.
233     :rtype: str
234     """
235
236     url = "{}/{}/lastCompletedBuild/buildNumber".format(jenkins_url, job_name)
237     cmd = "wget -qO- {url}".format(url=url)
238
239     return execute_command(cmd)
240
241
242 def archive_input_data(spec):
243     """Archive the report.
244
245     :param spec: Specification read from the specification file.
246     :type spec: Specification
247     :raises PresentationError: If it is not possible to archive the input data.
248     """
249
250     logging.info("    Archiving the input data files ...")
251
252     if spec.is_debug:
253         extension = spec.debug["input-format"]
254     else:
255         extension = spec.input["file-format"]
256     data_files = get_files(spec.environment["paths"]["DIR[WORKING,DATA]"],
257                            extension=extension)
258     dst = spec.environment["paths"]["DIR[STATIC,ARCH]"]
259     logging.info("      Destination: {0}".format(dst))
260
261     try:
262         if not isdir(dst):
263             makedirs(dst)
264
265         for data_file in data_files:
266             logging.info("      Copying the file: {0} ...".format(data_file))
267             copy(data_file, dst)
268
269     except (Error, OSError) as err:
270         raise PresentationError("Not possible to archive the input data.",
271                                 str(err))
272
273     logging.info("    Done.")