add unittests for testcase and refactor testcase related implementation
[releng.git] / utils / test / result_collection_api / resources / handlers.py
1 ##############################################################################
2 # Copyright (c) 2015 Orange
3 # guyrodrigue.koffi@orange.com / koffirodrigue@gmail.com
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 # feng.xiaowei@zte.com.cn refactor db.pod to db.pods         5-19-2016
9 # feng.xiaowei@zte.com.cn refactor test_project to project   5-19-2016
10 # feng.xiaowei@zte.com.cn refactor response body             5-19-2016
11 # feng.xiaowei@zte.com.cn refactor pod/project response info 5-19-2016
12 # feng.xiaowei@zte.com.cn refactor case related handler      5-20-2016
13 ##############################################################################
14
15 import json
16
17 from tornado.web import RequestHandler, asynchronous, HTTPError
18 from tornado import gen
19 from datetime import datetime, timedelta
20
21 from models import TestResult, CreateResponse
22 from resources.testcase_models import Testcase
23 from resources.project_models import Project
24 from resources.pod_models import Pod
25 from common.constants import DEFAULT_REPRESENTATION, HTTP_BAD_REQUEST, \
26     HTTP_NOT_FOUND, HTTP_FORBIDDEN
27 from common.config import prepare_put_request
28
29 from dashboard.dashboard_utils import get_dashboard_cases, \
30     check_dashboard_ready_project, check_dashboard_ready_case, \
31     get_dashboard_result
32
33
34 def format_data(data, cls):
35     cls_data = cls.from_dict(data)
36     return cls_data.format_http()
37
38
39 class GenericApiHandler(RequestHandler):
40     """
41     The purpose of this class is to take benefit of inheritance and prepare
42     a set of common functions for
43     the handlers
44     """
45
46     def initialize(self):
47         """ Prepares the database for the entire class """
48         self.db = self.settings["db"]
49
50     def prepare(self):
51         if not (self.request.method == "GET" or self.request.method == "DELETE"):
52             if self.request.headers.get("Content-Type") is not None:
53                 if self.request.headers["Content-Type"].startswith(
54                         DEFAULT_REPRESENTATION):
55                     try:
56                         self.json_args = json.loads(self.request.body)
57                     except (ValueError, KeyError, TypeError) as error:
58                         raise HTTPError(HTTP_BAD_REQUEST,
59                                         "Bad Json format [{}]".
60                                         format(error))
61                 else:
62                     self.json_args = None
63
64     def finish_request(self, json_object=None):
65         if json_object:
66             self.write(json.dumps(json_object))
67         self.set_header("Content-Type", DEFAULT_REPRESENTATION)
68         self.finish()
69
70
71 class VersionHandler(RequestHandler):
72     """ Display a message for the API version """
73     def get(self):
74         self.write("Collection of test result API, v1")
75
76
77 class PodHandler(GenericApiHandler):
78     """ Handle the requests about the POD Platforms
79     HTTP Methdods :
80         - GET : Get PODS
81         - POST : Create a pod
82         - DELETE : DELETE POD
83     """
84
85     def initialize(self):
86         """ Prepares the database for the entire class """
87         super(PodHandler, self).initialize()
88
89     @asynchronous
90     @gen.coroutine
91     def get(self, pod_name=None):
92         """
93         Get all pods or a single pod
94         :param pod_id:
95         """
96         query = dict()
97
98         if pod_name is not None:
99             query["name"] = pod_name
100             answer = yield self.db.pods.find_one(query)
101             if answer is None:
102                 raise HTTPError(HTTP_NOT_FOUND,
103                                 "{} Not Exist".format(pod_name))
104             else:
105                 answer = format_data(answer, Pod)
106         else:
107             res = []
108             cursor = self.db.pods.find(query)
109             while (yield cursor.fetch_next):
110                 res.append(format_data(cursor.next_object(), Pod))
111             answer = {'pods': res}
112
113         self.finish_request(answer)
114
115     @asynchronous
116     @gen.coroutine
117     def post(self):
118         """ Create a POD"""
119
120         if self.json_args is None:
121             raise HTTPError(HTTP_BAD_REQUEST)
122
123         query = {"name": self.json_args.get("name")}
124
125         # check for existing name in db
126         the_pod = yield self.db.pods.find_one(query)
127         if the_pod is not None:
128             raise HTTPError(HTTP_FORBIDDEN,
129                             "{} already exists as a pod".format(
130                                 self.json_args.get("name")))
131
132         pod = Pod.from_dict(self.json_args)
133         pod.creation_date = datetime.now()
134
135         yield self.db.pods.insert(pod.format())
136
137         res = CreateResponse(self.request.full_url() + '/{}'.format(pod.name))
138         self.finish_request(res.format())
139
140     @asynchronous
141     @gen.coroutine
142     def delete(self, pod_name):
143         """ Remove a POD
144
145         # check for an existing pod to be deleted
146         mongo_dict = yield self.db.pods.find_one(
147             {'name': pod_name})
148         pod = TestProject.pod(mongo_dict)
149         if pod is None:
150             raise HTTPError(HTTP_NOT_FOUND,
151                             "{} could not be found as a pod to be deleted"
152                             .format(pod_name))
153
154         # just delete it, or maybe save it elsewhere in a future
155         res = yield self.db.projects.remove(
156             {'name': pod_name})
157
158         self.finish_request(answer)
159         """
160         pass
161
162
163 class ProjectHandler(GenericApiHandler):
164     """
165     TestProjectHandler Class
166     Handle the requests about the Test projects
167     HTTP Methdods :
168         - GET : Get all test projects and details about a specific one
169         - POST : Add a test project
170         - PUT : Edit test projects information (name and/or description)
171         - DELETE : Remove a test project
172     """
173
174     def initialize(self):
175         """ Prepares the database for the entire class """
176         super(ProjectHandler, self).initialize()
177
178     @asynchronous
179     @gen.coroutine
180     def get(self, project_name=None):
181         """
182         Get Project(s) info
183         :param project_name:
184         """
185
186         query = dict()
187
188         if project_name is not None:
189             query["name"] = project_name
190             answer = yield self.db.projects.find_one(query)
191             if answer is None:
192                 raise HTTPError(HTTP_NOT_FOUND,
193                                 "{} Not Exist".format(project_name))
194             else:
195                 answer = format_data(answer, Project)
196         else:
197             res = []
198             cursor = self.db.projects.find(query)
199             while (yield cursor.fetch_next):
200                 res.append(format_data(cursor.next_object(), Project))
201             answer = {'projects': res}
202
203         self.finish_request(answer)
204
205     @asynchronous
206     @gen.coroutine
207     def post(self):
208         """ Create a test project"""
209
210         if self.json_args is None:
211             raise HTTPError(HTTP_BAD_REQUEST)
212
213         query = {"name": self.json_args.get("name")}
214
215         # check for name in db
216         the_project = yield self.db.projects.find_one(query)
217         if the_project is not None:
218             raise HTTPError(HTTP_FORBIDDEN,
219                             "{} already exists as a project".format(
220                                 self.json_args.get("name")))
221
222         project = Project.from_dict(self.json_args)
223         project.creation_date = datetime.now()
224
225         yield self.db.projects.insert(project.format())
226
227         res = CreateResponse(self.request.full_url() + '/{}'.format(project.name))
228         self.finish_request(res.format())
229
230
231     @asynchronous
232     @gen.coroutine
233     def put(self, project_name):
234         """ Updates the name and description of a test project"""
235
236         if self.json_args is None:
237             raise HTTPError(HTTP_BAD_REQUEST)
238
239         query = {'name': project_name}
240         from_project = yield self.db.projects.find_one(query)
241         if from_project is None:
242             raise HTTPError(HTTP_NOT_FOUND,
243                             "{} could not be found".format(project_name))
244
245         project = Project.from_dict(from_project)
246         new_name = self.json_args.get("name")
247         new_description = self.json_args.get("description")
248
249         # check for payload name parameter in db
250         # avoid a request if the project name has not changed in the payload
251         if new_name != project.name:
252             to_project = yield self.db.projects.find_one(
253                 {"name": new_name})
254             if to_project is not None:
255                 raise HTTPError(HTTP_FORBIDDEN,
256                                 "{} already exists as a project"
257                                 .format(new_name))
258
259         # new dict for changes
260         request = dict()
261         request = prepare_put_request(request,
262                                       "name",
263                                       new_name,
264                                       project.name)
265         request = prepare_put_request(request,
266                                       "description",
267                                       new_description,
268                                       project.description)
269
270         """ raise exception if there isn't a change """
271         if not request:
272             raise HTTPError(HTTP_FORBIDDEN, "Nothing to update")
273
274         """ we merge the whole document """
275         edit_request = project.format()
276         edit_request.update(request)
277
278         """ Updating the DB """
279         yield self.db.projects.update({'name': project_name}, edit_request)
280         new_project = yield self.db.projects.find_one({"_id": project._id})
281
282         self.finish_request(format_data(new_project, Project))
283
284     @asynchronous
285     @gen.coroutine
286     def delete(self, project_name):
287         """ Remove a test project"""
288         query = {'name': project_name}
289
290         # check for an existing project to be deleted
291         project = yield self.db.projects.find_one(query)
292         if project is None:
293             raise HTTPError(HTTP_NOT_FOUND,
294                             "{} could not be found as a project to be deleted"
295                             .format(project_name))
296
297         # just delete it, or maybe save it elsewhere in a future
298         yield self.db.projects.remove(query)
299
300         self.finish_request()
301
302
303 class TestcaseHandler(GenericApiHandler):
304     """
305     TestCasesHandler Class
306     Handle the requests about the Test cases for test projects
307     HTTP Methdods :
308         - GET : Get all test cases and details about a specific one
309         - POST : Add a test project
310         - PUT : Edit test projects information (name and/or description)
311     """
312
313     def initialize(self):
314         """ Prepares the database for the entire class """
315         super(TestcaseHandler, self).initialize()
316
317     @asynchronous
318     @gen.coroutine
319     def get(self, project_name, case_name=None):
320         """
321         Get testcases(s) info
322         :param project_name:
323         :param case_name:
324         """
325
326         query = {'project_name': project_name}
327
328         if case_name is not None:
329             query["name"] = case_name
330             answer = yield self.db.testcases.find_one(query)
331             if answer is None:
332                 raise HTTPError(HTTP_NOT_FOUND,
333                                 "{} Not Exist".format(case_name))
334             else:
335                 answer = format_data(answer, Testcase)
336         else:
337             res = []
338             cursor = self.db.testcases.find(query)
339             while (yield cursor.fetch_next):
340                 res.append(format_data(cursor.next_object(), Testcase))
341             answer = {'testcases': res}
342
343         self.finish_request(answer)
344
345     @asynchronous
346     @gen.coroutine
347     def post(self, project_name):
348         """ Create a test case"""
349
350         if self.json_args is None:
351             raise HTTPError(HTTP_BAD_REQUEST,
352                             "Check your request payload")
353
354         # retrieve test project
355         project = yield self.db.projects.find_one(
356             {"name": project_name})
357         if project is None:
358             raise HTTPError(HTTP_FORBIDDEN,
359                             "Could not find project {}"
360                             .format(project_name))
361
362         case_name = self.json_args.get('name')
363         the_testcase = yield self.db.testcases.find_one(
364             {"project_name": project_name, 'name': case_name})
365         if the_testcase:
366             raise HTTPError(HTTP_FORBIDDEN,
367                             "{} already exists as a case in project {}"
368                             .format(case_name, project_name))
369
370         testcase = Testcase.from_dict(self.json_args)
371         testcase.project_name = project_name
372         testcase.creation_date = datetime.now()
373
374         yield self.db.testcases.insert(testcase.format())
375         res = CreateResponse(self.request.full_url() + '/{}'.format(testcase.name))
376         self.finish_request(res.format())
377
378     @asynchronous
379     @gen.coroutine
380     def put(self, project_name, case_name):
381         """
382         Updates the name and description of a test case
383         :raises HTTPError (HTTP_NOT_FOUND, HTTP_FORBIDDEN)
384         """
385
386         query = {'project_name': project_name, 'name': case_name}
387
388         if self.json_args is None:
389             raise HTTPError(HTTP_BAD_REQUEST, "No payload")
390
391         # check if there is a case for the project in url parameters
392         from_testcase = yield self.db.testcases.find_one(query)
393         if from_testcase is None:
394             raise HTTPError(HTTP_NOT_FOUND,
395                             "{} could not be found as a {} case to be updated"
396                             .format(case_name, project_name))
397
398         testcase = Testcase.from_dict(from_testcase)
399         new_name = self.json_args.get("name")
400         new_project_name = self.json_args.get("project_name")
401         if not new_project_name:
402             new_project_name = project_name
403         new_description = self.json_args.get("description")
404
405         # check if there is not an existing test case
406         # with the name provided in the json payload
407         if new_name != case_name or new_project_name != project_name:
408             to_testcase = yield self.db.testcases.find_one(
409                 {'project_name': new_project_name, 'name': new_name})
410             if to_testcase is not None:
411                 raise HTTPError(HTTP_FORBIDDEN,
412                                 "{} already exists as a case in project"
413                                 .format(new_name, new_project_name))
414
415         # new dict for changes
416         request = dict()
417         request = prepare_put_request(request,
418                                       "name",
419                                       new_name,
420                                       testcase.name)
421         request = prepare_put_request(request,
422                                       "project_name",
423                                       new_project_name,
424                                       testcase.project_name)
425         request = prepare_put_request(request,
426                                       "description",
427                                       new_description,
428                                       testcase.description)
429
430         # we raise an exception if there isn't a change
431         if not request:
432             raise HTTPError(HTTP_FORBIDDEN,
433                             "Nothing to update")
434
435         # we merge the whole document """
436         edit_request = testcase.format()
437         edit_request.update(request)
438
439         """ Updating the DB """
440         yield self.db.testcases.update(query, edit_request)
441         new_case = yield self.db.testcases.find_one({"_id": testcase._id})
442         self.finish_request(format_data(new_case, Testcase))
443
444     @asynchronous
445     @gen.coroutine
446     def delete(self, project_name, case_name):
447         """ Remove a test case"""
448
449         query = {'project_name': project_name, 'name': case_name}
450
451         # check for an existing case to be deleted
452         testcase = yield self.db.testcases.find_one(query)
453         if testcase is None:
454             raise HTTPError(HTTP_NOT_FOUND,
455                             "{}/{} could not be found as a case to be deleted"
456                             .format(project_name, case_name))
457
458         # just delete it, or maybe save it elsewhere in a future
459         yield self.db.testcases.remove(query)
460         self.finish_request()
461
462
463 class TestResultsHandler(GenericApiHandler):
464     """
465     TestResultsHandler Class
466     Handle the requests about the Test project's results
467     HTTP Methdods :
468         - GET : Get all test results and details about a specific one
469         - POST : Add a test results
470         - DELETE : Remove a test result
471     """
472
473     def initialize(self):
474         """ Prepares the database for the entire class """
475         super(TestResultsHandler, self).initialize()
476         self.name = "test_result"
477
478     @asynchronous
479     @gen.coroutine
480     def get(self, result_id=None):
481         """
482         Retrieve result(s) for a test project on a specific POD.
483         Available filters for this request are :
484          - project : project name
485          - case : case name
486          - pod : pod name
487          - version : platform version (Arno-R1, ...)
488          - installer (fuel, ...)
489          - build_tag : Jenkins build tag name
490          - period : x (x last days)
491          - scenario : the test scenario (previously version)
492          - criteria : the global criteria status passed or failed
493          - trust_indicator : evaluate the stability of the test case to avoid
494          running systematically long and stable test case
495
496
497         :param result_id: Get a result by ID
498         :raise HTTPError
499
500         GET /results/project=functest&case=vPing&version=Arno-R1 \
501         &pod=pod_name&period=15
502         => get results with optional filters
503         """
504
505         project_arg = self.get_query_argument("project", None)
506         case_arg = self.get_query_argument("case", None)
507         pod_arg = self.get_query_argument("pod", None)
508         version_arg = self.get_query_argument("version", None)
509         installer_arg = self.get_query_argument("installer", None)
510         build_tag_arg = self.get_query_argument("build_tag", None)
511         scenario_arg = self.get_query_argument("scenario", None)
512         criteria_arg = self.get_query_argument("criteria", None)
513         period_arg = self.get_query_argument("period", None)
514         trust_indicator_arg = self.get_query_argument("trust_indicator", None)
515
516         # prepare request
517         get_request = dict()
518         if result_id is None:
519             if project_arg is not None:
520                 get_request["project_name"] = project_arg
521
522             if case_arg is not None:
523                 get_request["case_name"] = case_arg
524
525             if pod_arg is not None:
526                 get_request["pod_name"] = pod_arg
527
528             if version_arg is not None:
529                 get_request["version"] = version_arg
530
531             if installer_arg is not None:
532                 get_request["installer"] = installer_arg
533
534             if build_tag_arg is not None:
535                 get_request["build_tag"] = build_tag_arg
536
537             if scenario_arg is not None:
538                 get_request["scenario"] = scenario_arg
539
540             if criteria_arg is not None:
541                 get_request["criteria_tag"] = criteria_arg
542
543             if trust_indicator_arg is not None:
544                 get_request["trust_indicator_arg"] = trust_indicator_arg
545
546             if period_arg is not None:
547                 try:
548                     period_arg = int(period_arg)
549                 except:
550                     raise HTTPError(HTTP_BAD_REQUEST)
551
552                 if period_arg > 0:
553                     period = datetime.now() - timedelta(days=period_arg)
554                     obj = {"$gte": str(period)}
555                     get_request["creation_date"] = obj
556         else:
557             get_request["_id"] = result_id
558
559         print get_request
560         res = []
561         # fetching results
562         cursor = self.db.test_results.find(get_request)
563         while (yield cursor.fetch_next):
564             test_result = TestResult.test_result_from_dict(
565                 cursor.next_object())
566             res.append(test_result.format_http())
567
568         # building meta object
569         meta = dict()
570         meta["total"] = len(res)
571
572         # final response object
573         answer = dict()
574         answer["test_results"] = res
575         answer["meta"] = meta
576         self.finish_request(answer)
577
578     @asynchronous
579     @gen.coroutine
580     def post(self):
581         """
582         Create a new test result
583         :return: status of the request
584         :raise HTTPError
585         """
586
587         # check for request payload
588         if self.json_args is None:
589             raise HTTPError(HTTP_BAD_REQUEST)
590
591         # check for missing parameters in the request payload
592         if self.json_args.get("project_name") is None:
593             raise HTTPError(HTTP_BAD_REQUEST)
594         if self.json_args.get("case_name") is None:
595             raise HTTPError(HTTP_BAD_REQUEST)
596         # check for pod_name instead of id,
597         # keeping id for current implementations
598         if self.json_args.get("pod_name") is None:
599             raise HTTPError(HTTP_BAD_REQUEST)
600
601         # TODO : replace checks with jsonschema
602         # check for project
603         mongo_dict = yield self.db.projects.find_one(
604             {"name": self.json_args.get("project_name")})
605         if mongo_dict is None:
606             raise HTTPError(HTTP_NOT_FOUND,
607                             "Could not find project [{}] "
608                             .format(self.json_args.get("project_name")))
609
610         # check for case
611         mongo_dict = yield self.db.testcases.find_one(
612             {"name": self.json_args.get("case_name")})
613         if mongo_dict is None:
614             raise HTTPError(HTTP_NOT_FOUND,
615                             "Could not find case [{}] "
616                             .format(self.json_args.get("case_name")))
617
618         # check for pod
619         mongo_dict = yield self.db.pods.find_one(
620             {"name": self.json_args.get("pod_name")})
621         if mongo_dict is None:
622             raise HTTPError(HTTP_NOT_FOUND,
623                             "Could not find POD [{}] "
624                             .format(self.json_args.get("pod_name")))
625
626         # convert payload to object
627         test_result = TestResult.test_result_from_dict(self.json_args)
628         test_result.creation_date = datetime.now()
629
630         future = self.db.test_results.insert(test_result.format(),
631                                              check_keys=False)
632         result = yield future
633         test_result._id = result
634
635         self.finish_request(test_result.format_http())
636
637
638 class DashboardHandler(GenericApiHandler):
639     """
640     DashboardHandler Class
641     Handle the requests about the Test project's results
642     in a dahboard ready format
643     HTTP Methdods :
644         - GET : Get all test results and details about a specific one
645     """
646     def initialize(self):
647         """ Prepares the database for the entire class """
648         super(DashboardHandler, self).initialize()
649         self.name = "dashboard"
650
651     @asynchronous
652     @gen.coroutine
653     def get(self, result_id=None):
654         """
655         Retrieve dashboard ready result(s) for a test project
656         Available filters for this request are :
657          - project : project name
658          - case : case name
659          - pod : pod name
660          - version : platform version (Arno-R1, ...)
661          - installer (fuel, ...)
662          - period : x (x last days)
663
664
665         :param result_id: Get a result by ID
666         :raise HTTPError
667
668         GET /dashboard?project=functest&case=vPing&version=Arno-R1 \
669         &pod=pod_name&period=15
670         => get results with optional filters
671         """
672
673         project_arg = self.get_query_argument("project", None)
674         case_arg = self.get_query_argument("case", None)
675         pod_arg = self.get_query_argument("pod", None)
676         version_arg = self.get_query_argument("version", None)
677         installer_arg = self.get_query_argument("installer", None)
678         period_arg = self.get_query_argument("period", None)
679
680         # prepare request
681         get_request = dict()
682
683         # /dashboard?project=<>&pod=<>...
684         if (result_id is None):
685             if project_arg is not None:
686                 get_request["project_name"] = project_arg
687
688             if case_arg is not None:
689                 get_request["case_name"] = case_arg
690
691             if pod_arg is not None:
692                 get_request["pod_name"] = pod_arg
693
694             if version_arg is not None:
695                 get_request["version"] = version_arg
696
697             if installer_arg is not None:
698                 get_request["installer"] = installer_arg
699
700             if period_arg is not None:
701                 try:
702                     period_arg = int(period_arg)
703                 except:
704                     raise HTTPError(HTTP_BAD_REQUEST)
705                 if period_arg > 0:
706                     period = datetime.now() - timedelta(days=period_arg)
707                     obj = {"$gte": str(period)}
708                     get_request["creation_date"] = obj
709         else:
710             get_request["_id"] = result_id
711
712         dashboard = []
713
714         # on /dashboard retrieve the list of projects and testcases
715         # ready for dashboard
716         if project_arg is None:
717             raise HTTPError(HTTP_NOT_FOUND,
718                             "error:Project name missing")
719         elif check_dashboard_ready_project(project_arg, "./dashboard"):
720             res = []
721
722             if case_arg is None:
723                 raise HTTPError(
724                     HTTP_NOT_FOUND,
725                     "error:Test case missing for project " + project_arg)
726
727             # special case of status for project
728             if case_arg == "status":
729                 del get_request["case_name"]
730                 # retention time to be agreed
731                 # last five days by default?
732                 # TODO move to DB
733                 period = datetime.now() - timedelta(days=5)
734                 get_request["creation_date"] = {"$gte": period}
735
736             # fetching results
737             cursor = self.db.test_results.find(get_request)
738             while (yield cursor.fetch_next):
739                 test_result = TestResult.test_result_from_dict(
740                     cursor.next_object())
741                 res.append(test_result.format_http())
742
743             if check_dashboard_ready_case(project_arg, case_arg):
744                 dashboard = get_dashboard_result(project_arg, case_arg, res)
745             else:
746                 raise HTTPError(
747                     HTTP_NOT_FOUND,
748                     "error:" + case_arg +
749                     " test case not case dashboard ready on project " +
750                     project_arg)
751
752         else:
753             dashboard.append(
754                 {"error": "Project not recognized or not dashboard ready"})
755             dashboard.append(
756                 {"Dashboard-ready-projects":
757                     get_dashboard_cases("./dashboard")})
758             raise HTTPError(
759                 HTTP_NOT_FOUND,
760                 "error: no dashboard ready data for this project")
761
762         # fetching results
763         # cursor = self.db.test_results.find(get_request)
764         # while (yield cursor.fetch_next):
765         #    test_result = TestResult.test_result_from_dict(
766         #        cursor.next_object())
767         #    res.append(test_result.format_http())
768
769         # building meta object
770         meta = dict()
771
772         # final response object
773         answer = dict()
774         answer["dashboard"] = dashboard
775         answer["meta"] = meta
776         self.finish_request(answer)