common_io_test.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. '''
  2. Common tests for IOs:
  3. * check presence of all necessary attr
  4. * check types
  5. * write/read consistency
  6. See BaseTestIO.
  7. The public URL is in url_for_tests.
  8. To deposite new testing files, please create a account at
  9. gin.g-node.org and upload files at NeuralEnsemble/ephy_testing_data
  10. data repo.
  11. '''
  12. __test__ = False
  13. import os
  14. from copy import copy
  15. import unittest
  16. from neo.core import Block, Segment
  17. from neo.test.tools import (assert_same_sub_schema,
  18. assert_neo_object_is_compliant,
  19. assert_sub_schema_is_lazy_loaded,
  20. assert_children_empty)
  21. from neo.test.rawiotest.tools import (can_use_network, make_all_directories,
  22. download_test_file, create_local_temp_dir)
  23. from neo.test.iotest.tools import (cleanup_test_file,
  24. close_object_safe, create_generic_io_object,
  25. create_generic_reader,
  26. create_generic_writer,
  27. iter_generic_io_objects,
  28. iter_generic_readers, iter_read_objects,
  29. read_generic,
  30. write_generic)
  31. from neo.test.generate_datasets import generate_from_supported_objects
  32. # url_for_tests = "https://portal.g-node.org/neo/" #This is the old place
  33. url_for_tests = "https://web.gin.g-node.org/NeuralEnsemble/ephy_testing_data/raw/master/"
  34. class BaseTestIO:
  35. '''
  36. This class make common tests for all IOs.
  37. Several startegies:
  38. * for IO able to read write : test_write_then_read
  39. * for IO able to read write with hash conservation (optional):
  40. test_read_then_write
  41. * for all IOs : test_assert_readed_neo_object_is_compliant
  42. 2 cases:
  43. * files are at G-node and downloaded:
  44. download_test_files_if_not_present
  45. * files are generated by MyIO.write()
  46. '''
  47. # ~ __test__ = False
  48. # all IO test need to modify this:
  49. ioclass = None # the IOclass to be tested
  50. files_to_test = [] # list of files to test compliances
  51. files_to_download = [] # when files are at G-Node
  52. # when reading then writing produces files with identical hashes
  53. hash_conserved_when_write_read = False
  54. # when writing then reading creates an identical neo object
  55. read_and_write_is_bijective = True
  56. # allow environment to tell avoid using network
  57. use_network = can_use_network()
  58. local_test_dir = None
  59. def setUp(self):
  60. '''
  61. Set up the test fixture. This is run for every test
  62. '''
  63. self.files_to_test = copy(self.__class__.files_to_test)
  64. self.higher = self.ioclass.supported_objects[0]
  65. self.shortname = self.ioclass.__name__.lower().rstrip('io')
  66. # these objects can both be written and read
  67. self.io_readandwrite = list(set(self.ioclass.readable_objects) &
  68. set(self.ioclass.writeable_objects))
  69. # these objects can be either written or read
  70. self.io_readorwrite = list(set(self.ioclass.readable_objects) |
  71. set(self.ioclass.writeable_objects))
  72. self.create_local_dir_if_not_exists()
  73. self.download_test_files_if_not_present()
  74. self.files_generated = []
  75. self.generate_files_for_io_able_to_write()
  76. self.files_to_test.extend(self.files_generated)
  77. def create_local_dir_if_not_exists(self):
  78. '''
  79. Create a local directory to store testing files and return it.
  80. The directory path is also written to self.local_test_dir
  81. '''
  82. self.local_test_dir = create_local_temp_dir(
  83. self.shortname, directory=os.environ.get("NEO_TEST_FILE_DIR", None))
  84. return self.local_test_dir
  85. def download_test_files_if_not_present(self):
  86. '''
  87. Download %s file at G-node for testing
  88. url_for_tests is global at beginning of this file.
  89. ''' % self.ioclass.__name__
  90. if not self.use_network:
  91. raise unittest.SkipTest("Requires download of data from the web")
  92. url = url_for_tests + self.shortname
  93. try:
  94. make_all_directories(self.files_to_download, self.local_test_dir)
  95. download_test_file(self.files_to_download,
  96. self.local_test_dir, url)
  97. except OSError as exc:
  98. raise unittest.TestCase.failureException(exc)
  99. download_test_files_if_not_present.__test__ = False
  100. def cleanup_file(self, path):
  101. '''
  102. Remove test files or directories safely.
  103. '''
  104. cleanup_test_file(self.ioclass, path, directory=self.local_test_dir)
  105. def able_to_write_or_read(self, writeread=False, readwrite=False):
  106. '''
  107. Return True if generalized writing or reading is possible.
  108. If writeread=True, return True if writing then reading is
  109. possible and produces identical neo objects.
  110. If readwrite=True, return True if reading then writing is possible
  111. and produces files with identical hashes.
  112. '''
  113. # Find the highest object that is supported by the IO
  114. # Test only if it is a Block or Segment, and if it can both read
  115. # and write this object.
  116. if self.higher not in self.io_readandwrite:
  117. return False
  118. if self.higher not in [Block, Segment]:
  119. return False
  120. # when io need external knowldge for writting or read such as
  121. # sampling_rate (RawBinaryIO...) the test is too much complex to design
  122. # genericaly.
  123. if (self.higher in self.ioclass.read_params and
  124. len(self.ioclass.read_params[self.higher]) != 0):
  125. return False
  126. # handle cases where the test should write then read
  127. if writeread and not self.read_and_write_is_bijective:
  128. return False
  129. # handle cases where the test should read then write
  130. if readwrite and not self.hash_conserved_when_write_read:
  131. return False
  132. return True
  133. def get_filename_path(self, filename):
  134. '''
  135. Get the path to a filename in the current temporary file directory
  136. '''
  137. return os.path.join(self.local_test_dir, filename)
  138. def generic_io_object(self, filename=None, return_path=False, clean=False):
  139. '''
  140. Create an io object in a generic way that can work with both
  141. file-based and directory-based io objects.
  142. If filename is None, create a filename (default).
  143. If return_path is True, return the full path of the file along with
  144. the io object. return ioobj, path. Default is False.
  145. If clean is True, try to delete existing versions of the file
  146. before creating the io object. Default is False.
  147. '''
  148. return create_generic_io_object(ioclass=self.ioclass,
  149. filename=filename,
  150. directory=self.local_test_dir,
  151. return_path=return_path,
  152. clean=clean)
  153. def create_file_reader(self, filename=None, return_path=False,
  154. clean=False, target=None, readall=False):
  155. '''
  156. Create a function that can read from the specified filename.
  157. If filename is None, create a filename (default).
  158. If return_path is True, return the full path of the file along with
  159. the reader function. return reader, path. Default is False.
  160. If clean is True, try to delete existing versions of the file
  161. before creating the io object. Default is False.
  162. If target is None, use the first supported_objects from ioobj
  163. If target is False, use the 'read' method.
  164. If target is the Block or Segment class, use read_block or
  165. read_segment, respectively.
  166. If target is a string, use 'read_'+target.
  167. If readall is True, use the read_all_ method instead of the read_
  168. method. Default is False.
  169. '''
  170. ioobj, path = self.generic_io_object(filename=filename,
  171. return_path=True, clean=clean)
  172. res = create_generic_reader(ioobj, target=target, readall=readall)
  173. if return_path:
  174. return res, path
  175. return res
  176. def create_file_writer(self, filename=None, return_path=False,
  177. clean=False, target=None):
  178. '''
  179. Create a function that can write from the specified filename.
  180. If filename is None, create a filename (default).
  181. If return_path is True, return the full path of the file along with
  182. the writer function. return writer, path. Default is False.
  183. If clean is True, try to delete existing versions of the file
  184. before creating the io object. Default is False.
  185. If target is None, use the first supported_objects from ioobj
  186. If target is False, use the 'write' method.
  187. If target is the Block or Segment class, use write_block or
  188. write_segment, respectively.
  189. If target is a string, use 'write_'+target.
  190. '''
  191. ioobj, path = self.generic_io_object(filename=filename,
  192. return_path=True, clean=clean)
  193. res = create_generic_writer(ioobj, target=target)
  194. if return_path:
  195. return res, path
  196. return res
  197. def read_file(self, filename=None, return_path=False, clean=False,
  198. target=None, readall=False, lazy=False):
  199. '''
  200. Read from the specified filename.
  201. If filename is None, create a filename (default).
  202. If return_path is True, return the full path of the file along with
  203. the object. return obj, path. Default is False.
  204. If clean is True, try to delete existing versions of the file
  205. before creating the io object. Default is False.
  206. If target is None, use the first supported_objects from ioobj
  207. If target is False, use the 'read' method.
  208. If target is the Block or Segment class, use read_block or
  209. read_segment, respectively.
  210. If target is a string, use 'read_'+target.
  211. The lazy parameter is passed to the reader. Defaults is True.
  212. If readall is True, use the read_all_ method instead of the read_
  213. method. Default is False.
  214. '''
  215. ioobj, path = self.generic_io_object(filename=filename,
  216. return_path=True, clean=clean)
  217. obj = read_generic(ioobj, target=target, lazy=lazy,
  218. readall=readall, return_reader=False)
  219. if return_path:
  220. return obj, path
  221. return obj
  222. def write_file(self, obj=None, filename=None, return_path=False,
  223. clean=False, target=None):
  224. '''
  225. Write the target object to a file using the given neo io object ioobj.
  226. If filename is None, create a filename (default).
  227. If return_path is True, return the full path of the file along with
  228. the object. return obj, path. Default is False.
  229. If clean is True, try to delete existing versions of the file
  230. before creating the io object. Default is False.
  231. If target is None, use the first supported_objects from ioobj
  232. If target is False, use the 'read' method.
  233. If target is the Block or Segment class, use read_block or
  234. read_segment, respectively.
  235. If target is a string, use 'read_'+target.
  236. obj is the object to write. If obj is None, an object is created
  237. automatically for the io class.
  238. '''
  239. ioobj, path = self.generic_io_object(filename=filename,
  240. return_path=True, clean=clean)
  241. obj = write_generic(ioobj, target=target, return_reader=False)
  242. if return_path:
  243. return obj, path
  244. return obj
  245. def iter_io_objects(self, return_path=False, clean=False):
  246. '''
  247. Return an iterable over the io objects created from files_to_test
  248. If return_path is True, yield the full path of the file along with
  249. the io object. yield ioobj, path Default is False.
  250. If clean is True, try to delete existing versions of the file
  251. before creating the io object. Default is False.
  252. '''
  253. return iter_generic_io_objects(ioclass=self.ioclass,
  254. filenames=self.files_to_test,
  255. directory=self.local_test_dir,
  256. return_path=return_path,
  257. clean=clean)
  258. def iter_readers(self, target=None, readall=False,
  259. return_path=False, return_ioobj=False, clean=False):
  260. '''
  261. Return an iterable over readers created from files_to_test.
  262. If return_path is True, return the full path of the file along with
  263. the reader object. return reader, path.
  264. If return_ioobj is True, return the io object as well as the reader.
  265. return reader, ioobj. Default is False.
  266. If both return_path and return_ioobj is True,
  267. return reader, path, ioobj. Default is False.
  268. If clean is True, try to delete existing versions of the file
  269. before creating the io object. Default is False.
  270. If readall is True, use the read_all_ method instead of the
  271. read_ method. Default is False.
  272. '''
  273. return iter_generic_readers(ioclass=self.ioclass,
  274. filenames=self.files_to_test,
  275. directory=self.local_test_dir,
  276. return_path=return_path,
  277. return_ioobj=return_ioobj,
  278. target=target,
  279. clean=clean,
  280. readall=readall)
  281. def iter_objects(self, target=None, return_path=False, return_ioobj=False,
  282. return_reader=False, clean=False, readall=False,
  283. lazy=False):
  284. '''
  285. Iterate over objects read from the list of filenames in files_to_test.
  286. If target is None, use the first supported_objects from ioobj
  287. If target is False, use the 'read' method.
  288. If target is the Block or Segment class, use read_block or
  289. read_segment, respectively.
  290. If target is a string, use 'read_'+target.
  291. If return_path is True, yield the full path of the file along with
  292. the object. yield obj, path.
  293. If return_ioobj is True, yield the io object as well as the object.
  294. yield obj, ioobj. Default is False.
  295. If return_reader is True, yield the io reader function as well as the
  296. object. yield obj, reader. Default is False.
  297. If some combination of return_path, return_ioobj, and return_reader
  298. is True, they are yielded in the order: obj, path, ioobj, reader.
  299. If clean is True, try to delete existing versions of the file
  300. before creating the io object. Default is False.
  301. The lazy parameters is passed to the reader. Defaults is True.
  302. If readall is True, use the read_all_ method instead of the read_
  303. method. Default is False.
  304. '''
  305. return iter_read_objects(ioclass=self.ioclass,
  306. filenames=self.files_to_test,
  307. directory=self.local_test_dir,
  308. target=target,
  309. return_path=return_path,
  310. return_ioobj=return_ioobj,
  311. return_reader=return_reader,
  312. clean=clean, readall=readall,
  313. lazy=lazy)
  314. def generate_files_for_io_able_to_write(self):
  315. '''
  316. Write files for use in testing.
  317. '''
  318. self.files_generated = []
  319. if not self.able_to_write_or_read():
  320. return
  321. generate_from_supported_objects(self.ioclass.supported_objects)
  322. ioobj, path = self.generic_io_object(return_path=True, clean=True)
  323. if ioobj is None:
  324. return
  325. self.files_generated.append(path)
  326. write_generic(ioobj, target=self.higher)
  327. close_object_safe(ioobj)
  328. def test_write_then_read(self):
  329. '''
  330. Test for IO that are able to write and read - here %s:
  331. 1 - Generate a full schema with supported objects.
  332. 2 - Write to a file
  333. 3 - Read from the file
  334. 4 - Check the hierachy
  335. 5 - Check data
  336. Work only for IO for Block and Segment for the highest object
  337. (main cases).
  338. ''' % self.ioclass.__name__
  339. if not self.able_to_write_or_read(writeread=True):
  340. return
  341. ioobj1 = self.generic_io_object(clean=True)
  342. if ioobj1 is None:
  343. return
  344. ob1 = write_generic(ioobj1, target=self.higher)
  345. close_object_safe(ioobj1)
  346. ioobj2 = self.generic_io_object()
  347. # Read the highest supported object from the file
  348. obj_reader = create_generic_reader(ioobj2, target=False)
  349. ob2 = obj_reader()[0]
  350. if self.higher == Segment:
  351. ob2 = ob2.segments[0]
  352. # some formats (e.g. elphy) do not support double floating
  353. # point spiketrains
  354. try:
  355. assert_same_sub_schema(ob1, ob2, True, 1e-8)
  356. assert_neo_object_is_compliant(ob1)
  357. assert_neo_object_is_compliant(ob2)
  358. # intercept exceptions and add more information
  359. except BaseException as exc:
  360. raise
  361. close_object_safe(ioobj2)
  362. def test_read_then_write(self):
  363. '''
  364. Test for IO that are able to read and write, here %s:
  365. 1 - Read a file
  366. 2 Write object set in another file
  367. 3 Compare the 2 files hash
  368. NOTE: TODO: Not implemented yet
  369. ''' % self.ioclass.__name__
  370. if not self.able_to_write_or_read(readwrite=True):
  371. return
  372. # assert_file_contents_equal(a, b)
  373. def test_assert_readed_neo_object_is_compliant(self):
  374. '''
  375. Reading %s files in `files_to_test` produces compliant objects.
  376. Compliance test: neo.test.tools.assert_neo_object_is_compliant for
  377. lazy mode.
  378. ''' % self.ioclass.__name__
  379. for obj, path in self.iter_objects(lazy=False, return_path=True):
  380. try:
  381. # Check compliance of the block
  382. assert_neo_object_is_compliant(obj)
  383. # intercept exceptions and add more information
  384. except BaseException as exc:
  385. exc.args += ('from %s' % os.path.basename(path), )
  386. raise
  387. def test_readed_with_lazy_is_compliant(self):
  388. '''
  389. Reading %s files in `files_to_test` with `lazy` is compliant.
  390. Test the reader with lazy = True.
  391. The schema must contain proxy objects.
  392. ''' % self.ioclass.__name__
  393. # This is for files presents at G-Node or generated
  394. if self.ioclass.support_lazy:
  395. for obj, path in self.iter_objects(lazy=True, return_path=True):
  396. try:
  397. assert_sub_schema_is_lazy_loaded(obj)
  398. # intercept exceptions and add more information
  399. except BaseException as exc:
  400. raise