blob: 28314f6ab82fb7199e126e0d324e150a28006387 [file] [log] [blame]
Dennis Dmitriev6f59add2016-10-18 13:45:27 +03001# Copyright 2016 Mirantis, Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15import copy
16import os
17import shutil
18import tempfile
19import time
20import traceback
21
dis2b2d8632016-12-08 17:56:57 +020022import jinja2
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030023import paramiko
24import yaml
25from devops.helpers import helpers
26from devops.helpers import ssh_client
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030027
28from tcp_tests import logger
29from tcp_tests import settings
30from tcp_tests.helpers import ext
31
32LOG = logger.logger
33
34
35def get_test_method_name():
36 raise NotImplementedError
37
38
39def update_yaml(yaml_tree=None, yaml_value='', is_uniq=True,
40 yaml_file=settings.TIMESTAT_PATH_YAML, remote=None):
41 """Store/update a variable in YAML file.
42
43 yaml_tree - path to the variable in YAML file, will be created if absent,
44 yaml_value - value of the variable, will be overwritten if exists,
45 is_uniq - If false, add the unique two-digit suffix to the variable name.
46 """
47 def get_file(path, remote=None, mode="r"):
48 if remote:
49 return remote.open(path, mode)
50 else:
51 return open(path, mode)
52
53 if yaml_tree is None:
54 yaml_tree = []
55 with get_file(yaml_file, remote) as file_obj:
56 yaml_data = yaml.safe_load(file_obj)
57
58 # Walk through the 'yaml_data' dict, find or create a tree using
59 # sub-keys in order provided in 'yaml_tree' list
60 item = yaml_data
61 for n in yaml_tree[:-1]:
62 if n not in item:
63 item[n] = {}
64 item = item[n]
65
66 if is_uniq:
67 last = yaml_tree[-1]
68 else:
69 # Create an uniq suffix in range '_00' to '_99'
70 for n in range(100):
71 last = str(yaml_tree[-1]) + '_' + str(n).zfill(2)
72 if last not in item:
73 break
74
75 item[last] = yaml_value
76 with get_file(yaml_file, remote, mode='w') as file_obj:
77 yaml.dump(yaml_data, file_obj, default_flow_style=False)
78
79
80class TimeStat(object):
81 """Context manager for measuring the execution time of the code.
82
83 Usage:
84 with TimeStat([name],[is_uniq=True]):
85 """
86
87 def __init__(self, name=None, is_uniq=False):
88 if name:
89 self.name = name
90 else:
91 self.name = 'timestat'
92 self.is_uniq = is_uniq
93 self.begin_time = 0
94 self.end_time = 0
95 self.total_time = 0
96
97 def __enter__(self):
98 self.begin_time = time.time()
99 return self
100
101 def __exit__(self, exc_type, exc_value, exc_tb):
102 self.end_time = time.time()
103 self.total_time = self.end_time - self.begin_time
104
105 # Create a path where the 'self.total_time' will be stored.
106 yaml_path = []
107
108 # There will be a list of one or two yaml subkeys:
109 # - first key name is the method name of the test
110 method_name = get_test_method_name()
111 if method_name:
112 yaml_path.append(method_name)
113
114 # - second (subkey) name is provided from the decorator (the name of
115 # the just executed function), or manually.
116 yaml_path.append(self.name)
117
118 try:
119 update_yaml(yaml_path, '{:.2f}'.format(self.total_time),
120 self.is_uniq)
121 except Exception:
122 LOG.error("Error storing time statistic for {0}"
123 " {1}".format(yaml_path, traceback.format_exc()))
124 raise
125
126 @property
127 def spent_time(self):
128 return time.time() - self.begin_time
129
130
131def reduce_occurrences(items, text):
132 """ Return string without items(substrings)
133 Args:
134 items: iterable of strings
135 test: string
136 Returns:
137 string
138 Raise:
139 AssertionError if any substing not present in source text
140 """
141 for item in items:
142 LOG.debug(
143 "Verifying string {} is shown in "
144 "\"\"\"\n{}\n\"\"\"".format(item, text))
145 assert text.count(item) != 0
146 text = text.replace(item, "", 1)
147 return text
148
149
150def generate_keys():
151 key = paramiko.RSAKey.generate(1024)
152 public = key.get_base64()
153 dirpath = tempfile.mkdtemp()
154 key.write_private_key_file(os.path.join(dirpath, 'id_rsa'))
155 with open(os.path.join(dirpath, 'id_rsa.pub'), 'w') as pub_file:
156 pub_file.write(public)
157 return dirpath
158
159
160def clean_dir(dirpath):
161 shutil.rmtree(dirpath)
162
163
Tatyana Leontovich81128412017-04-05 18:46:29 +0300164def retry(tries_number=2, exception=Exception):
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300165 def _retry(func):
166 assert tries_number >= 1, 'ERROR! @retry is called with no tries!'
167
168 def wrapper(*args, **kwargs):
169 iter_number = 1
170 while True:
171 try:
172 LOG.debug('Calling function "{0}" with args "{1}" and '
173 'kwargs "{2}". Try # {3}.'.format(func.__name__,
174 args,
175 kwargs,
176 iter_number))
177 return func(*args, **kwargs)
178 except exception as e:
179 if iter_number > tries_number:
180 LOG.debug('Failed to execute function "{0}" with {1} '
181 'tries!'.format(func.__name__, tries_number))
182 raise e
183 iter_number += 1
184 return wrapper
185 return _retry
186
187
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300188class YamlEditor(object):
189 """Manipulations with local or remote .yaml files.
190
191 Usage:
192
193 with YamlEditor("tasks.yaml") as editor:
194 editor.content[key] = "value"
195
196 with YamlEditor("astute.yaml", ip=self.admin_ip) as editor:
197 editor.content[key] = "value"
198 """
199
200 def __init__(self, file_path, host=None, port=None,
201 username=None, password=None, private_keys=None,
202 document_id=0,
203 default_flow_style=False, default_style=None):
204 self.__file_path = file_path
205 self.host = host
206 self.port = port or 22
207 self.username = username
208 self.__password = password
209 self.__private_keys = private_keys or []
210 self.__content = None
211 self.__documents = [{}, ]
212 self.__document_id = document_id
213 self.__original_content = None
214 self.default_flow_style = default_flow_style
215 self.default_style = default_style
216
217 @property
218 def file_path(self):
219 """Open file path
220
221 :rtype: str
222 """
223 return self.__file_path
224
225 @property
226 def content(self):
227 if self.__content is None:
228 self.__content = self.get_content()
229 return self.__content
230
231 @content.setter
232 def content(self, new_content):
233 self.__content = new_content
234
235 def __get_file(self, mode="r"):
236 if self.host:
237 remote = ssh_client.SSHClient(
238 host=self.host,
239 port=self.port,
240 username=self.username,
241 password=self.__password,
242 private_keys=self.__private_keys)
243
244 return remote.open(self.__file_path, mode=mode)
245 else:
246 return open(self.__file_path, mode=mode)
247
248 def get_content(self):
249 """Return a single document from YAML"""
250 def multi_constructor(loader, tag_suffix, node):
251 """Stores all unknown tags content into a dict
252
253 Original yaml:
254 !unknown_tag
255 - some content
256
257 Python object:
258 {"!unknown_tag": ["some content", ]}
259 """
260 if type(node.value) is list:
261 if type(node.value[0]) is tuple:
262 return {node.tag: loader.construct_mapping(node)}
263 else:
264 return {node.tag: loader.construct_sequence(node)}
265 else:
266 return {node.tag: loader.construct_scalar(node)}
267
268 yaml.add_multi_constructor("!", multi_constructor)
269 with self.__get_file() as file_obj:
270 self.__documents = [x for x in yaml.load_all(file_obj)]
271 return self.__documents[self.__document_id]
272
273 def write_content(self, content=None):
274 if content:
275 self.content = content
276 self.__documents[self.__document_id] = self.content
277
278 def representer(dumper, data):
279 """Represents a dict key started with '!' as a YAML tag
280
281 Assumes that there is only one !tag in the dict at the
282 current indent.
283
284 Python object:
285 {"!unknown_tag": ["some content", ]}
286
287 Resulting yaml:
288 !unknown_tag
289 - some content
290 """
291 key = data.keys()[0]
292 if key.startswith("!"):
293 value = data[key]
294 if type(value) is dict:
295 node = dumper.represent_mapping(key, value)
296 elif type(value) is list:
297 node = dumper.represent_sequence(key, value)
298 else:
299 node = dumper.represent_scalar(key, value)
300 else:
301 node = dumper.represent_mapping(u'tag:yaml.org,2002:map', data)
302 return node
303
304 yaml.add_representer(dict, representer)
305 with self.__get_file("w") as file_obj:
306 yaml.dump_all(self.__documents, file_obj,
307 default_flow_style=self.default_flow_style,
308 default_style=self.default_style)
309
310 def __enter__(self):
311 self.__content = self.get_content()
312 self.__original_content = copy.deepcopy(self.content)
313 return self
314
315 def __exit__(self, x, y, z):
316 if self.content == self.__original_content:
317 return
318 self.write_content()
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200319
320
dis2b2d8632016-12-08 17:56:57 +0200321def render_template(file_path):
322 required_env_vars = set()
323 optional_env_vars = dict()
324 def os_env(var_name, default=None):
325 var = os.environ.get(var_name, default)
326
327 if var is None:
328 raise Exception("Environment variable '{0}' is undefined!".format(var_name))
329
330 if default is None:
331 required_env_vars.add(var_name)
332 else:
333 optional_env_vars[var_name] = default
334
335 return var
336
337 options = {
338 'os_env': os_env,
339 }
340 LOG.info("Reading template {0}".format(file_path))
341
342 path, filename = os.path.split(file_path)
343 environment = jinja2.Environment(
344 loader=jinja2.FileSystemLoader([path, os.path.dirname(path)], followlinks=True))
345 template = environment.get_template(filename).render(options)
346
347 if required_env_vars:
348 LOG.info("Required environment variables:")
349 for var in required_env_vars:
350 LOG.info(" {0}".format(var))
351 if optional_env_vars:
352 LOG.info("Optional environment variables:")
353 for var, default in sorted(optional_env_vars.iteritems()):
354 LOG.info(" {0} , default value = {1}".format(var, default))
355 return template
356
357
358def read_template(file_path):
359 """Read yaml as a jinja template"""
360 template = render_template(file_path)
361 return yaml.load(template)
362
363
Dennis Dmitriev535869c2016-11-16 22:38:06 +0200364def extract_name_from_mark(mark):
365 """Simple function to extract name from pytest mark
366
367 :param mark: pytest.mark.MarkInfo
368 :rtype: string or None
369 """
370 if mark:
371 if len(mark.args) > 0:
372 return mark.args[0]
373 elif 'name' in mark.kwargs:
374 return mark.kwargs['name']
375 return None
376
377
378def get_top_fixtures_marks(request, mark_name):
379 """Order marks according to fixtures order
380
381 When a test use fixtures that depend on each other in some order,
382 that fixtures can have the same pytest mark.
383
384 This method extracts such marks from fixtures that are used in the
385 current test and return the content of the marks ordered by the
386 fixture dependences.
387 If the test case have the same mark, than the content of this mark
388 will be the first element in the resulting list.
389
390 :param request: pytest 'request' fixture
391 :param mark_name: name of the mark to search on the fixtures and the test
392
393 :rtype list: marks content, from last to first executed.
394 """
395
396 fixtureinfo = request.session._fixturemanager.getfixtureinfo(
397 request.node, request.function, request.cls)
398
399 top_fixtures_names = []
400 for _ in enumerate(fixtureinfo.name2fixturedefs):
401 parent_fixtures = set()
402 child_fixtures = set()
403 for name in sorted(fixtureinfo.name2fixturedefs):
404 if name in top_fixtures_names:
405 continue
406 parent_fixtures.add(name)
407 child_fixtures.update(
408 fixtureinfo.name2fixturedefs[name][0].argnames)
409 top_fixtures_names.extend(list(parent_fixtures - child_fixtures))
410
411 top_fixtures_marks = []
412
413 if mark_name in request.function.func_dict:
414 # The top priority is the 'revert_snapshot' mark on the test
415 top_fixtures_marks.append(
416 extract_name_from_mark(
417 request.function.func_dict[mark_name]))
418
419 for top_fixtures_name in top_fixtures_names:
420 fd = fixtureinfo.name2fixturedefs[top_fixtures_name][0]
421 if mark_name in fd.func.func_dict:
422 fixture_mark = extract_name_from_mark(
423 fd.func.func_dict[mark_name])
424 # Append the snapshot names in the order that fixtures are called
425 # starting from the last called fixture to the first one
426 top_fixtures_marks.append(fixture_mark)
427
428 LOG.debug("Fixtures ordered from last to first called: {0}"
429 .format(top_fixtures_names))
430 LOG.debug("Marks ordered from most to least preffered: {0}"
431 .format(top_fixtures_marks))
432
433 return top_fixtures_marks