functionaltestframework.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. #!/usr/bin/env python3
  2. # Contest Management System - http://cms-dev.github.io/
  3. # Copyright © 2012 Bernard Blackham <bernard@largestprime.net>
  4. # Copyright © 2013-2018 Stefano Maggiolo <s.maggiolo@gmail.com>
  5. # Copyright © 2013-2016 Luca Wehrstedt <luca.wehrstedt@gmail.com>
  6. # Copyright © 2014 Luca Versari <veluca93@gmail.com>
  7. # Copyright © 2014 William Di Luigi <williamdiluigi@gmail.com>
  8. # Copyright © 2016 Peyman Jabbarzade Ganje <peyman.jabarzade@gmail.com>
  9. # Copyright © 2017 Luca Chiodini <luca@chiodini.org>
  10. #
  11. # This program is free software: you can redistribute it and/or modify
  12. # it under the terms of the GNU Affero General Public License as
  13. # published by the Free Software Foundation, either version 3 of the
  14. # License, or (at your option) any later version.
  15. #
  16. # This program is distributed in the hope that it will be useful,
  17. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. # GNU Affero General Public License for more details.
  20. #
  21. # You should have received a copy of the GNU Affero General Public License
  22. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  23. import json
  24. import logging
  25. import re
  26. import sys
  27. import time
  28. from cmstestsuite import CONFIG, TestException, sh
  29. from cmstestsuite.web import Browser
  30. from cmstestsuite.web.AWSRequests import \
  31. AWSLoginRequest, AWSSubmissionViewRequest, AWSUserTestViewRequest
  32. from cmstestsuite.web.CWSRequests import \
  33. CWSLoginRequest, SubmitRequest, SubmitUserTestRequest
  34. logger = logging.getLogger(__name__)
  35. class FunctionalTestFramework:
  36. """An object encapsulating the status of a functional test
  37. It maintains facilities to interact with the services while running a
  38. functional tests, e.g. via virtual browsers, and also offers facilities
  39. to create and retrieve objects from the services.
  40. """
  41. # Base URLs for AWS and CWS
  42. AWS_BASE_URL = "http://localhost:8889"
  43. CWS_BASE_URL = "http://localhost:8888"
  44. # Regexes for submission statuses.
  45. WAITING_STATUSES = re.compile(
  46. r'Compiling\.\.\.|Evaluating\.\.\.|Scoring\.\.\.|Evaluated')
  47. COMPLETED_STATUSES = re.compile(
  48. r'Compilation failed|Evaluated \(|Scored \(')
  49. # Regexes for user test statuses
  50. WAITING_STATUSES_USER_TEST = re.compile(
  51. r'Compiling\.\.\.|Evaluating\.\.\.')
  52. COMPLETED_STATUSES_USER_TEST = re.compile(
  53. r'Compilation failed|Evaluated')
  54. # Singleton instance for this class.
  55. __instance = None
  56. def __new__(cls):
  57. if FunctionalTestFramework.__instance is None:
  58. FunctionalTestFramework.__instance = object.__new__(cls)
  59. return FunctionalTestFramework.__instance
  60. def __init__(self):
  61. # This holds the decoded-JSON of the cms.conf configuration file.
  62. # Lazily loaded, to be accessed through the getter method.
  63. self._cms_config = None
  64. # Persistent browsers to access AWS and CWS. Lazily loaded, to be
  65. # accessed through the getter methods.
  66. self._aws_browser = None
  67. self._cws_browser = None
  68. # List of users and tasks we created as part of the test.
  69. self.created_users = {}
  70. self.created_tasks = {}
  71. # Information on the administrator running the tests.
  72. self.admin_info = {}
  73. def get_aws_browser(self):
  74. if self._aws_browser is None:
  75. self._aws_browser = Browser()
  76. lr = AWSLoginRequest(self._aws_browser,
  77. self.admin_info["username"],
  78. self.admin_info["password"],
  79. base_url=self.AWS_BASE_URL)
  80. self._aws_browser.login(lr)
  81. return self._aws_browser
  82. def get_cws_browser(self, user_id):
  83. if self._cws_browser is None:
  84. self._cws_browser = Browser()
  85. username = self.created_users[user_id]['username']
  86. password = self.created_users[user_id]['password']
  87. lr = CWSLoginRequest(
  88. self._cws_browser, username, password,
  89. base_url=self.CWS_BASE_URL)
  90. self._cws_browser.login(lr)
  91. return self._cws_browser
  92. def initialize_aws(self):
  93. """Create an admin.
  94. The username will be admin_<suffix>, where <suffix> will be the first
  95. integer (from 1) for which an admin with that name doesn't yet exist.
  96. return (str): the suffix.
  97. """
  98. logger.info("Creating admin...")
  99. self.admin_info["password"] = "adminpwd"
  100. suffix = "1"
  101. while True:
  102. self.admin_info["username"] = "admin_%s" % suffix
  103. logger.info("Trying %(username)s" % self.admin_info)
  104. try:
  105. sh([sys.executable, "cmscontrib/AddAdmin.py",
  106. "%(username)s" % self.admin_info,
  107. "-p", "%(password)s" % self.admin_info],
  108. ignore_failure=False)
  109. except TestException:
  110. suffix = str(int(suffix) + 1)
  111. else:
  112. break
  113. return suffix
  114. def get_cms_config(self):
  115. if self._cms_config is None:
  116. with open("%(CONFIG_PATH)s" % CONFIG, "rt", encoding="utf-8") as f:
  117. self._cms_config = json.load(f)
  118. return self._cms_config
  119. def admin_req(self, path, args=None, files=None):
  120. browser = self.get_aws_browser()
  121. return browser.do_request(self.AWS_BASE_URL + '/' + path, args, files)
  122. def get_tasks(self):
  123. """Return the existing tasks
  124. return ({string: {id: string, title: string}}): the tasks, as a
  125. dictionary with the task name as key.
  126. """
  127. r = self.admin_req('tasks')
  128. groups = re.findall(r'''
  129. <tr>\s*
  130. <td><a\s+href="./task/(\d+)">(.*)</a></td>\s*
  131. <td>(.*)</td>\s*
  132. ''', r.text, re.X)
  133. tasks = {}
  134. for g in groups:
  135. id_, name, title = g
  136. id_ = int(id_)
  137. tasks[name] = {
  138. 'title': title,
  139. 'id': id_,
  140. }
  141. return tasks
  142. def get_users(self, contest_id):
  143. """Return the existing users
  144. return ({string: {id: string, firstname: string, lastname: string}):
  145. the users, as a dictionary with the username as key.
  146. """
  147. r = self.admin_req('contest/%s/users' % contest_id)
  148. groups = re.findall(r'''
  149. <tr> \s*
  150. <td> \s* (.*) \s* </td> \s*
  151. <td> \s* (.*) \s* </td> \s*
  152. <td><a\s+href="./user/(\d+)">(.*)</a></td>
  153. ''', r.text, re.X)
  154. users = {}
  155. for g in groups:
  156. firstname, lastname, id_, username = g
  157. id_ = int(id_)
  158. users[username] = {
  159. 'firstname': firstname,
  160. 'lastname': lastname,
  161. 'id': id_,
  162. }
  163. return users
  164. def add_contest(self, **kwargs):
  165. add_args = {
  166. "name": kwargs.get('name'),
  167. "description": kwargs.get('description'),
  168. }
  169. resp = self.admin_req('contests/add', args=add_args)
  170. # Contest ID is returned as HTTP response.
  171. page = resp.text
  172. match = re.search(
  173. r'<form enctype="multipart/form-data" '
  174. r'action="../contest/([0-9]+)" '
  175. r'method="POST" name="edit_contest" style="display:inline;">',
  176. page)
  177. if match is not None:
  178. contest_id = int(match.groups()[0])
  179. self.admin_req('contest/%s' % contest_id, args=kwargs)
  180. return contest_id
  181. else:
  182. raise TestException("Unable to create contest.")
  183. def add_task(self, **kwargs):
  184. add_args = {
  185. "name": kwargs.get('name'),
  186. "title": kwargs.get('title'),
  187. }
  188. r = self.admin_req('tasks/add', args=add_args)
  189. response = r.text
  190. match_task_id = re.search(r'/task/([0-9]+)$', r.url)
  191. match_dataset_id = re.search(r'/dataset/([0-9]+)', response)
  192. if match_task_id and match_dataset_id:
  193. task_id = int(match_task_id.group(1))
  194. dataset_id = int(match_dataset_id.group(1))
  195. edit_args = {}
  196. for k, v in kwargs.items():
  197. edit_args[k.replace("{{dataset_id}}", str(dataset_id))] = v
  198. r = self.admin_req('task/%s' % task_id, args=edit_args)
  199. self.created_tasks[task_id] = kwargs
  200. else:
  201. raise TestException("Unable to create task.")
  202. r = self.admin_req('contest/' + kwargs["contest_id"] + '/tasks/add',
  203. args={"task_id": str(task_id)})
  204. g = re.search('<input type="radio" name="task_id" value="' +
  205. str(task_id) + '"/>', r.text)
  206. if g:
  207. return task_id
  208. else:
  209. raise TestException("Unable to assign task to contest.")
  210. def add_manager(self, task_id, manager):
  211. args = {}
  212. files = [
  213. ('manager', manager),
  214. ]
  215. dataset_id = self.get_task_active_dataset_id(task_id)
  216. self.admin_req('dataset/%s/managers/add' % dataset_id,
  217. files=files, args=args)
  218. def get_task_active_dataset_id(self, task_id):
  219. resp = self.admin_req('task/%s' % task_id)
  220. page = resp.text
  221. match = re.search(
  222. r'id="title_dataset_([0-9]+).* \(Live\)</',
  223. page)
  224. if match is None:
  225. raise TestException("Unable to create contest.")
  226. dataset_id = int(match.groups()[0])
  227. return dataset_id
  228. def add_testcase(self, task_id, num, input_file, output_file, public):
  229. files = [
  230. ('input', input_file),
  231. ('output', output_file),
  232. ]
  233. args = {}
  234. args["codename"] = "%03d" % num
  235. if public:
  236. args['public'] = '1'
  237. dataset_id = self.get_task_active_dataset_id(task_id)
  238. self.admin_req('dataset/%s/testcases/add' % dataset_id,
  239. files=files, args=args)
  240. def add_user(self, **kwargs):
  241. r = self.admin_req('users/add', args=kwargs)
  242. g = re.search(r'/user/([0-9]+)$', r.url)
  243. if g:
  244. user_id = int(g.group(1))
  245. self.created_users[user_id] = kwargs
  246. else:
  247. raise TestException("Unable to create user.")
  248. kwargs["user_id"] = user_id
  249. r = self.admin_req('contest/%s/users/add' % kwargs["contest_id"],
  250. args=kwargs)
  251. g = re.search('<input type="radio" name="user_id" value="' +
  252. str(user_id) + '"/>', r.text)
  253. if g:
  254. return user_id
  255. else:
  256. raise TestException("Unable to create participation.")
  257. def add_existing_task(self, task_id, **kwargs):
  258. """Inform the framework of an existing task"""
  259. self.created_tasks[task_id] = kwargs
  260. def add_existing_user(self, user_id, **kwargs):
  261. """Inform the framework of an existing user"""
  262. self.created_users[user_id] = kwargs
  263. def cws_submit(self, task_id, user_id,
  264. submission_format, filenames, language):
  265. task = (task_id, self.created_tasks[task_id]['name'])
  266. browser = self.get_cws_browser(user_id)
  267. sr = SubmitRequest(browser, task, base_url=self.CWS_BASE_URL,
  268. submission_format=submission_format,
  269. filenames=filenames, language=language)
  270. sr.execute()
  271. submission_id = sr.get_submission_id()
  272. if submission_id is None:
  273. raise TestException("Failed to submit solution.")
  274. return submission_id
  275. def cws_submit_user_test(self, task_id, user_id,
  276. submission_format, filenames, language):
  277. task = (task_id, self.created_tasks[task_id]['name'])
  278. browser = self.get_cws_browser(user_id)
  279. sr = SubmitUserTestRequest(
  280. browser, task, base_url=self.CWS_BASE_URL,
  281. submission_format=submission_format,
  282. filenames=filenames, language=language)
  283. sr.execute()
  284. user_test_id = sr.get_user_test_id()
  285. if user_test_id is None:
  286. raise TestException("Failed to submit user test.")
  287. return user_test_id
  288. def get_evaluation_result(self, contest_id, submission_id, timeout=60):
  289. browser = self.get_aws_browser()
  290. sleep_interval = 0.1
  291. while timeout > 0:
  292. timeout -= sleep_interval
  293. sr = AWSSubmissionViewRequest(browser,
  294. submission_id,
  295. base_url=self.AWS_BASE_URL)
  296. sr.execute()
  297. result = sr.get_submission_info()
  298. status = result['status']
  299. if self.COMPLETED_STATUSES.search(status):
  300. return result
  301. if self.WAITING_STATUSES.search(status):
  302. time.sleep(sleep_interval)
  303. continue
  304. raise TestException("Unknown submission status: %s" % status)
  305. raise TestException("Waited too long for submission result.")
  306. def get_user_test_result(self, contest_id, user_test_id, timeout=60):
  307. browser = self.get_aws_browser()
  308. sleep_interval = 0.1
  309. while timeout > 0:
  310. timeout -= sleep_interval
  311. sr = AWSUserTestViewRequest(browser,
  312. user_test_id,
  313. base_url=self.AWS_BASE_URL)
  314. sr.execute()
  315. result = sr.get_user_test_info()
  316. status = result['status']
  317. if self.COMPLETED_STATUSES_USER_TEST.search(status):
  318. return result
  319. if self.WAITING_STATUSES_USER_TEST.search(status):
  320. time.sleep(sleep_interval)
  321. continue
  322. raise TestException("Unknown user test status: %s" % status)
  323. raise TestException("Waited too long for user test result.")