暫無描述
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_lecrud.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. """
  2. Tests for _LeCrud and LeCrud
  3. """
  4. import copy
  5. import unittest
  6. from unittest import TestCase
  7. from unittest.mock import patch
  8. import EditorialModel
  9. import DataSource.dummy
  10. import leapi
  11. import leapi.test.utils
  12. from leapi.lecrud import _LeCrud
  13. ## @brief Test LeCrud methods
  14. # @note those tests need the full dynamically generated code
  15. class LeCrudTestCase(TestCase):
  16. @classmethod
  17. def setUpClass(cls):
  18. """ Write the generated code in a temporary directory and import it """
  19. cls.tmpdir = leapi.test.utils.tmp_load_factory_code()
  20. @classmethod
  21. def tearDownClass(cls):
  22. """ Remove the temporary directory created at class setup """
  23. leapi.test.utils.cleanup(cls.tmpdir)
  24. def test_split_query_filter(self):
  25. """ Tests the _split_filter() classmethod """
  26. import dyncode
  27. query_results = {
  28. 'Hello = world' : ('Hello', '=', 'world'),
  29. 'hello <= "world"': ('hello', '<=', '"world"'),
  30. '_he42_ll-o >= \'world"': ('_he42_ll-o', '>=', '\'world"'),
  31. 'foo in ["foo", 42, \'bar\']': ('foo', ' in ', '["foo", 42, \'bar\']'),
  32. ' bar42 < 42': ('bar42', '<', '42'),
  33. ' _hidden > 1337': ('_hidden', '>', '1337'),
  34. '_42 not in foobar': ('_42', ' not in ', 'foobar'),
  35. 'hello in foo':('hello', ' in ', 'foo'),
  36. "\t\t\thello\t\t\nin\nfoo\t\t\n\t":('hello', ' in ', 'foo'),
  37. "hello \nnot\tin \nfoo":('hello', ' not in ', 'foo'),
  38. 'hello != bar':('hello', '!=', 'bar'),
  39. 'hello = "world>= <= != in not in"': ('hello', '=', '"world>= <= != in not in"'),
  40. 'superior.parent = 13': ('superior.parent', '=', '13'),
  41. 'hello like "%world%"': ('hello', ' like ', '"%world%"'),
  42. 'world not like "foo%bar"': ('world', ' not like ', '"foo%bar"'),
  43. }
  44. for query, result in query_results.items():
  45. res = dyncode.LeCrud._split_filter(query)
  46. self.assertEqual(res, result, "When parsing the query : '%s' the returned value is different from the expected '%s'"%(query, result))
  47. def test_invalid_split_query_filter(self):
  48. """ Testing the _split_filter() method with invalid queries """
  49. import dyncode
  50. invalid_queries = [
  51. '42 = 42',
  52. '4hello = foo',
  53. 'foo == bar',
  54. 'hello >> world',
  55. 'hello = ',
  56. ' = world',
  57. '=',
  58. '42',
  59. '"hello" = world',
  60. 'foo.bar = 15',
  61. ]
  62. for query in invalid_queries:
  63. with self.assertRaises(ValueError, msg='But the query was not valid : "%s"'%query):
  64. dyncode.LeCrud._split_filter(query)
  65. def test_field_is_relational(self):
  66. """ Testing the test to check if a field is relational """
  67. from dyncode import LeCrud
  68. test_fields = {
  69. 'superior.parent': True,
  70. 'subordinate.parent': True,
  71. 'foofoo.foo': False,
  72. }
  73. for field, eres in test_fields.items():
  74. self.assertEqual(LeCrud._field_is_relational(field), eres)
  75. def test_check_datas(self):
  76. """ testing the check_datas* methods """
  77. from dyncode import Publication, Numero, LeObject
  78. datas = { 'titre':'foobar' }
  79. Numero.check_datas_value(datas, False, False)
  80. Numero.check_datas_value(datas, True, False)
  81. with self.assertRaises(leapi.lecrud.LeApiDataCheckError):
  82. Numero.check_datas_value({}, True)
  83. def test_prepare_filters(self):
  84. """ Testing the _prepare_filters() method """
  85. from dyncode import Publication, Numero, LeObject, Personnes
  86. #Simple filters
  87. filters = [
  88. 'lodel_id = 1',
  89. 'superior.parent > 2'
  90. ]
  91. filt, rfilt = Numero._prepare_filters(filters)
  92. self.assertEqual(filt, [('lodel_id', '=', '1')])
  93. self.assertEqual(rfilt, [((leapi.leobject.REL_SUP,'parent'), '>', '2')])
  94. #All fields, no relationnal and class given
  95. filters = []
  96. res_filt = []
  97. for field in Numero._fields:
  98. filters.append('%s=1'%field)
  99. res_filt.append((field, '=', '1'))
  100. filt, rfilt = Publication._prepare_filters(filters)
  101. self.assertEqual(rfilt, [])
  102. self.assertEqual(filt, res_filt)
  103. #Mixed type filters (tuple and string)
  104. filters = [
  105. ('lodel_id', '<=', '0'),
  106. 'subordinate.parent = 2',
  107. ]
  108. filt, rfilt = Numero._prepare_filters(filters)
  109. self.assertEqual(filt, [('lodel_id', '<=', '0')])
  110. self.assertEqual(rfilt, [((leapi.leobject.REL_SUB,'parent'), '=', '2')])
  111. def test_prepare_filters_invalid(self):
  112. """ Testing the _prepare_filters() method """
  113. from dyncode import LeCrud, Publication, Numero, Personnes, LeObject
  114. #Numero fields filters but no letype nor leclass given
  115. filters = []
  116. res_filt = []
  117. for field in Numero._fields:
  118. filters.append('%s=1'%field)
  119. res_filt.append((field, '=', '1'))
  120. with self.assertRaises(leapi.lecrud.LeApiDataCheckError):
  121. LeObject._prepare_filters(filters)
  122. #simply invalid filters
  123. filters = ['hello world !']
  124. with self.assertRaises(ValueError):
  125. Personnes._prepare_filters(filters)
  126. def test_prepare_order_fields(self):
  127. """ Testing the _prepare_order_fields """
  128. from dyncode import Article
  129. order_fields_list = [
  130. (
  131. ['titre'],
  132. [('titre', 'ASC')]
  133. ),
  134. (
  135. [('titre', 'asc')],
  136. [('titre', 'ASC')]
  137. ),
  138. (
  139. ['lodel_id', ('titre', 'asc')],
  140. [('lodel_id', 'ASC'), ('titre', 'ASC')]
  141. ),
  142. (
  143. [('titre', 'desc')],
  144. [('titre', 'DESC')]
  145. ),
  146. ]
  147. for fields_arg, expected_result in order_fields_list:
  148. result = Article._prepare_order_fields(fields_arg)
  149. self.assertEqual(result, expected_result)
  150. #
  151. # Tests mocking the datasource
  152. @patch('DataSource.dummy.leapidatasource.DummyDatasource.insert')
  153. def test_insert(self, dsmock):
  154. from dyncode import Publication, Numero, LeObject, Personne, Article
  155. ndatas = [
  156. (Numero, {'titre' : 'FooBar'}),
  157. (Numero, {'titre':'hello'}),
  158. (Personne, {'nom':'world', 'prenom':'hello'}),
  159. (Article, {'titre': 'Ar Boof', 'soustitre': 'Wow!'}),
  160. ]
  161. for lecclass, ndats in ndatas:
  162. lecclass.insert(ndats)
  163. # Adding default values for internal fields
  164. expt_dats = copy.copy(ndats)
  165. expt_dats['string'] = None
  166. expt_dats['class_id'] = lecclass._class_id
  167. expt_dats['type_id'] = lecclass._type_id if lecclass.implements_letype() else None
  168. dsmock.assert_called_once_with(lecclass, **expt_dats)
  169. dsmock.reset_mock()
  170. lecclass.insert(ndats)
  171. dsmock.assert_called_once_with(lecclass, **expt_dats)
  172. dsmock.reset_mock()
  173. ## @todo try failing on inserting from LeClass child or LeObject
  174. @patch('DataSource.dummy.leapidatasource.DummyDatasource.insert')
  175. def test_insert_fails(self, dsmock):
  176. from dyncode import Publication, Numero, LeObject, Personne, Article
  177. ndatas = [
  178. (Numero, dict()),
  179. (Numero, {'titre':'hello', 'lodel_id':42}),
  180. (Numero, {'tititre': 'hehello'}),
  181. (Personne, {'titre':'hello'}),
  182. (Article, {'titre': 'hello'}),
  183. ]
  184. for lecclass, ndats in ndatas:
  185. with self.assertRaises(leapi.lecrud.LeApiDataCheckError, msg="But trying to insert %s as %s"%(ndats, lecclass.__name__)):
  186. lecclass.insert(ndats)
  187. assert not dsmock.called
  188. pass
  189. @patch('DataSource.dummy.leapidatasource.DummyDatasource.update')
  190. def test_update(self, dsmock):
  191. from dyncode import Publication, Numero, LeObject
  192. args_l = [
  193. (
  194. Numero,
  195. {'lodel_id':'1'},
  196. {'titre': 'foobar'},
  197. 1,
  198. ),
  199. ]
  200. for ccls, initarg, qdatas, eid in args_l:
  201. obji = ccls(**initarg)
  202. obji._instanciation_complete = True # fake full-instance
  203. obji.update(qdatas)
  204. dsmock.assert_called_once_with(ccls, eid, **qdatas)
  205. ## @todo test invalid get
  206. @patch('DataSource.dummy.leapidatasource.DummyDatasource.select')
  207. def test_get(self, dsmock):
  208. """ Test the select method without group, limit, sort or offset """
  209. from dyncode import Publication, Numero, LeObject, Textes
  210. args = [
  211. (
  212. Numero,
  213. ['lodel_id', 'superior.parent'],
  214. ['titre != "foobar"'],
  215. ['lodel_id', (leapi.leobject.REL_SUP, 'parent')],
  216. [ ('titre','!=', '"foobar"'),
  217. ('type_id', '=', Numero._type_id),
  218. ('class_id', '=', Numero._class_id),
  219. ],
  220. []
  221. ),
  222. (
  223. Numero,
  224. ['lodel_id', 'titre', 'superior.parent', 'subordinate.translation'],
  225. ['superior.parent in [1,2,3,4,5]'],
  226. ['lodel_id', 'titre', (leapi.leobject.REL_SUP,'parent'), (leapi.leobject.REL_SUB, 'translation')],
  227. [
  228. ('type_id', '=', Numero._type_id),
  229. ('class_id', '=', Numero._class_id),
  230. ],
  231. [( (leapi.leobject.REL_SUP, 'parent'), ' in ', '[1,2,3,4,5]')]
  232. ),
  233. (
  234. Numero,
  235. [],
  236. [],
  237. Numero.fieldlist(),
  238. [
  239. ('type_id', '=', Numero._type_id),
  240. ('class_id', '=', Numero._class_id),
  241. ],
  242. []
  243. ),
  244. (
  245. Textes,
  246. ['lodel_id', 'titre', 'soustitre', 'superior.parent'],
  247. ['titre != "foobar"'],
  248. ['lodel_id', 'titre', 'soustitre', (leapi.leobject.REL_SUP, 'parent')],
  249. [ ('titre','!=', '"foobar"'),
  250. ('class_id', '=', Textes._class_id),
  251. ],
  252. [],
  253. ),
  254. (
  255. LeObject,
  256. ['lodel_id'],
  257. [],
  258. ['lodel_id'],
  259. [],
  260. [],
  261. ),
  262. ]
  263. for callcls, field_list, filters, fl_ds, filters_ds, rfilters_ds in args:
  264. callcls.get(filters, field_list)
  265. dsmock.assert_called_with(
  266. target_cls = callcls,
  267. field_list = fl_ds,
  268. filters = filters_ds,
  269. rel_filters = rfilters_ds,
  270. order=None,
  271. group=None,
  272. limit=None,
  273. offset=0,
  274. instanciate=True
  275. )
  276. dsmock.reset_mock()
  277. @patch('DataSource.dummy.leapidatasource.DummyDatasource.select')
  278. def test_get_complete(self, dsmock):
  279. """ Test the select method with group limit sort and offset arguments """
  280. from dyncode import Numero
  281. args = [
  282. (
  283. Numero,
  284. {
  285. 'query_filters': [],
  286. 'field_list': ['lodel_id'],
  287. 'group': ['titre'],
  288. 'limit': 10,
  289. },
  290. {
  291. 'target_cls': Numero,
  292. 'field_list': ['lodel_id'],
  293. 'filters': [],
  294. 'rel_filters': [],
  295. 'group': [('titre', 'ASC')],
  296. 'order': None,
  297. 'limit': 10,
  298. 'offset': 0,
  299. },
  300. ),
  301. (
  302. Numero,
  303. {
  304. 'query_filters': ['superior.parent = 20'],
  305. 'field_list': ['lodel_id'],
  306. 'offset': 1024,
  307. 'order': ['titre', ('lodel_id', 'desc')]
  308. },
  309. {
  310. 'target_cls': Numero,
  311. 'field_list': ['lodel_id'],
  312. 'filters': [],
  313. 'rel_filters': [((leapi.lecrud.REL_SUP, 'parent'), '=', '20')],
  314. 'group': None,
  315. 'order': [('titre', 'ASC'), ('lodel_id', 'DESC')],
  316. 'limit': None,
  317. 'offset': 1024,
  318. },
  319. ),
  320. (
  321. Numero,
  322. {
  323. 'query_filters': ['superior.parent = 20'],
  324. 'field_list': ['lodel_id'],
  325. 'offset': 1024,
  326. 'limit': 2,
  327. 'order': ['titre', ('lodel_id', 'desc')],
  328. 'group': ['titre'],
  329. },
  330. {
  331. 'target_cls': Numero,
  332. 'field_list': ['lodel_id'],
  333. 'filters': [],
  334. 'rel_filters': [((leapi.lecrud.REL_SUP, 'parent'), '=', '20')],
  335. 'group': [('titre', 'ASC')],
  336. 'order': [('titre', 'ASC'), ('lodel_id', 'DESC')],
  337. 'limit': 2,
  338. 'offset': 1024,
  339. },
  340. ),
  341. ]
  342. for callcls, getargs, exptargs in args:
  343. if callcls.implements_letype:
  344. exptargs['filters'].append( ('type_id', '=', callcls._type_id) )
  345. if callcls.implements_leclass:
  346. exptargs['filters'].append( ('class_id', '=', callcls._class_id) )
  347. exptargs['instanciate'] = True
  348. callcls.get(**getargs)
  349. dsmock.assert_called_once_with(**exptargs)
  350. dsmock.reset_mock()
  351. #
  352. # Utils methods check
  353. #
  354. def test_name2class(self):
  355. """ Testing name2class method """
  356. from dyncode import LeCrud, LeObject, LeRel2Type
  357. # Fetch all leapi dyn classes
  358. cls_lst = ['LeCrud', 'LeObject', 'LeRelation', 'LeHierarch', 'LeRel2Type', 'LeClass', 'LeType']
  359. leo_lst = LeObject._me_uid.values()
  360. r2t_lst = list()
  361. for leo in leo_lst:
  362. if leo.is_leclass() and hasattr(leo, '_linked_types'):
  363. for relation_name, relleo in leo._linked_types.items():
  364. r2t_lst.append(LeRel2Type.relname(leo, relleo, relation_name))
  365. leo_lst = [cls.__name__ for cls in leo_lst]
  366. # Begin test
  367. for clsname in cls_lst + leo_lst + r2t_lst:
  368. cls = LeCrud.name2class(clsname)
  369. self.assertEqual(cls.__name__, clsname)
  370. #Testing bad name or type
  371. badnames = ['_LeObject', 'foobar']
  372. for badname in badnames:
  373. self.assertFalse(LeCrud.name2class(badname))
  374. badnames = [int, LeObject]
  375. for badname in badnames:
  376. with self.assertRaises(ValueError):
  377. LeCrud.name2class(badname)
  378. def test_leobject(self):
  379. """ Test the LeObject shortcut getter """
  380. from dyncode import LeObject, LeCrud
  381. self.assertEqual(LeObject, LeCrud.leobject())
  382. def test_uidname(self):
  383. """ Tests the uid name getter """
  384. from dyncode import LeCrud, LeObject, LeRelation, Article, LeRel2Type
  385. with self.assertRaises(NotImplementedError):
  386. LeCrud.uidname()
  387. for lec in [LeObject, LeRelation, Article, LeRel2Type]:
  388. self.assertIn(lec.uidname(), lec._uid_fieldtype.keys())
  389. self.assertEqual(LeObject.uidname(), 'lodel_id')
  390. self.assertEqual(LeRelation.uidname(), 'id_relation')
  391. def test_typeasserts(self):
  392. """ Tests te implements_le* and is_le* methods """
  393. from dyncode import LeObject, LeCrud, LeRelation, LeHierarch, LeRel2Type, Article, Textes, RelTextesPersonneAuteur
  394. self.assertTrue(LeObject.is_leobject())