common_io_test.py 22 KB

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