3 # Copyright (c) 2016 Orange and others.
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
10 """Define the parent class of all Xtesting TestCases."""
13 from datetime import datetime
22 from boto3.s3.transfer import TransferConfig
27 from six.moves import urllib
29 from xtesting.utils import decorators
30 from xtesting.utils import env
32 __author__ = "Cedric Ollivier <cedric.ollivier@orange.com>"
35 @six.add_metaclass(abc.ABCMeta)
37 # pylint: disable=too-many-instance-attributes
38 """Base model for single test case."""
41 """everything is OK"""
43 EX_RUN_ERROR = os.EX_SOFTWARE
46 EX_PUSH_TO_DB_ERROR = os.EX_SOFTWARE - 1
47 """push_to_db() failed"""
49 EX_TESTCASE_FAILED = os.EX_SOFTWARE - 2
50 """results are false"""
52 EX_TESTCASE_SKIPPED = os.EX_SOFTWARE - 3
53 """requirements are unmet"""
55 EX_PUBLISH_ARTIFACTS_ERROR = os.EX_SOFTWARE - 4
56 """publish_artifacts() failed"""
58 dir_results = "/var/lib/xtesting/results"
59 _job_name_rule = "(dai|week)ly-(.+?)-[0-9]*"
60 headers = {'Content-Type': 'application/json'}
61 __logger = logging.getLogger(__name__)
63 def __init__(self, **kwargs):
65 self.project_name = kwargs.get('project_name', 'xtesting')
66 self.case_name = kwargs.get('case_name', '')
67 self.criteria = kwargs.get('criteria', 100)
71 self.is_skipped = False
72 self.output_log_name = 'xtesting.log'
73 self.output_debug_log_name = 'xtesting.debug.log'
74 self.res_dir = "{}/{}".format(self.dir_results, self.case_name)
78 assert self.project_name
83 result = 'PASS' if(self.is_successful(
84 ) == TestCase.EX_OK) else 'FAIL'
85 msg = prettytable.PrettyTable(
86 header_style='upper', padding_width=5,
87 field_names=['test case', 'project', 'duration',
89 msg.add_row([self.case_name, self.project_name,
90 self.get_duration(), result])
91 return msg.get_string()
92 except AssertionError:
93 self.__logger.error("We cannot print invalid objects")
94 return super(TestCase, self).__str__()
96 def get_duration(self):
97 """Return the duration of the test case.
100 duration if start_time and stop_time are set
106 assert self.start_time
107 assert self.stop_time
108 if self.stop_time < self.start_time:
110 return "{0[0]:02.0f}:{0[1]:02.0f}".format(divmod(
111 self.stop_time - self.start_time, 60))
112 except Exception: # pylint: disable=broad-except
113 self.__logger.error("Please run test before getting the duration")
116 def is_successful(self):
117 """Interpret the result of the test case.
119 It allows getting the result of TestCase. It completes run()
120 which only returns the execution status.
122 It can be overriden if checking result is not suitable.
125 TestCase.EX_OK if result is 'PASS'.
126 TestCase.EX_TESTCASE_SKIPPED if test case is skipped.
127 TestCase.EX_TESTCASE_FAILED otherwise.
131 return TestCase.EX_TESTCASE_SKIPPED
133 assert self.result is not None
134 if (not isinstance(self.result, str) and
135 not isinstance(self.criteria, str)):
136 if self.result >= self.criteria:
137 return TestCase.EX_OK
139 # Backward compatibility
140 # It must be removed as soon as TestCase subclasses
141 # stop setting result = 'PASS' or 'FAIL'.
142 # In this case criteria is unread.
143 self.__logger.warning(
144 "Please update result which must be an int!")
145 if self.result == 'PASS':
146 return TestCase.EX_OK
147 except AssertionError:
148 self.__logger.error("Please run test before checking the results")
149 return TestCase.EX_TESTCASE_FAILED
151 def check_requirements(self): # pylint: disable=no-self-use
152 """Check the requirements of the test case.
154 It can be overriden on purpose.
156 self.is_skipped = False
159 def run(self, **kwargs):
160 """Run the test case.
162 It allows running TestCase and getting its execution
165 The subclasses must override the default implementation which
168 The new implementation must set the following attributes to
169 push the results to DB:
176 kwargs: Arbitrary keyword arguments.
179 @decorators.can_dump_request_to_file
180 def push_to_db(self):
181 """Push the results of the test case to the DB.
183 It allows publishing the results and checking the status.
185 It could be overriden if the common implementation is not
188 The following attributes must be set before pushing the results to DB:
196 The next vars must be set in env:
205 TestCase.EX_OK if results were pushed to DB.
206 TestCase.EX_PUSH_TO_DB_ERROR otherwise.
210 return TestCase.EX_PUSH_TO_DB_ERROR
211 assert self.project_name
212 assert self.case_name
213 assert self.start_time
214 assert self.stop_time
215 url = env.get('TEST_DB_URL')
216 data = {"project_name": self.project_name,
217 "case_name": self.case_name,
218 "details": self.details}
219 data["installer"] = env.get('INSTALLER_TYPE')
220 data["scenario"] = env.get('DEPLOY_SCENARIO')
221 data["pod_name"] = env.get('NODE_NAME')
222 data["build_tag"] = env.get('BUILD_TAG')
223 data["criteria"] = 'PASS' if self.is_successful(
224 ) == TestCase.EX_OK else 'FAIL'
225 data["start_date"] = datetime.fromtimestamp(
226 self.start_time).strftime('%Y-%m-%d %H:%M:%S')
227 data["stop_date"] = datetime.fromtimestamp(
228 self.stop_time).strftime('%Y-%m-%d %H:%M:%S')
230 data["version"] = re.search(
231 TestCase._job_name_rule,
232 env.get('BUILD_TAG')).group(2)
233 except Exception: # pylint: disable=broad-except
234 data["version"] = "unknown"
236 url, data=json.dumps(data, sort_keys=True),
237 headers=self.headers)
238 req.raise_for_status()
239 if urllib.parse.urlparse(url).scheme != "file":
240 # href must be postprocessed as OPNFV testapi is misconfigured
241 # (localhost is returned)
242 uid = re.sub(r'^.*/api/v1/results/*', '', req.json()["href"])
243 netloc = env.get('TEST_DB_EXT_URL') if env.get(
244 'TEST_DB_EXT_URL') else env.get('TEST_DB_URL')
246 "The results were successfully pushed to DB: \n\n%s\n",
247 os.path.join(netloc, uid))
248 except AssertionError:
249 self.__logger.exception(
250 "Please run test before publishing the results")
251 return TestCase.EX_PUSH_TO_DB_ERROR
252 except requests.exceptions.HTTPError:
253 self.__logger.exception("The HTTP request raises issues")
254 return TestCase.EX_PUSH_TO_DB_ERROR
255 except Exception: # pylint: disable=broad-except
256 self.__logger.exception("The results cannot be pushed to DB")
257 return TestCase.EX_PUSH_TO_DB_ERROR
258 return TestCase.EX_OK
260 def publish_artifacts(self): # pylint: disable=too-many-locals
261 """Push the artifacts to the S3 repository.
263 It allows publishing the artifacts.
265 It could be overriden if the common implementation is not
268 The credentials must be configured before publishing the artifacts:
270 * fill ~/.aws/credentials or ~/.boto,
271 * set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in env.
273 The next vars must be set in env:
275 * S3_ENDPOINT_URL (http://127.0.0.1:9000),
276 * S3_DST_URL (s3://xtesting/prefix),
277 * HTTP_DST_URL (http://127.0.0.1/prefix).
280 TestCase.EX_OK if artifacts were published to repository.
281 TestCase.EX_PUBLISH_ARTIFACTS_ERROR otherwise.
284 b3resource = boto3.resource(
285 's3', endpoint_url=os.environ["S3_ENDPOINT_URL"])
286 dst_s3_url = os.environ["S3_DST_URL"]
287 multipart_threshold = 5 * 1024 ** 5 if "google" in os.environ[
288 "S3_ENDPOINT_URL"] else 8 * 1024 * 1024
289 config = TransferConfig(multipart_threshold=multipart_threshold)
290 bucket_name = urllib.parse.urlparse(dst_s3_url).netloc
292 b3resource.meta.client.head_bucket(Bucket=bucket_name)
293 except botocore.exceptions.ClientError as exc:
294 error_code = exc.response['Error']['Code']
295 if error_code == '404':
296 # pylint: disable=no-member
297 b3resource.create_bucket(Bucket=bucket_name)
299 typ, value, traceback = sys.exc_info()
300 six.reraise(typ, value, traceback)
301 except Exception: # pylint: disable=broad-except
302 typ, value, traceback = sys.exc_info()
303 six.reraise(typ, value, traceback)
304 path = urllib.parse.urlparse(dst_s3_url).path.strip("/")
305 dst_http_url = os.environ["HTTP_DST_URL"]
307 self.details["links"] = []
308 for log_file in [self.output_log_name, self.output_debug_log_name]:
309 if os.path.exists(os.path.join(self.dir_results, log_file)):
310 abs_file = os.path.join(self.dir_results, log_file)
311 mime_type = mimetypes.guess_type(abs_file)
313 "Publishing %s %s", abs_file, mime_type)
314 # pylint: disable=no-member
315 b3resource.Bucket(bucket_name).upload_file(
316 abs_file, os.path.join(path, log_file), Config=config,
317 ExtraArgs={'ContentType': mime_type[
318 0] or 'application/octet-stream'})
319 link = os.path.join(dst_http_url, log_file)
320 output_str += "\n{}".format(link)
321 self.details["links"].append(link)
322 for root, _, files in os.walk(self.res_dir):
323 for pub_file in files:
324 abs_file = os.path.join(root, pub_file)
325 mime_type = mimetypes.guess_type(abs_file)
327 "Publishing %s %s", abs_file, mime_type)
328 # pylint: disable=no-member
329 b3resource.Bucket(bucket_name).upload_file(
331 os.path.join(path, os.path.relpath(
332 os.path.join(root, pub_file),
333 start=self.dir_results)),
335 ExtraArgs={'ContentType': mime_type[
336 0] or 'application/octet-stream'})
337 link = os.path.join(dst_http_url, os.path.relpath(
338 os.path.join(root, pub_file),
339 start=self.dir_results))
340 output_str += "\n{}".format(link)
341 self.details["links"].append(link)
343 "All artifacts were successfully published: %s\n", output_str)
344 return TestCase.EX_OK
345 except KeyError as ex:
346 self.__logger.error("Please check env var: %s", str(ex))
347 return TestCase.EX_PUBLISH_ARTIFACTS_ERROR
348 except botocore.exceptions.NoCredentialsError:
350 "Please fill ~/.aws/credentials, ~/.boto or set "
351 "AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in env")
352 return TestCase.EX_PUBLISH_ARTIFACTS_ERROR
353 except Exception: # pylint: disable=broad-except
354 self.__logger.exception("Cannot publish the artifacts")
355 return TestCase.EX_PUBLISH_ARTIFACTS_ERROR
358 """Clean the resources.
360 It can be overriden if resources must be deleted after
361 running the test case.