1
1

test_validation.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. import os
  2. import sys
  3. import unittest
  4. import odml
  5. import odml.validation
  6. import odml.terminology
  7. from . import test_samplefile as samplefile
  8. from .util import TEST_RESOURCES_DIR as RES_DIR
  9. try:
  10. from StringIO import StringIO
  11. except ImportError:
  12. from io import StringIO
  13. Validate = odml.validation.Validation
  14. class TestValidation(unittest.TestCase):
  15. def setUp(self):
  16. self.doc = samplefile.SampleFileCreator().create_document()
  17. @staticmethod
  18. def filter_repository_errors(errors):
  19. find_msg = "A section should have an associated repository"
  20. return filter(lambda x: find_msg not in x.msg, errors)
  21. def assertError(self, res, err, filter_rep=True):
  22. """
  23. Passes only if err appears in res.errors
  24. """
  25. errs = res.errors
  26. if filter_rep:
  27. errs = self.filter_repository_errors(errs)
  28. for i in errs:
  29. if err in i.msg:
  30. return
  31. self.assertEqual(errs, err)
  32. def test_property_values_cardinality(self):
  33. doc = odml.Document()
  34. sec = odml.Section(name="sec", type="sec_type", parent=doc)
  35. # Test no caught warning on empty cardinality
  36. prop = odml.Property(name="prop_empty_cardinality", values=[1, 2, 3, 4], parent=sec)
  37. # Check that the current property is not in the list of validation warnings or errors
  38. for err in Validate(doc).errors:
  39. self.assertNotEqual(err.obj.id, prop.id)
  40. # Test no warning on valid cardinality
  41. prop = odml.Property(name="prop_valid_cardinality", values=[1, 2, 3, 4],
  42. val_cardinality=(2, 10), parent=sec)
  43. for err in Validate(doc).errors:
  44. self.assertNotEqual(err.obj.id, prop.id)
  45. # Test maximum value cardinality validation
  46. test_val = [1, 2, 3]
  47. test_card = 2
  48. prop = odml.Property(name="prop_invalid_max_val", values=test_val,
  49. val_cardinality=test_card, parent=sec)
  50. test_msg_base = "Property values cardinality violated"
  51. test_msg = "%s (maximum %s values, %s found)" % (test_msg_base, test_card, len(prop.values))
  52. for err in Validate(doc).errors:
  53. if err.obj.id == prop.id:
  54. self.assertFalse(err.is_error)
  55. self.assertIn(test_msg, err.msg)
  56. # Test minimum value cardinality validation
  57. test_val = "I am a nice text to test"
  58. test_card = (4, None)
  59. prop = odml.Property(name="prop_invalid_min_val", values=test_val,
  60. val_cardinality=test_card, parent=sec)
  61. test_msg = "%s (minimum %s values, %s found)" % (test_msg_base, test_card[0],
  62. len(prop.values))
  63. for err in Validate(doc).errors:
  64. if err.obj.id == prop.id:
  65. self.assertFalse(err.is_error)
  66. self.assertIn(test_msg, err.msg)
  67. def test_section_properties_cardinality(self):
  68. msg_base = "Section properties cardinality violated"
  69. doc = odml.Document()
  70. # Test no caught warning on empty cardinality
  71. sec = odml.Section(name="prop_empty_cardinality", type="test", parent=doc)
  72. # Check that the current section did not throw any properties cardinality warnings
  73. for err in Validate(doc).errors:
  74. if err.obj.id == sec.id:
  75. self.assertNotIn(msg_base, err.msg)
  76. # Test no warning on valid cardinality
  77. sec = odml.Section(name="prop_valid_cardinality", prop_cardinality=(1, 2), parent=doc)
  78. _ = odml.Property(parent=sec)
  79. for err in Validate(doc).errors:
  80. if err.obj.id == sec.id:
  81. self.assertNotIn(msg_base, err.msg)
  82. # Test maximum value cardinality validation
  83. test_range = 3
  84. test_card = 2
  85. sec = odml.Section(name="prop_invalid_max_val", prop_cardinality=test_card, parent=doc)
  86. for _ in range(test_range):
  87. _ = odml.Property(parent=sec)
  88. test_msg = "%s (maximum %s values, %s found)" % (msg_base, test_card, len(sec.properties))
  89. # Once ValidationErrors provide validation ids, the following can be simplified.
  90. found = False
  91. for err in Validate(doc).errors:
  92. if err.obj.id == sec.id and msg_base in err.msg:
  93. self.assertFalse(err.is_error)
  94. self.assertIn(test_msg, err.msg)
  95. found = True
  96. self.assertTrue(found)
  97. # Test minimum value cardinality validation
  98. test_card = (4, None)
  99. sec = odml.Section(name="prop_invalid_min_val", prop_cardinality=test_card, parent=sec)
  100. _ = odml.Property(parent=sec)
  101. test_msg = "%s (minimum %s values, %s found)" % (msg_base, test_card[0],
  102. len(sec.properties))
  103. # Once ValidationErrors provide validation ids, the following can be simplified.
  104. found = False
  105. for err in Validate(doc).errors:
  106. if err.obj.id == sec.id and msg_base in err.msg:
  107. self.assertFalse(err.is_error)
  108. self.assertIn(test_msg, err.msg)
  109. found = True
  110. self.assertTrue(found)
  111. def test_section_sections_cardinality(self):
  112. msg_base = "Section sections cardinality violated"
  113. doc = odml.Document()
  114. # Test no caught warning on empty cardinality
  115. sec = odml.Section(name="sec_empty_cardinality", type="test", parent=doc)
  116. # Check that the current section did not throw any sections cardinality warnings
  117. for err in Validate(doc).errors:
  118. if err.obj.id == sec.id:
  119. self.assertNotIn(msg_base, err.msg)
  120. # Test no warning on valid cardinality
  121. sec = odml.Section(name="sec_valid_cardinality", sec_cardinality=(1, 2), parent=doc)
  122. _ = odml.Section(name="sub_sec_valid_cardinality", type="test", parent=sec)
  123. for err in Validate(doc).errors:
  124. if err.obj.id == sec.id:
  125. self.assertNotIn(msg_base, err.msg)
  126. # Test maximum value cardinality validation
  127. test_range = 3
  128. test_card = 2
  129. sec = odml.Section(name="sec_invalid_max_val", sec_cardinality=test_card, parent=doc)
  130. for i in range(test_range):
  131. sec_name = "sub_sec_invalid_max_val_%s" % i
  132. _ = odml.Section(name=sec_name, type="test", parent=sec)
  133. test_msg = "%s (maximum %s values, %s found)" % (msg_base, test_card, len(sec.sections))
  134. # Once ValidationErrors provide validation ids, the following can be simplified.
  135. found = False
  136. for err in Validate(doc).errors:
  137. if err.obj.id == sec.id and msg_base in err.msg:
  138. self.assertFalse(err.is_error)
  139. self.assertIn(test_msg, err.msg)
  140. found = True
  141. self.assertTrue(found)
  142. # Test minimum value cardinality validation
  143. test_card = (4, None)
  144. sec = odml.Section(name="sec_invalid_min_val", sec_cardinality=test_card, parent=sec)
  145. _ = odml.Section(name="sub_sec_invalid_min_val", type="test", parent=sec)
  146. test_msg = "%s (minimum %s values, %s found)" % (msg_base, test_card[0],
  147. len(sec.sections))
  148. # Once ValidationErrors provide validation ids, the following can be simplified.
  149. found = False
  150. for err in Validate(doc).errors:
  151. if err.obj.id == sec.id and msg_base in err.msg:
  152. self.assertFalse(err.is_error)
  153. self.assertIn(test_msg, err.msg)
  154. found = True
  155. self.assertTrue(found)
  156. def test_uniques(self):
  157. self.assertRaises(KeyError, samplefile.parse, """
  158. s1[t1]
  159. s1[t1]
  160. """)
  161. self.assertRaises(KeyError, samplefile.parse, """
  162. s1[t1]
  163. - p1
  164. - p1
  165. """)
  166. def test_property_in_terminology(self):
  167. odml.validation.Validation.register_handler("property",
  168. odml.validation.property_terminology_check)
  169. doc = samplefile.parse("""
  170. s1[t1]
  171. - P1
  172. """)
  173. odml.terminology.terminologies['term'] = samplefile.parse("""
  174. S1[T1]
  175. - P1
  176. """)
  177. doc.repository = 'term'
  178. res = Validate(doc)
  179. self.assertEqual(res.errors, [])
  180. doc = samplefile.parse("""
  181. s1[t1]
  182. - p1
  183. - P1
  184. """)
  185. doc.repository = 'term'
  186. res = Validate(doc)
  187. self.assertError(res, "Property 'p1' not found in terminology")
  188. def test_property_values(self):
  189. # different units
  190. doc = samplefile.parse("""s1[t1]""")
  191. prop = odml.Property(name="p1", value=[0, 1])
  192. doc["s1"].append(prop)
  193. # missing dependency
  194. prop.dependency = "p2"
  195. res = Validate(doc)
  196. self.assertError(res, "non-existent dependency object")
  197. def test_property_unique_ids(self):
  198. """
  199. Test if identical ids in properties raise a validation error
  200. """
  201. doc = odml.Document()
  202. sec_one = odml.Section("sec1", parent=doc)
  203. sec_two = odml.Section("sec2", parent=doc)
  204. prop = odml.Property("prop", parent=sec_one)
  205. cprop = prop.clone(keep_id=True)
  206. sec_two.append(cprop)
  207. res = Validate(doc)
  208. self.assertError(res, "Duplicate id in Property")
  209. def test_section_unique_ids(self):
  210. """
  211. Test if identical ids in sections raise a validation error.
  212. """
  213. doc = odml.Document()
  214. sec = odml.Section("sec", parent=doc)
  215. csec = sec.clone(keep_id=True)
  216. sec.append(csec)
  217. res = Validate(doc)
  218. self.assertError(res, "Duplicate id in Section")
  219. def test_section_name_readable(self):
  220. """
  221. Test if section name is not uuid and thus more readable.
  222. """
  223. doc = odml.Document()
  224. sec = odml.Section("sec", parent=doc)
  225. sec.name = sec.id
  226. res = Validate(doc)
  227. self.assertError(res, "Name not assigned")
  228. def test_property_name_readable(self):
  229. """
  230. Test if property name is not uuid and thus more readable.
  231. """
  232. doc = odml.Document()
  233. sec = odml.Section("sec", parent=doc)
  234. prop = odml.Property("prop", parent=sec)
  235. prop.name = prop.id
  236. res = Validate(doc)
  237. self.assertError(res, "Name not assigned")
  238. def test_standalone_section(self):
  239. """
  240. Test if standalone section does not return errors if required attributes are correct.
  241. If type is not specified, check error message.
  242. """
  243. sec_one = odml.Section("sec1")
  244. res = Validate(sec_one)
  245. self.assertError(res, "Section type not specified")
  246. def test_standalone_property(self):
  247. """
  248. Test if standalone property does not return errors if required attributes are correct.
  249. """
  250. prop = odml.Property()
  251. prop.type = ""
  252. errs = list(filter(lambda x: x.is_error, Validate(prop).errors))
  253. self.assertEqual(len(errs), 0)
  254. def test_section_init(self):
  255. """
  256. Test validation errors printed to stdout on section init.
  257. """
  258. check_msg = "Missing required attribute 'type'"
  259. val_errs = StringIO()
  260. old_stdout = sys.stdout
  261. sys.stdout = val_errs
  262. odml.Section(name="sec", type=None)
  263. sys.stdout = old_stdout
  264. self.assertIn(check_msg, val_errs.getvalue())
  265. def test_prop_string_values(self):
  266. """
  267. Test if property values set as dtype string but could be of different dtype
  268. raise validation warning.
  269. """
  270. val = ['-13', '101', '-11', 'hello']
  271. prop0 = odml.Property(name='words', dtype="string", values=val)
  272. self.assertEqual(len(Validate(prop0).errors), 0)
  273. msg_base = 'Dtype of property "%s" currently is "string", but might fit dtype "%s"!'
  274. val = ['-13', '101', '-11', '0', '-8']
  275. prop1 = odml.Property(name='members', dtype="string", values=val)
  276. self.assertError(Validate(prop1), msg_base % ("members", "int"))
  277. val = ['-4.8', '10.0', '-11.9', '-10.0', '18.0']
  278. prop2 = odml.Property(name='potential', dtype="string", values=val)
  279. self.assertError(Validate(prop2), msg_base % ("potential", "float"))
  280. val = ['1997-12-14', '00-12-14', '89-07-04']
  281. prop3 = odml.Property(name='dates', dtype="string", values=val)
  282. self.assertError(Validate(prop3), msg_base % ("dates", "date"))
  283. val = ['97-12-14 11:11:11', '97-12-14 12:12', '1997-12-14 03:03']
  284. prop4 = odml.Property(name='datetimes', dtype="string", values=val)
  285. self.assertError(Validate(prop4), msg_base % ("datetimes", "datetime"))
  286. val = ['11:11:11', '12:12:12', '03:03:03']
  287. prop5 = odml.Property(name='times', dtype="string", values=val)
  288. self.assertError(Validate(prop5), msg_base % ("times", "time"))
  289. val = ['False', True, 'TRUE', False, 't']
  290. prop6 = odml.Property(name='sent', dtype="string", values=val)
  291. self.assertError(Validate(prop6), msg_base % ("sent", "boolean"))
  292. val = ['line1\n line2', 'line3\n line4', '\nline5\nline6']
  293. prop7 = odml.Property(name='texts', dtype="string", values=val)
  294. self.assertError(Validate(prop7), msg_base % ("texts", "text"))
  295. val = ['(39.12; 67.19)', '(39.12; 67.19)', '(39.12; 67.18)']
  296. prop8 = odml.Property(name="Location", dtype='string', values=val)
  297. self.assertError(Validate(prop8), msg_base % ("Location", "2-tuple"))
  298. val = ['(39.12; 89; 67.19)', '(39.12; 78; 67.19)', '(39.12; 56; 67.18)']
  299. prop9 = odml.Property(name="Coos", dtype='string', values=val)
  300. self.assertError(Validate(prop9), msg_base % ("Coos", "3-tuple"))
  301. def load_section_validation(self, doc):
  302. filter_func = lambda x: x.msg == filter_msg and x.obj.name == filter_name
  303. # Check error for deliberate empty section type
  304. filter_msg = "Missing required attribute 'type'"
  305. filter_name = "sec_type_empty"
  306. self.assertGreater(len(list(filter(filter_func, Validate(doc).errors))), 0)
  307. # Check warning for not specified section type
  308. filter_msg = "Section type not specified"
  309. filter_name = "sec_type_undefined"
  310. self.assertGreater(len(list(filter(filter_func, Validate(doc).errors))), 0)
  311. def test_load_section_xml(self):
  312. """
  313. Test if loading xml document raises validation errors for Sections with undefined type.
  314. """
  315. path = os.path.join(RES_DIR, "validation_section.xml")
  316. doc = odml.load(path)
  317. self.load_section_validation(doc)
  318. def test_load_section_json(self):
  319. """
  320. Test if loading json document raises validation errors for Sections with undefined type.
  321. """
  322. path = os.path.join(RES_DIR, "validation_section.json")
  323. doc = odml.load(path, "JSON")
  324. self.load_section_validation(doc)
  325. def test_load_section_yaml(self):
  326. """
  327. Test if loading yaml document raises validation errors for Sections with undefined type.
  328. """
  329. path = os.path.join(RES_DIR, "validation_section.yaml")
  330. doc = odml.load(path, "YAML")
  331. self.load_section_validation(doc)
  332. def load_dtypes_validation(self, doc):
  333. msg_base = 'Dtype of property "%s" currently is "string", but might fit dtype "%s"!'
  334. doc_val = Validate(doc)
  335. self.assertError(doc_val, msg_base % ("members_no", "int"))
  336. self.assertError(doc_val, msg_base % ("potential_no", "float"))
  337. self.assertError(doc_val, msg_base % ("dates_no", "date"))
  338. self.assertError(doc_val, msg_base % ("datetimes_no", "datetime"))
  339. self.assertError(doc_val, msg_base % ("times_no", "time"))
  340. self.assertError(doc_val, msg_base % ("sent_no", "boolean"))
  341. self.assertError(doc_val, msg_base % ("Location_no", "2-tuple"))
  342. self.assertError(doc_val, msg_base % ("Coos_no", "3-tuple"))
  343. self.assertError(doc_val, msg_base % ("members_mislabelled", "int"))
  344. self.assertError(doc_val, msg_base % ("potential_mislabelled", "float"))
  345. self.assertError(doc_val, msg_base % ("dates_mislabelled", "date"))
  346. self.assertError(doc_val, msg_base % ("datetimes_mislabelled", "datetime"))
  347. self.assertError(doc_val, msg_base % ("times_mislabelled", "time"))
  348. self.assertError(doc_val, msg_base % ("sent_mislabelled", "boolean"))
  349. self.assertError(doc_val, msg_base % ("texts_mislabelled", "text"))
  350. self.assertError(doc_val, msg_base % ("Location_mislabelled", "2-tuple"))
  351. self.assertError(doc_val, msg_base % ("Coos_mislabelled", "3-tuple"))
  352. def test_load_dtypes_xml(self):
  353. """
  354. Test if loading xml document raises validation errors
  355. for Properties with undefined dtypes.
  356. """
  357. path = os.path.join(RES_DIR, "validation_dtypes.xml")
  358. doc = odml.load(path)
  359. self.load_dtypes_validation(doc)
  360. def test_load_dtypes_json(self):
  361. """
  362. Test if loading json document raises validation errors
  363. for Properties with undefined dtypes.
  364. """
  365. path = os.path.join(RES_DIR, "validation_dtypes.json")
  366. doc = odml.load(path, "JSON")
  367. self.load_dtypes_validation(doc)
  368. def test_load_dtypes_yaml(self):
  369. """
  370. Test if loading yaml document raises validation errors
  371. for Properties with undefined dtypes.
  372. """
  373. path = os.path.join(RES_DIR, "validation_dtypes.yaml")
  374. doc = odml.load(path, "YAML")
  375. self.load_dtypes_validation(doc)
  376. def test_section_in_terminology(self):
  377. """
  378. Test optional section in terminology validation.
  379. """
  380. doc = samplefile.parse("""s1[T1]""")
  381. # Set up custom validation and add section_repository_present handler
  382. res = odml.validation.Validation(doc, validate=False, reset=True)
  383. handler = odml.validation.section_repository_present
  384. res.register_custom_handler('section', handler)
  385. res.run_validation()
  386. self.assertError(res, "A section should have an associated repository",
  387. filter_rep=False)
  388. odml.terminology.terminologies['map'] = samplefile.parse("""
  389. s0[t0]
  390. - S1[T1]
  391. """)
  392. doc.sections[0].repository = 'map'
  393. res = Validate(doc)
  394. # self.assertEqual(list(self.filter_mapping_errors(res.errors)), [])
  395. self.assertEqual(res.errors, [])