Add ContentType when publishing artifacts
[functest-xtesting.git] / xtesting / core / testcase.py
1 #!/usr/bin/env python
2
3 # Copyright (c) 2016 Orange and others.
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 """Define the parent class of all Xtesting TestCases."""
11
12 import abc
13 from datetime import datetime
14 import json
15 import logging
16 import mimetypes
17 import os
18 import re
19 import sys
20
21 import boto3
22 import botocore
23 import prettytable
24 import requests
25 import six
26 from six.moves import urllib
27
28 from xtesting.utils import decorators
29 from xtesting.utils import env
30
31 __author__ = "Cedric Ollivier <cedric.ollivier@orange.com>"
32
33
34 @six.add_metaclass(abc.ABCMeta)
35 class TestCase(object):
36     # pylint: disable=too-many-instance-attributes
37     """Base model for single test case."""
38
39     EX_OK = os.EX_OK
40     """everything is OK"""
41
42     EX_RUN_ERROR = os.EX_SOFTWARE
43     """run() failed"""
44
45     EX_PUSH_TO_DB_ERROR = os.EX_SOFTWARE - 1
46     """push_to_db() failed"""
47
48     EX_TESTCASE_FAILED = os.EX_SOFTWARE - 2
49     """results are false"""
50
51     EX_TESTCASE_SKIPPED = os.EX_SOFTWARE - 3
52     """requirements are unmet"""
53
54     EX_PUBLISH_ARTIFACTS_ERROR = os.EX_SOFTWARE - 4
55     """publish_artifacts() failed"""
56
57     dir_results = "/var/lib/xtesting/results"
58     _job_name_rule = "(dai|week)ly-(.+?)-[0-9]*"
59     _headers = {'Content-Type': 'application/json'}
60     __logger = logging.getLogger(__name__)
61
62     def __init__(self, **kwargs):
63         self.details = {}
64         self.project_name = kwargs.get('project_name', 'xtesting')
65         self.case_name = kwargs.get('case_name', '')
66         self.criteria = kwargs.get('criteria', 100)
67         self.result = 0
68         self.start_time = 0
69         self.stop_time = 0
70         self.is_skipped = False
71         self.output_log_name = 'xtesting.log'
72         self.output_debug_log_name = 'xtesting.debug.log'
73         self.res_dir = "{}/{}".format(self.dir_results, self.case_name)
74
75     def __str__(self):
76         try:
77             assert self.project_name
78             assert self.case_name
79             if self.is_skipped:
80                 result = 'SKIP'
81             else:
82                 result = 'PASS' if(self.is_successful(
83                     ) == TestCase.EX_OK) else 'FAIL'
84             msg = prettytable.PrettyTable(
85                 header_style='upper', padding_width=5,
86                 field_names=['test case', 'project', 'duration',
87                              'result'])
88             msg.add_row([self.case_name, self.project_name,
89                          self.get_duration(), result])
90             return msg.get_string()
91         except AssertionError:
92             self.__logger.error("We cannot print invalid objects")
93             return super(TestCase, self).__str__()
94
95     def get_duration(self):
96         """Return the duration of the test case.
97
98         Returns:
99             duration if start_time and stop_time are set
100             "XX:XX" otherwise.
101         """
102         try:
103             if self.is_skipped:
104                 return "00:00"
105             assert self.start_time
106             assert self.stop_time
107             if self.stop_time < self.start_time:
108                 return "XX:XX"
109             return "{0[0]:02.0f}:{0[1]:02.0f}".format(divmod(
110                 self.stop_time - self.start_time, 60))
111         except Exception:  # pylint: disable=broad-except
112             self.__logger.error("Please run test before getting the duration")
113             return "XX:XX"
114
115     def is_successful(self):
116         """Interpret the result of the test case.
117
118         It allows getting the result of TestCase. It completes run()
119         which only returns the execution status.
120
121         It can be overriden if checking result is not suitable.
122
123         Returns:
124             TestCase.EX_OK if result is 'PASS'.
125             TestCase.EX_TESTCASE_SKIPPED if test case is skipped.
126             TestCase.EX_TESTCASE_FAILED otherwise.
127         """
128         try:
129             if self.is_skipped:
130                 return TestCase.EX_TESTCASE_SKIPPED
131             assert self.criteria
132             assert self.result is not None
133             if (not isinstance(self.result, str) and
134                     not isinstance(self.criteria, str)):
135                 if self.result >= self.criteria:
136                     return TestCase.EX_OK
137             else:
138                 # Backward compatibility
139                 # It must be removed as soon as TestCase subclasses
140                 # stop setting result = 'PASS' or 'FAIL'.
141                 # In this case criteria is unread.
142                 self.__logger.warning(
143                     "Please update result which must be an int!")
144                 if self.result == 'PASS':
145                     return TestCase.EX_OK
146         except AssertionError:
147             self.__logger.error("Please run test before checking the results")
148         return TestCase.EX_TESTCASE_FAILED
149
150     def check_requirements(self):  # pylint: disable=no-self-use
151         """Check the requirements of the test case.
152
153         It can be overriden on purpose.
154         """
155         self.is_skipped = False
156
157     @abc.abstractmethod
158     def run(self, **kwargs):
159         """Run the test case.
160
161         It allows running TestCase and getting its execution
162         status.
163
164         The subclasses must override the default implementation which
165         is false on purpose.
166
167         The new implementation must set the following attributes to
168         push the results to DB:
169
170             * result,
171             * start_time,
172             * stop_time.
173
174         Args:
175             kwargs: Arbitrary keyword arguments.
176         """
177
178     @decorators.can_dump_request_to_file
179     def push_to_db(self):
180         """Push the results of the test case to the DB.
181
182         It allows publishing the results and checking the status.
183
184         It could be overriden if the common implementation is not
185         suitable.
186
187         The following attributes must be set before pushing the results to DB:
188
189             * project_name,
190             * case_name,
191             * result,
192             * start_time,
193             * stop_time.
194
195         The next vars must be set in env:
196
197             * TEST_DB_URL,
198             * INSTALLER_TYPE,
199             * DEPLOY_SCENARIO,
200             * NODE_NAME,
201             * BUILD_TAG.
202
203         Returns:
204             TestCase.EX_OK if results were pushed to DB.
205             TestCase.EX_PUSH_TO_DB_ERROR otherwise.
206         """
207         try:
208             if self.is_skipped:
209                 return TestCase.EX_PUSH_TO_DB_ERROR
210             assert self.project_name
211             assert self.case_name
212             assert self.start_time
213             assert self.stop_time
214             url = env.get('TEST_DB_URL')
215             data = {"project_name": self.project_name,
216                     "case_name": self.case_name,
217                     "details": self.details}
218             data["installer"] = env.get('INSTALLER_TYPE')
219             data["scenario"] = env.get('DEPLOY_SCENARIO')
220             data["pod_name"] = env.get('NODE_NAME')
221             data["build_tag"] = env.get('BUILD_TAG')
222             data["criteria"] = 'PASS' if self.is_successful(
223                 ) == TestCase.EX_OK else 'FAIL'
224             data["start_date"] = datetime.fromtimestamp(
225                 self.start_time).strftime('%Y-%m-%d %H:%M:%S')
226             data["stop_date"] = datetime.fromtimestamp(
227                 self.stop_time).strftime('%Y-%m-%d %H:%M:%S')
228             try:
229                 data["version"] = re.search(
230                     TestCase._job_name_rule,
231                     env.get('BUILD_TAG')).group(2)
232             except Exception:  # pylint: disable=broad-except
233                 data["version"] = "unknown"
234             req = requests.post(
235                 url, data=json.dumps(data, sort_keys=True),
236                 headers=self._headers)
237             req.raise_for_status()
238             if urllib.parse.urlparse(url).scheme != "file":
239                 res_url = req.json()["href"]
240                 if env.get('TEST_DB_EXT_URL'):
241                     res_url = res_url.replace(
242                         env.get('TEST_DB_URL'), env.get('TEST_DB_EXT_URL'))
243                 self.__logger.info(
244                     "The results were successfully pushed to DB: \n\n%s\n",
245                     res_url)
246         except AssertionError:
247             self.__logger.exception(
248                 "Please run test before publishing the results")
249             return TestCase.EX_PUSH_TO_DB_ERROR
250         except requests.exceptions.HTTPError:
251             self.__logger.exception("The HTTP request raises issues")
252             return TestCase.EX_PUSH_TO_DB_ERROR
253         except Exception:  # pylint: disable=broad-except
254             self.__logger.exception("The results cannot be pushed to DB")
255             return TestCase.EX_PUSH_TO_DB_ERROR
256         return TestCase.EX_OK
257
258     def publish_artifacts(self):  # pylint: disable=too-many-locals
259         """Push the artifacts to the S3 repository.
260
261         It allows publishing the artifacts.
262
263         It could be overriden if the common implementation is not
264         suitable.
265
266         The credentials must be configured before publishing the artifacts:
267
268             * fill ~/.aws/credentials or ~/.boto,
269             * set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in env.
270
271         The next vars must be set in env:
272
273             * S3_ENDPOINT_URL (http://127.0.0.1:9000),
274             * S3_DST_URL (s3://xtesting/prefix),
275             * HTTP_DST_URL (http://127.0.0.1/prefix).
276
277         Returns:
278             TestCase.EX_OK if artifacts were published to repository.
279             TestCase.EX_PUBLISH_ARTIFACTS_ERROR otherwise.
280         """
281         try:
282             b3resource = boto3.resource(
283                 's3', endpoint_url=os.environ["S3_ENDPOINT_URL"])
284             dst_s3_url = os.environ["S3_DST_URL"]
285             bucket_name = urllib.parse.urlparse(dst_s3_url).netloc
286             try:
287                 b3resource.meta.client.head_bucket(Bucket=bucket_name)
288             except botocore.exceptions.ClientError as exc:
289                 error_code = exc.response['Error']['Code']
290                 if error_code == '404':
291                     # pylint: disable=no-member
292                     b3resource.create_bucket(Bucket=bucket_name)
293                 else:
294                     typ, value, traceback = sys.exc_info()
295                     six.reraise(typ, value, traceback)
296             except Exception:  # pylint: disable=broad-except
297                 typ, value, traceback = sys.exc_info()
298                 six.reraise(typ, value, traceback)
299             path = urllib.parse.urlparse(dst_s3_url).path.strip("/")
300             dst_http_url = os.environ["HTTP_DST_URL"]
301             output_str = "\n"
302             self.details["links"] = []
303             for log_file in [self.output_log_name, self.output_debug_log_name]:
304                 if os.path.exists(os.path.join(self.dir_results, log_file)):
305                     abs_file = os.path.join(self.dir_results, log_file)
306                     mime_type = mimetypes.guess_type(abs_file)
307                     self.__logger.debug(
308                         "Publishing %s %s", abs_file, mime_type)
309                     # pylint: disable=no-member
310                     b3resource.Bucket(bucket_name).upload_file(
311                         abs_file,
312                         os.path.join(path, log_file),
313                         ExtraArgs={'ContentType': mime_type[
314                             0] or 'application/octet-stream'})
315                     link = os.path.join(dst_http_url, log_file)
316                     output_str += "\n{}".format(link)
317                     self.details["links"].append(link)
318             for root, _, files in os.walk(self.res_dir):
319                 for pub_file in files:
320                     abs_file = os.path.join(root, pub_file)
321                     mime_type = mimetypes.guess_type(abs_file)
322                     self.__logger.debug(
323                         "Publishing %s %s", abs_file, mime_type)
324                     # pylint: disable=no-member
325                     b3resource.Bucket(bucket_name).upload_file(
326                         abs_file,
327                         os.path.join(path, os.path.relpath(
328                             os.path.join(root, pub_file),
329                             start=self.dir_results)),
330                         ExtraArgs={'ContentType': mime_type[
331                             0] or 'application/octet-stream'})
332                     link = os.path.join(dst_http_url, os.path.relpath(
333                         os.path.join(root, pub_file),
334                         start=self.dir_results))
335                     output_str += "\n{}".format(link)
336                     self.details["links"].append(link)
337             self.__logger.info(
338                 "All artifacts were successfully published: %s\n", output_str)
339             return TestCase.EX_OK
340         except KeyError as ex:
341             self.__logger.error("Please check env var: %s", str(ex))
342             return TestCase.EX_PUBLISH_ARTIFACTS_ERROR
343         except botocore.exceptions.NoCredentialsError:
344             self.__logger.error(
345                 "Please fill ~/.aws/credentials, ~/.boto or set "
346                 "AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in env")
347             return TestCase.EX_PUBLISH_ARTIFACTS_ERROR
348         except Exception:  # pylint: disable=broad-except
349             self.__logger.exception("Cannot publish the artifacts")
350             return TestCase.EX_PUBLISH_ARTIFACTS_ERROR
351
352     def clean(self):
353         """Clean the resources.
354
355         It can be overriden if resources must be deleted after
356         running the test case.
357         """