databasemixin.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. #!/usr/bin/env python3
  2. # Contest Management System - http://cms-dev.github.io/
  3. # Copyright © 2015-2018 Stefano Maggiolo <s.maggiolo@gmail.com>
  4. # Copyright © 2018 Luca Wehrstedt <luca.wehrstedt@gmail.com>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU Affero General Public License as
  8. # published by the Free Software Foundation, either version 3 of the
  9. # License, or (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU Affero General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Affero General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """A unittest.TestCase mixin for tests interacting with the database.
  19. This mixin will connect to a different DB, recreating it for each
  20. testing class; it will also create a session at each test setup.
  21. The mixin also offers a series of get_<object> (to build an object, not
  22. attached to any session) and add_<object> (to build and add the object
  23. to the default session) methods. Without arguments, these will create
  24. minimal objects with random values in the fields, and callers can
  25. specify as many fields as they like.
  26. When the object depends on a "parent" object, the caller can specify
  27. it, or leave it for the function to create. When there is a common
  28. ancestor through multiple paths, the functions check that it is the
  29. same regardless of the path used to reach it.
  30. """
  31. from datetime import timedelta
  32. import cms
  33. # Monkeypatch the db string.
  34. # Noqa to avoid complaints due to imports after a statement.
  35. cms.config.database += "fortesting" # noqa
  36. from cms.db import engine, metadata, Announcement, Contest, Dataset, Evaluation, \
  37. Executable, File, Manager, Message, Participation, Question, Session, \
  38. Statement, Submission, SubmissionResult, Task, Team, Testcase, User, \
  39. UserTest, UserTestResult, drop_db, init_db, Token, UserTestFile, \
  40. UserTestManager
  41. from cms.db.filecacher import DBBackend
  42. from cmstestsuite.unit_tests.testidgenerator import unique_long_id, \
  43. unique_unicode_id, unique_digest
  44. class DatabaseObjectGeneratorMixin:
  45. """Mixin to create database objects without a session.
  46. This is to be preferred to DatabaseMixin when a session is not required, in
  47. order to save some initialization and cleanup time.
  48. The methods in this mixin are static (actually, class methods to allow
  49. overriding); we use a mixin to keep them together and avoid the need to
  50. import a lot of names.
  51. """
  52. @classmethod
  53. def get_contest(cls, **kwargs):
  54. """Create a contest"""
  55. args = {
  56. "name": unique_unicode_id(),
  57. "description": unique_unicode_id(),
  58. }
  59. args.update(kwargs)
  60. contest = Contest(**args)
  61. return contest
  62. @classmethod
  63. def get_announcement(cls, contest=None, **kwargs):
  64. """Create an announcement"""
  65. contest = contest if contest is not None else cls.get_contest()
  66. args = {
  67. "contest": contest,
  68. "subject": unique_unicode_id(),
  69. "text": unique_unicode_id(),
  70. "timestamp": (contest.start + timedelta(0, unique_long_id())),
  71. }
  72. args.update(kwargs)
  73. announcement = Announcement(**args)
  74. return announcement
  75. @classmethod
  76. def get_user(cls, **kwargs):
  77. """Create a user"""
  78. args = {
  79. "username": unique_unicode_id(),
  80. "password": "",
  81. "first_name": unique_unicode_id(),
  82. "last_name": unique_unicode_id(),
  83. }
  84. args.update(kwargs)
  85. user = User(**args)
  86. return user
  87. @classmethod
  88. def get_participation(cls, user=None, contest=None, **kwargs):
  89. """Create a participation"""
  90. user = user if user is not None else cls.get_user()
  91. contest = contest if contest is not None else cls.get_contest()
  92. args = {
  93. "user": user,
  94. "contest": contest,
  95. }
  96. args.update(kwargs)
  97. participation = Participation(**args)
  98. return participation
  99. @classmethod
  100. def get_message(cls, participation=None, **kwargs):
  101. """Create a message."""
  102. participation = participation if participation is not None \
  103. else cls.get_participation()
  104. args = {
  105. "participation": participation,
  106. "subject": unique_unicode_id(),
  107. "text": unique_unicode_id(),
  108. "timestamp": (participation.contest.start
  109. + timedelta(0, unique_long_id())),
  110. }
  111. args.update(kwargs)
  112. message = Message(**args)
  113. return message
  114. @classmethod
  115. def get_question(cls, participation=None, **kwargs):
  116. """Create a question."""
  117. participation = participation if participation is not None \
  118. else cls.get_participation()
  119. args = {
  120. "participation": participation,
  121. "subject": unique_unicode_id(),
  122. "text": unique_unicode_id(),
  123. "question_timestamp": (participation.contest.start
  124. + timedelta(0, unique_long_id())),
  125. }
  126. args.update(kwargs)
  127. question = Question(**args)
  128. return question
  129. @classmethod
  130. def get_task(cls, **kwargs):
  131. """Create a task"""
  132. args = {
  133. "name": unique_unicode_id(),
  134. "title": unique_unicode_id(),
  135. }
  136. args.update(kwargs)
  137. task = Task(**args)
  138. return task
  139. @classmethod
  140. def get_dataset(cls, task=None, **kwargs):
  141. """Create a dataset"""
  142. task = task if task is not None else cls.get_task()
  143. args = {
  144. "task": task,
  145. "description": unique_unicode_id(),
  146. "task_type": "",
  147. # "None" won't work here as the column is defined as non
  148. # nullable. As soon as we'll depend on SQLAlchemy 1.1 we
  149. # will be able to put JSON.NULL here instead.
  150. "task_type_parameters": {},
  151. "score_type": "",
  152. # Same here.
  153. "score_type_parameters": {},
  154. }
  155. args.update(kwargs)
  156. dataset = Dataset(**args)
  157. return dataset
  158. @classmethod
  159. def get_manager(cls, dataset=None, **kwargs):
  160. """Create a manager."""
  161. dataset = dataset if dataset is not None else cls.get_dataset()
  162. args = {
  163. "dataset": dataset,
  164. "filename": unique_unicode_id(),
  165. "digest": unique_digest(),
  166. }
  167. args.update(kwargs)
  168. manager = Manager(**args)
  169. return manager
  170. @classmethod
  171. def get_submission(cls, task=None, participation=None, **kwargs):
  172. """Create a submission."""
  173. task = task if task is not None \
  174. else cls.get_task(contest=cls.get_contest())
  175. participation = participation if participation is not None \
  176. else cls.get_participation(contest=task.contest)
  177. assert task.contest == participation.contest
  178. args = {
  179. "task": task,
  180. "participation": participation,
  181. "timestamp": (task.contest.start + timedelta(0, unique_long_id())),
  182. }
  183. args.update(kwargs)
  184. submission = Submission(**args)
  185. return submission
  186. @classmethod
  187. def get_token(cls, submission=None, **kwargs):
  188. """Create a token."""
  189. submission = submission if submission is not None \
  190. else cls.get_submission()
  191. args = {
  192. "submission": submission,
  193. "timestamp": (submission.task.contest.start
  194. + timedelta(seconds=unique_long_id())),
  195. }
  196. args.update(kwargs)
  197. token = Token(**args)
  198. return token
  199. @classmethod
  200. def get_submission_result(cls, submission=None, dataset=None, **kwargs):
  201. """Create a submission result."""
  202. task = None
  203. task = submission.task if submission is not None else task
  204. task = dataset.task if dataset is not None else task
  205. submission = submission if submission is not None \
  206. else cls.get_submission(task=task)
  207. dataset = dataset if dataset is not None \
  208. else cls.get_dataset(task=task)
  209. assert submission.task == dataset.task
  210. args = {
  211. "submission": submission,
  212. "dataset": dataset,
  213. }
  214. args.update(kwargs)
  215. submission_result = SubmissionResult(**args)
  216. return submission_result
  217. @classmethod
  218. def get_team(cls, **kwargs):
  219. """Create a team"""
  220. args = {
  221. "code": unique_unicode_id(),
  222. "name": unique_unicode_id(),
  223. }
  224. args.update(kwargs)
  225. team = Team(**args)
  226. return team
  227. class DatabaseMixin(DatabaseObjectGeneratorMixin):
  228. """Mixin for tests with database access."""
  229. @classmethod
  230. def setUpClass(cls):
  231. super().setUpClass()
  232. assert "fortesting" in str(engine), \
  233. "Monkey patching of DB connection string failed"
  234. drop_db()
  235. init_db()
  236. @classmethod
  237. def tearDownClass(cls):
  238. drop_db()
  239. super().tearDownClass()
  240. def setUp(self):
  241. super().setUp()
  242. self.session = Session()
  243. def tearDown(self):
  244. self.session.rollback()
  245. super().tearDown()
  246. def delete_data(self):
  247. """Delete all the data in the DB.
  248. This is useful to call during tear down, for tests that rely on
  249. starting from a clean DB.
  250. """
  251. for table in metadata.tables.values():
  252. self.session.execute(table.delete())
  253. self.session.commit()
  254. @staticmethod
  255. def add_fsobject(digest, content):
  256. dbbackend = DBBackend()
  257. fobj = dbbackend.create_file(digest)
  258. fobj.write(content)
  259. dbbackend.commit_file(fobj, digest)
  260. def add_contest(self, **kwargs):
  261. """Create a contest and add it to the session"""
  262. contest = self.get_contest(**kwargs)
  263. self.session.add(contest)
  264. return contest
  265. def add_announcement(self, **kwargs):
  266. """Create an announcement and add it to the session"""
  267. announcement = self.get_announcement(**kwargs)
  268. self.session.add(announcement)
  269. return announcement
  270. def add_user(self, **kwargs):
  271. """Create a user and add it to the session"""
  272. user = self.get_user(**kwargs)
  273. self.session.add(user)
  274. return user
  275. def add_participation(self, **kwargs):
  276. """Create a participation and add it to the session"""
  277. participation = self.get_participation(**kwargs)
  278. self.session.add(participation)
  279. return participation
  280. def add_message(self, **kwargs):
  281. """Create a message and add it to the session"""
  282. message = self.get_message(**kwargs)
  283. self.session.add(message)
  284. return message
  285. def add_question(self, **kwargs):
  286. """Create a question and add it to the session"""
  287. question = self.get_question(**kwargs)
  288. self.session.add(question)
  289. return question
  290. def add_task(self, **kwargs):
  291. """Create a task and add it to the session"""
  292. task = self.get_task(**kwargs)
  293. self.session.add(task)
  294. return task
  295. def add_statement(self, task=None, **kwargs):
  296. """Create a statement and add it to the session"""
  297. task = task if task is not None else self.add_task()
  298. args = {
  299. "task": task,
  300. "digest": unique_digest(),
  301. "language": unique_unicode_id(),
  302. }
  303. args.update(kwargs)
  304. statement = Statement(**args)
  305. self.session.add(statement)
  306. return statement
  307. def add_dataset(self, **kwargs):
  308. """Create a dataset and add it to the session"""
  309. dataset = self.get_dataset(**kwargs)
  310. self.session.add(dataset)
  311. return dataset
  312. def add_manager(self, dataset=None, **kwargs):
  313. """Create a manager and add it to the session."""
  314. manager = self.get_manager(dataset=dataset, **kwargs)
  315. self.session.add(manager)
  316. return manager
  317. def add_testcase(self, dataset=None, **kwargs):
  318. """Add a testcase."""
  319. dataset = dataset if dataset is not None else self.add_dataset()
  320. args = {
  321. "dataset": dataset,
  322. "codename": unique_unicode_id(),
  323. "input": unique_digest(),
  324. "output": unique_digest(),
  325. }
  326. args.update(kwargs)
  327. testcase = Testcase(**args)
  328. self.session.add(testcase)
  329. return testcase
  330. def add_submission(self, task=None, participation=None, **kwargs):
  331. """Add a submission."""
  332. submission = self.get_submission(task, participation, **kwargs)
  333. self.session.add(submission)
  334. return submission
  335. def add_file(self, submission=None, **kwargs):
  336. """Create a file and add it to the session"""
  337. if submission is None:
  338. submission = self.add_submission()
  339. args = {
  340. "submission": submission,
  341. "filename": unique_unicode_id(),
  342. "digest": unique_digest(),
  343. }
  344. args.update(kwargs)
  345. file_ = File(**args)
  346. self.session.add(file_)
  347. return file_
  348. def add_token(self, submission=None, **kwargs):
  349. """Create a token and add it to the session"""
  350. token = self.get_token(submission, **kwargs)
  351. self.session.add(token)
  352. return token
  353. def add_submission_result(self, submission=None, dataset=None, **kwargs):
  354. """Add a submission result."""
  355. submission_result = self.get_submission_result(
  356. submission, dataset, **kwargs)
  357. self.session.add(submission_result)
  358. return submission_result
  359. def add_executable(self, submission_result=None, **kwargs):
  360. """Create an executable and add it to the session"""
  361. submission_result = submission_result \
  362. if submission_result is not None \
  363. else self.add_submission_result()
  364. args = {
  365. "submission_result": submission_result,
  366. "digest": unique_digest(),
  367. "filename": unique_unicode_id(),
  368. }
  369. args.update(kwargs)
  370. executable = Executable(**args)
  371. self.session.add(executable)
  372. return executable
  373. def add_evaluation(self, submission_result=None, testcase=None, **kwargs):
  374. """Add an evaluation."""
  375. dataset = None
  376. dataset = submission_result.dataset \
  377. if submission_result is not None else dataset
  378. dataset = testcase.dataset if testcase is not None else dataset
  379. submission_result = submission_result \
  380. if submission_result is not None \
  381. else self.add_submission_result(dataset=dataset)
  382. testcase = testcase if testcase is not None else self.add_testcase()
  383. assert submission_result.dataset == testcase.dataset
  384. args = {
  385. "submission_result": submission_result,
  386. "testcase": testcase,
  387. }
  388. args.update(kwargs)
  389. evaluation = Evaluation(**args)
  390. self.session.add(evaluation)
  391. return evaluation
  392. def add_user_test(self, task=None, participation=None, **kwargs):
  393. """Add a user test."""
  394. if task is None:
  395. task = self.add_task(contest=self.add_contest())
  396. participation = participation \
  397. if participation is not None \
  398. else self.add_participation(contest=task.contest)
  399. assert task.contest == participation.contest
  400. args = {
  401. "task": task,
  402. "participation": participation,
  403. "input": unique_digest(),
  404. "timestamp": (task.contest.start + timedelta(0, unique_long_id())),
  405. }
  406. args.update(kwargs)
  407. user_test = UserTest(**args)
  408. self.session.add(user_test)
  409. return user_test
  410. def add_user_test_file(self, user_test=None, **kwargs):
  411. """Create a user test file and add it to the session"""
  412. if user_test is None:
  413. user_test = self.add_user_test()
  414. args = {
  415. "user_test": user_test,
  416. "filename": unique_unicode_id(),
  417. "digest": unique_digest(),
  418. }
  419. args.update(kwargs)
  420. user_test_file = UserTestFile(**args)
  421. self.session.add(user_test_file)
  422. return user_test_file
  423. def add_user_test_manager(self, user_test=None, **kwargs):
  424. """Create a user test manager and add it to the session"""
  425. if user_test is None:
  426. user_test = self.add_user_test()
  427. args = {
  428. "user_test": user_test,
  429. "filename": unique_unicode_id(),
  430. "digest": unique_digest(),
  431. }
  432. args.update(kwargs)
  433. user_test_manager = UserTestManager(**args)
  434. self.session.add(user_test_manager)
  435. return user_test_manager
  436. def add_user_test_result(self, user_test=None, dataset=None, **kwargs):
  437. """Add a user test result."""
  438. task = None
  439. task = user_test.task if user_test is not None else task
  440. task = dataset.task if dataset is not None else task
  441. user_test = user_test \
  442. if user_test is not None else self.add_user_test(task=task)
  443. dataset = dataset \
  444. if dataset is not None else self.add_dataset(task=task)
  445. assert user_test.task == dataset.task
  446. args = {
  447. "user_test": user_test,
  448. "dataset": dataset,
  449. }
  450. args.update(kwargs)
  451. user_test_result = UserTestResult(**args)
  452. self.session.add(user_test_result)
  453. return user_test_result
  454. # Other commonly used generation functions.
  455. def add_submission_with_results(self, task, participation,
  456. compilation_outcome=None):
  457. """Add a submission for the tasks, all of its results, and optionally
  458. the compilation outcome for all results.
  459. """
  460. submission = self.add_submission(task, participation)
  461. results = [self.add_submission_result(submission, dataset)
  462. for dataset in task.datasets]
  463. if compilation_outcome is not None:
  464. for result in results:
  465. result.set_compilation_outcome(compilation_outcome)
  466. return submission, results
  467. def add_user_test_with_results(self, compilation_outcome=None):
  468. """Add a user_test for the first tasks, all of its results, and
  469. optionally the compilation outcome for all results.
  470. """
  471. user_test = self.add_user_test(self.tasks[0], self.participation)
  472. results = [self.add_user_test_result(user_test, dataset)
  473. for dataset in self.tasks[0].datasets]
  474. if compilation_outcome is not None:
  475. for result in results:
  476. result.set_compilation_outcome(compilation_outcome)
  477. return user_test, results
  478. def add_team(self, **kwargs):
  479. """Create a team and add it to the session"""
  480. team = self.get_team(**kwargs)
  481. self.session.add(team)
  482. return team