diff_tests.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. # Copyright (C) 2011 Internet Systems Consortium.
  2. #
  3. # Permission to use, copy, modify, and distribute this software for any
  4. # purpose with or without fee is hereby granted, provided that the above
  5. # copyright notice and this permission notice appear in all copies.
  6. #
  7. # THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
  8. # DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
  9. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
  10. # INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
  11. # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
  12. # FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
  13. # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
  14. # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. import isc.log
  16. import unittest
  17. from isc.dns import Name, RRset, RRClass, RRType, RRTTL, Rdata
  18. from isc.xfrin.diff import Diff, NoSuchZone
  19. class TestError(Exception):
  20. """
  21. Just to have something to be raised during the tests.
  22. Not used outside.
  23. """
  24. pass
  25. class DiffTest(unittest.TestCase):
  26. """
  27. Tests for the isc.xfrin.diff.Diff class.
  28. It also plays role of a data source and an updater, so it can manipulate
  29. some test variables while being called.
  30. """
  31. def setUp(self):
  32. """
  33. This sets internal variables so we can see nothing was called yet.
  34. It also creates some variables used in multiple tests.
  35. """
  36. # Track what was called already
  37. self.__updater_requested = False
  38. self.__compact_called = False
  39. self.__data_operations = []
  40. self.__apply_called = False
  41. self.__commit_called = False
  42. self.__broken_called = False
  43. # Some common values
  44. self.__rrclass = RRClass.IN()
  45. self.__type = RRType.A()
  46. self.__ttl = RRTTL(3600)
  47. # And RRsets
  48. # Create two valid rrsets
  49. self.__rrset1 = RRset(Name('a.example.org.'), self.__rrclass,
  50. self.__type, self.__ttl)
  51. self.__rdata = Rdata(self.__type, self.__rrclass, '192.0.2.1')
  52. self.__rrset1.add_rdata(self.__rdata)
  53. self.__rrset2 = RRset(Name('b.example.org.'), self.__rrclass,
  54. self.__type, self.__ttl)
  55. self.__rrset2.add_rdata(self.__rdata)
  56. # And two invalid
  57. self.__rrset_empty = RRset(Name('empty.example.org.'), self.__rrclass,
  58. self.__type, self.__ttl)
  59. self.__rrset_multi = RRset(Name('multi.example.org.'), self.__rrclass,
  60. self.__type, self.__ttl)
  61. self.__rrset_multi.add_rdata(self.__rdata)
  62. self.__rrset_multi.add_rdata(Rdata(self.__type, self.__rrclass,
  63. '192.0.2.2'))
  64. def __mock_compact(self):
  65. """
  66. This can be put into the diff to hook into its compact method and see
  67. if it gets called.
  68. """
  69. self.__compact_called = True
  70. def __mock_apply(self):
  71. """
  72. This can be put into the diff to hook into its apply method and see
  73. it gets called.
  74. """
  75. self.__apply_called = True
  76. def __broken_operation(self, *args):
  77. """
  78. This can be used whenever an operation should fail. It raises TestError.
  79. It should take whatever amount of parameters needed, so it can be put
  80. quite anywhere.
  81. """
  82. self.__broken_called = True
  83. raise TestError("Test error")
  84. def commit(self):
  85. """
  86. This is part of pretending to be a zone updater. This notes the commit
  87. was called.
  88. """
  89. self.__commit_called = True
  90. def add_rrset(self, rrset):
  91. """
  92. This one is part of pretending to be a zone updater. It writes down
  93. addition of an rrset was requested.
  94. """
  95. self.__data_operations.append(('add', rrset))
  96. def remove_rrset(self, rrset):
  97. """
  98. This one is part of pretending to be a zone updater. It writes down
  99. removal of an rrset was requested.
  100. """
  101. self.__data_operations.append(('remove', rrset))
  102. def get_class(self):
  103. """
  104. This one is part of pretending to be a zone updater. It returns
  105. the IN class.
  106. """
  107. return self.__rrclass
  108. def get_updater(self, zone_name, replace):
  109. """
  110. This one pretends this is the data source client and serves
  111. getting an updater.
  112. If zone_name is 'none.example.org.', it returns None, otherwise
  113. it returns self.
  114. """
  115. # The diff should not delete the old data.
  116. self.assertFalse(replace)
  117. self.__updater_requested = True
  118. # Pretend this zone doesn't exist
  119. if zone_name == Name('none.example.org.'):
  120. return None
  121. else:
  122. return self
  123. def test_create(self):
  124. """
  125. This test the case when the diff is successfuly created. It just
  126. tries it does not throw and gets the updater.
  127. """
  128. diff = Diff(self, Name('example.org.'))
  129. self.assertTrue(self.__updater_requested)
  130. self.assertEqual([], diff.get_buffer())
  131. def test_create_nonexist(self):
  132. """
  133. Try to create a diff on a zone that doesn't exist. This should
  134. raise a correct exception.
  135. """
  136. self.assertRaises(NoSuchZone, Diff, self, Name('none.example.org.'))
  137. self.assertTrue(self.__updater_requested)
  138. def __data_common(self, diff, method, name):
  139. """
  140. Common part of test for test_add and test_remove.
  141. """
  142. # Try putting there the bad data first
  143. self.assertRaises(ValueError, method, self.__rrset_empty)
  144. self.assertRaises(ValueError, method, self.__rrset_multi)
  145. # They were not added
  146. self.assertEqual([], diff.get_buffer())
  147. # Add some proper data
  148. method(self.__rrset1)
  149. method(self.__rrset2)
  150. dlist = [(name, self.__rrset1), (name, self.__rrset2)]
  151. self.assertEqual(dlist, diff.get_buffer())
  152. # Check the data are not destroyed by raising an exception because of
  153. # bad data
  154. self.assertRaises(ValueError, method, self.__rrset_empty)
  155. self.assertEqual(dlist, diff.get_buffer())
  156. def test_add(self):
  157. """
  158. Try to add few items into the diff and see they are stored in there.
  159. Also try passing an rrset that has differnt amount of RRs than 1.
  160. """
  161. diff = Diff(self, Name('example.org.'))
  162. self.__data_common(diff, diff.add_data, 'add')
  163. def test_remove(self):
  164. """
  165. Try scheduling removal of few items into the diff and see they are
  166. stored in there.
  167. Also try passing an rrset that has different amount of RRs than 1.
  168. """
  169. diff = Diff(self, Name('example.org.'))
  170. self.__data_common(diff, diff.remove_data, 'remove')
  171. def test_apply(self):
  172. """
  173. Schedule few additions and check the apply works by passing the
  174. data into the updater.
  175. """
  176. # Prepare the diff
  177. diff = Diff(self, Name('example.org.'))
  178. diff.add_data(self.__rrset1)
  179. diff.remove_data(self.__rrset2)
  180. dlist = [('add', self.__rrset1), ('remove', self.__rrset2)]
  181. self.assertEqual(dlist, diff.get_buffer())
  182. # Do the apply, hook the compact method
  183. diff.compact = self.__mock_compact
  184. diff.apply()
  185. # It should call the compact
  186. self.assertTrue(self.__compact_called)
  187. # And pass the data. Our local history of what happened is the same
  188. # format, so we can check the same way
  189. self.assertEqual(dlist, self.__data_operations)
  190. # And the buffer in diff should become empty, as everything
  191. # got inside.
  192. self.assertEqual([], diff.get_buffer())
  193. def test_commit(self):
  194. """
  195. If we call a commit, it should first apply whatever changes are
  196. left (we hook into that instead of checking the effect) and then
  197. the commit on the updater should have been called.
  198. Then we check it raises value error for whatever operation we try.
  199. """
  200. diff = Diff(self, Name('example.org.'))
  201. diff.add_data(self.__rrset1)
  202. orig_apply = diff.apply
  203. diff.apply = self.__mock_apply
  204. diff.commit()
  205. self.assertTrue(self.__apply_called)
  206. self.assertTrue(self.__commit_called)
  207. # The data should be handled by apply which we replaced.
  208. self.assertEqual([], self.__data_operations)
  209. # Now check all range of other methods raise ValueError
  210. self.assertRaises(ValueError, diff.commit)
  211. self.assertRaises(ValueError, diff.add_data, self.__rrset2)
  212. self.assertRaises(ValueError, diff.remove_data, self.__rrset1)
  213. diff.apply = orig_apply
  214. self.assertRaises(ValueError, diff.apply)
  215. # This one does not state it should raise, so check it doesn't
  216. # But it is NOP in this situation anyway
  217. diff.compact()
  218. def test_autoapply(self):
  219. """
  220. Test the apply is called all by itself after 100 tasks are added.
  221. """
  222. diff = Diff(self, Name('example.org.'))
  223. # A method to check the apply is called _after_ the 100th element
  224. # is added. We don't use it anywhere else, so we define it locally
  225. # as lambda function
  226. def check():
  227. self.assertEqual(100, len(diff.get_buffer()))
  228. self.__mock_apply()
  229. orig_apply = diff.apply
  230. diff.apply = check
  231. # If we put 99, nothing happens yet
  232. for i in range(0, 99):
  233. diff.add_data(self.__rrset1)
  234. expected = [('add', self.__rrset1)] * 99
  235. self.assertEqual(expected, diff.get_buffer())
  236. self.assertFalse(self.__apply_called)
  237. # Now we push the 100th and it should call the apply method
  238. # This will _not_ flush the data yet, as we replaced the method.
  239. # It, however, would in the real life.
  240. diff.add_data(self.__rrset1)
  241. # Now the apply method (which is replaced by our check) should
  242. # have been called. If it wasn't, this is false. If it was, but
  243. # still with 99 elements, the check would complain
  244. self.assertTrue(self.__apply_called)
  245. # Reset the buffer by calling the original apply.
  246. orig_apply()
  247. self.assertEqual([], diff.get_buffer())
  248. # Similar with remove
  249. self.__apply_called = False
  250. for i in range(0, 99):
  251. diff.remove_data(self.__rrset2)
  252. expected = [('remove', self.__rrset2)] * 99
  253. self.assertEqual(expected, diff.get_buffer())
  254. self.assertFalse(self.__apply_called)
  255. diff.remove_data(self.__rrset2)
  256. self.assertTrue(self.__apply_called)
  257. def test_compact(self):
  258. """
  259. Test the compaction works as expected, eg. it compacts only consecutive
  260. changes of the same operation and on the same domain/type.
  261. The test case checks that it does merge them, but also puts some
  262. different operations "in the middle", changes the type and name and
  263. places the same kind of change further away of each other to see they
  264. are not merged in that case.
  265. """
  266. diff = Diff(self, Name('example.org.'))
  267. # Check we can do a compact on empty data, it shouldn't break
  268. diff.compact()
  269. self.assertEqual([], diff.get_buffer())
  270. # This data is the way it should look like after the compact
  271. # ('operation', 'domain.prefix', 'type', ['rdata', 'rdata'])
  272. # The notes say why the each of consecutive can't be merged
  273. data = [
  274. ('add', 'a', 'A', ['192.0.2.1', '192.0.2.2']),
  275. # Different type.
  276. ('add', 'a', 'AAAA', ['2001:db8::1', '2001:db8::2']),
  277. # Different operation
  278. ('remove', 'a', 'AAAA', ['2001:db8::3']),
  279. # Different domain
  280. ('remove', 'b', 'AAAA', ['2001:db8::4']),
  281. # This does not get merged with the first, even if logically
  282. # possible. We just don't do this.
  283. ('add', 'a', 'A', ['192.0.2.3'])
  284. ]
  285. # Now, fill the data into the diff, in a "flat" way, one by one
  286. for (op, nprefix, rrtype, rdata) in data:
  287. name = Name(nprefix + '.example.org.')
  288. rrtype_obj = RRType(rrtype)
  289. for rdatum in rdata:
  290. rrset = RRset(name, self.__rrclass, rrtype_obj, self.__ttl)
  291. rrset.add_rdata(Rdata(rrtype_obj, self.__rrclass, rdatum))
  292. if op == 'add':
  293. diff.add_data(rrset)
  294. else:
  295. diff.remove_data(rrset)
  296. # Compact it
  297. diff.compact()
  298. # Now check they got compacted. They should be in the same order as
  299. # pushed inside. So it should be the same as data modulo being in
  300. # the rrsets and isc.dns objects.
  301. def check():
  302. buf = diff.get_buffer()
  303. self.assertEqual(len(data), len(buf))
  304. for (expected, received) in zip(data, buf):
  305. (eop, ename, etype, edata) = expected
  306. (rop, rrrset) = received
  307. self.assertEqual(eop, rop)
  308. ename_obj = Name(ename + '.example.org.')
  309. self.assertEqual(ename_obj, rrrset.get_name())
  310. # We check on names to make sure they are printed nicely
  311. self.assertEqual(etype, str(rrrset.get_type()))
  312. rdata = rrrset.get_rdata()
  313. self.assertEqual(len(edata), len(rdata))
  314. # It should also preserve the order
  315. for (edatum, rdatum) in zip(edata, rdata):
  316. self.assertEqual(edatum, str(rdatum))
  317. check()
  318. # Try another compact does nothing, but survives
  319. diff.compact()
  320. check()
  321. def test_wrong_class(self):
  322. """
  323. Test a wrong class of rrset is rejected.
  324. """
  325. diff = Diff(self, Name('example.org.'))
  326. rrset = RRset(Name('a.example.org.'), RRClass.CH(), RRType.NS(),
  327. self.__ttl)
  328. rrset.add_rdata(Rdata(RRType.NS(), RRClass.CH(), 'ns.example.org.'))
  329. self.assertRaises(ValueError, diff.add_data, rrset)
  330. self.assertRaises(ValueError, diff.remove_data, rrset)
  331. def __do_raise_test(self):
  332. """
  333. Do a raise test. Expects that one of the operations is exchanged for
  334. broken version.
  335. """
  336. diff = Diff(self, Name('example.org.'))
  337. diff.add_data(self.__rrset1)
  338. diff.remove_data(self.__rrset2)
  339. self.assertRaises(TestError, diff.commit)
  340. self.assertTrue(self.__broken_called)
  341. self.assertRaises(ValueError, diff.add_data, self.__rrset1)
  342. self.assertRaises(ValueError, diff.remove_data, self.__rrset2)
  343. self.assertRaises(ValueError, diff.commit)
  344. self.assertRaises(ValueError, diff.apply)
  345. def test_raise_add(self):
  346. """
  347. Test the exception from add_rrset is propagated and the diff can't be
  348. used afterwards.
  349. """
  350. self.add_rrset = self.__broken_operation
  351. self.__do_raise_test()
  352. def test_raise_remove(self):
  353. """
  354. Test the exception from remove_rrset is propagated and the diff can't be
  355. used afterwards.
  356. """
  357. self.remove_rrset = self.__broken_operation
  358. self.__do_raise_test()
  359. def test_raise_commit(self):
  360. """
  361. Test the exception from updater's commit gets propagated and it can't be
  362. used afterwards.
  363. """
  364. self.commit = self.__broken_operation
  365. self.__do_raise_test()
  366. if __name__ == "__main__":
  367. isc.log.init("bind10")
  368. unittest.main()