PageRenderTime 72ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/test/functional/storage.rb

http://github.com/jmettraux/ruote
Ruby | 729 lines | 385 code | 216 blank | 128 comment | 9 complexity | 089397ee21fbe6c106c2235901ef137c MD5 | raw file
  1. #
  2. # testing ruote
  3. #
  4. # Mon Dec 14 15:03:13 JST 2009
  5. #
  6. require File.expand_path('../../test_helper', __FILE__)
  7. require_json
  8. require File.expand_path('../../functional/storage_helper', __FILE__)
  9. require File.expand_path('../../functional/signals', __FILE__)
  10. require 'ruote'
  11. # Please note:
  12. # Operations return something trueish when they fail and nil
  13. # when they succeed.
  14. #
  15. # The pattern is: when it fails because the document passed as argument is
  16. # outdated, you will receive the current version of the document (trueish),
  17. # when it fails because the document is gone (deleted meanwhile), you will
  18. # receive true (which is obviously trueish).
  19. class FtStorage < Test::Unit::TestCase
  20. #
  21. # test preparation
  22. def setup
  23. @s = determine_storage({})
  24. @s = @s.storage if @s.respond_to?(:storage)
  25. %w[ errors expressions msgs workitems ].each do |t|
  26. @s.purge_type!(t)
  27. end
  28. end
  29. def teardown
  30. return unless @s
  31. @s.purge!
  32. @s.shutdown
  33. end
  34. #
  35. # helpers
  36. def put_toto_doc
  37. @s.put('_id' => 'toto', 'type' => 'errors', 'message' => 'testing')
  38. end
  39. def get_toto_doc
  40. @s.get('errors', 'toto')
  41. end
  42. #
  43. # the tests
  44. # === put
  45. # When successful, #put returns nil.
  46. #
  47. def test_put
  48. doc = { '_id' => 'toto', 'type' => 'errors', 'message' => 'testing' }
  49. r = @s.put(doc)
  50. assert_nil r
  51. assert_nil doc['_rev']
  52. assert_nil doc['put_at']
  53. doc = @s.get('errors', 'toto')
  54. assert_not_nil doc['_rev']
  55. assert_not_nil doc['put_at']
  56. assert_equal 'testing', doc['message']
  57. end
  58. # When a document with the same _id and type already existent, the put
  59. # doesn't happen and #put returns that already existing document.
  60. #
  61. def test_put_when_already_existent
  62. put_toto_doc
  63. doc = { '_id' => 'toto', 'type' => 'errors', 'message' => 'two' }
  64. r = @s.put(doc)
  65. assert_match /Hash$/, r.class.name
  66. assert_not_nil r['_rev']
  67. assert_not_nil r['put_at']
  68. assert_nil doc['_rev']
  69. assert_nil doc['put_at']
  70. end
  71. # A successful reput (_id/type/_rev do match) returns nil.
  72. #
  73. def test_reput
  74. put_toto_doc
  75. d0 = get_toto_doc
  76. d0['message'] = 'test_reput'
  77. r = @s.put(d0)
  78. assert_nil r
  79. d1 = get_toto_doc
  80. assert_not_equal d1['_rev'], d0['_rev']
  81. assert d1['put_at'] >= d0['put_at']
  82. assert_equal 'test_reput', d1['message']
  83. end
  84. # A reput with the wrong rev (our document is outdated probably) will
  85. # not happen and #put will return the current (newest probably) document.
  86. #
  87. def test_reput_fail_wrong_rev
  88. put_toto_doc
  89. d1 = get_toto_doc
  90. rev = d1['_rev']
  91. @s.put(d1.merge('message' => 'x'))
  92. d2 = get_toto_doc
  93. r = @s.put(d2.merge('_rev' => rev, 'message' => 'y'))
  94. assert_not_nil r
  95. assert_not_equal d1['_rev'], d2['_rev']
  96. assert_equal 'x', d2['message']
  97. end
  98. # Attempting to put a document that is gone (got deleted meanwhile) will
  99. # return true.
  100. #
  101. def test_reput_fail_gone
  102. put_toto_doc
  103. doc = get_toto_doc
  104. @s.delete(doc)
  105. r = @s.put(doc)
  106. assert_equal true, r
  107. end
  108. # Attempting to put a document with a _rev directly will raise an
  109. # ArgumentError.
  110. #
  111. def test_put_doc_with_rev
  112. put_toto_doc; doc = get_toto_doc
  113. # just to get a valid _rev
  114. r = @s.put(
  115. '_id' => 'doc_with_rev', 'type' => 'errors', '_rev' => doc['_rev'])
  116. assert_equal true, r
  117. end
  118. # #put takes an optional :update_rev. When set to true and the put
  119. # succeeds, the _rev and the put_at of the [local] document are set/updated.
  120. #
  121. def test_put_update_rev_new_document
  122. doc = { '_id' => 'urev', 'type' => 'errors' }
  123. r = @s.put(doc, :update_rev => true)
  124. assert_nil r
  125. assert_not_nil doc['_rev']
  126. assert_not_nil doc['put_at']
  127. end
  128. # When putting a document with the :update_rev option set, the just put
  129. # document will get the new _rev (and the put_at)
  130. #
  131. def test_put_update_rev_existing_document
  132. put_toto_doc; doc = get_toto_doc
  133. initial_rev = doc['_rev']
  134. initial_put_at = doc['put_at']
  135. r = @s.put(doc, :update_rev => true)
  136. assert_nil r
  137. assert_not_nil initial_rev
  138. assert_not_nil initial_put_at
  139. assert_not_nil doc['_rev']
  140. assert_not_equal doc['_rev'], initial_rev
  141. assert_not_nil doc['put_at']
  142. assert doc['put_at'] >= initial_put_at
  143. end
  144. # put_at and _rev should not repeat
  145. #
  146. def test_put_sequence
  147. revs = []
  148. doc = { '_id' => 'putseq', 'type' => 'errors' }
  149. 77.times do |i|
  150. r = @s.put(doc)
  151. doc = @s.get('errors', 'putseq')
  152. revs << doc['_rev']
  153. assert_nil r
  154. assert_not_nil doc['put_at']
  155. assert_equal i + 1, revs.uniq.size
  156. end
  157. end
  158. # Be lenient with the input (accept symbols, but turn them into strings).
  159. #
  160. def test_put_turns_symbols_into_strings
  161. r = @s.put('_id' => 'skeys', 'type' => 'errors', :a => :b)
  162. assert_nil r
  163. doc = @s.get('errors', 'skeys')
  164. return if doc.class != Hash
  165. # MongoDB uses BSON::OrderedHash which is happy with symbols...
  166. assert_equal 'b', doc['a']
  167. end
  168. # === get
  169. # Getting a non-existent document returns nil.
  170. #
  171. def test_get_non_existent
  172. assert_nil @s.get('errors', 'nemo')
  173. end
  174. # Getting a document returns it (well the most up-to-date revision of it).
  175. #
  176. def test_get
  177. put_toto_doc
  178. doc = @s.get('errors', 'toto')
  179. doc.delete('_rev')
  180. doc.delete('put_at')
  181. doc.delete('_wfid') # ruote-mon
  182. assert_equal(
  183. { '_id' => 'toto', 'type' => 'errors', 'message' => 'testing' },
  184. doc)
  185. end
  186. # === delete
  187. # When successful, #delete returns nil (like the other methods...).
  188. #
  189. def test_delete
  190. put_toto_doc
  191. doc = @s.get('errors', 'toto')
  192. r = @s.delete(doc)
  193. assert_equal nil, r
  194. doc = @s.get('errors', 'toto')
  195. assert_equal nil, doc
  196. end
  197. # When attempting to delete a document and that document argument has no
  198. # _rev, it will raise an ArgumentError.
  199. #
  200. def test_delete_document_without_rev
  201. assert_raise(
  202. ArgumentError, "can't delete doc without _rev"
  203. ) do
  204. @s.delete('_id' => 'without_rev', 'type' => 'errors')
  205. end
  206. end
  207. # Deleting a document that doesn't exist returns true.
  208. #
  209. def test_delete_non_existent_document
  210. put_toto_doc; doc = get_toto_doc
  211. # just to get a valid _rev
  212. r = @s.delete('_id' => 'ned', 'type' => 'errors', '_rev' => doc['_rev'])
  213. assert_equal true, r
  214. end
  215. # Deleting a document that is gone (got deleted meanwhile) returns true.
  216. #
  217. def test_delete_gone_document
  218. put_toto_doc
  219. doc = get_toto_doc
  220. @s.delete(doc)
  221. r = @s.delete(doc)
  222. assert_equal true, r
  223. end
  224. # === get_many
  225. # Get many documents at once, use a string or regex key, or not.
  226. #
  227. def test_get_many
  228. load_30_errors
  229. assert_equal 30, @s.get_many('errors').size
  230. assert_equal 0, @s.get_many('errors', '7').size
  231. assert_equal 1, @s.get_many('errors', '07').size
  232. assert_equal 1, @s.get_many('errors', /!07$/).size
  233. assert_equal 30, @s.get_many('errors', /^yy!/).size
  234. assert_equal 30, @s.get_many('errors', /y/).size
  235. assert_equal 'yy!07', @s.get_many('errors', '07').first['_id']
  236. assert_equal 'yy!07', @s.get_many('errors', /!07/).first['_id']
  237. end
  238. # Get many documents at once, use an array of string or regex keys.
  239. #
  240. def test_get_many_array_of_keys
  241. load_30_errors
  242. assert_equal 30, @s.get_many('errors').size
  243. assert_equal 2, @s.get_many('errors', [ '07', '08' ]).size
  244. assert_equal 2, @s.get_many('errors', [ /!07$/, /!08$/ ]).size
  245. assert_equal(
  246. %w[ yy!07 yy!08 ],
  247. @s.get_many('errors', [ '07', '08' ]).collect { |d| d['_id'] }.sort)
  248. assert_equal(
  249. %w[ yy!07 yy!08 ],
  250. @s.get_many('errors', [ /!07$/, /!08$/ ]).collect { |d| d['_id'] }.sort)
  251. end
  252. # Limit the number of documents received.
  253. #
  254. def test_get_many_limit
  255. load_30_errors
  256. assert_equal 10, @s.get_many('errors', nil, :limit => 10).size
  257. end
  258. # Count the documents (in a type).
  259. #
  260. def test_get_many_count
  261. load_30_errors
  262. assert_equal 30, @s.get_many('errors', nil, :count => true)
  263. end
  264. # Paginate documents.
  265. #
  266. def test_get_many_skip_and_limit
  267. load_30_errors
  268. assert_equal(
  269. %w[ yy!01 yy!02 yy!03 yy!04 ],
  270. @s.get_many(
  271. 'errors', nil, :skip => 0, :limit => 4
  272. ).collect { |d| d['_id'] })
  273. assert_equal(
  274. %w[ yy!04 yy!05 yy!06 ],
  275. @s.get_many(
  276. 'errors', nil, :skip => 3, :limit => 3
  277. ).collect { |d| d['_id'] })
  278. end
  279. # Pagination and :descending are not incompatible.
  280. #
  281. def test_get_many_skip_limit_and_reverse
  282. load_30_errors
  283. assert_equal(
  284. %w[ yy!30 yy!29 yy!28 ],
  285. @s.get_many(
  286. 'errors', nil, :skip => 0, :limit => 3, :descending => true
  287. ).collect { |d| d['_id'] })
  288. assert_equal(
  289. %w[ yy!27 yy!26 yy!25 ],
  290. @s.get_many(
  291. 'errors', nil, :skip => 3, :limit => 3, :descending => true
  292. ).collect { |d| d['_id'] })
  293. end
  294. # === purge!
  295. # Purge removes all the documents in the storage.
  296. #
  297. def test_purge
  298. put_toto_doc
  299. assert_equal 1, @s.get_many('errors').size
  300. @s.purge!
  301. assert_equal 0, @s.get_many('errors').size
  302. end
  303. # === reserve
  304. # Making sure that Storage#reserve(msg) returns true once and only once
  305. # for a given msg. Stresses the storage for a while and then checks
  306. # for collisions.
  307. #
  308. def test_reserve
  309. # TODO: eventually return here if the storage being tested has
  310. # no need for a real reserve implementation (ruote-swf for example).
  311. taoe = Thread.abort_on_exception
  312. Thread.abort_on_exception = true
  313. reserved = []
  314. threads = []
  315. threads << Thread.new do
  316. i = 0
  317. loop do
  318. @s.put_msg('launch', 'tree' => i)
  319. i = i + 1
  320. end
  321. end
  322. 2.times do
  323. threads << Thread.new do
  324. loop do
  325. msgs = @s.get_msgs
  326. msgs[0, 100].each do |msg|
  327. next if msg['tree'].nil?
  328. next unless @s.reserve(msg)
  329. if reserved.include?(msg['tree'])
  330. puts "=" * 80
  331. p [ :dbl, :r, msg['_rev'], :t, msg['tree'] ]
  332. end
  333. reserved << msg['tree']
  334. sleep(rand * 0.01)
  335. end
  336. end
  337. end
  338. end
  339. sleep 7
  340. threads.each { |t| t.terminate }
  341. Thread.abort_on_exception = taoe
  342. assert_equal false, reserved.empty?
  343. assert_equal reserved.size, reserved.uniq.size
  344. end
  345. # === ids
  346. # Storage#ids(type) returns all the ids present for a document type, in
  347. # sorted order.
  348. #
  349. def test_ids
  350. ids = load_30_errors
  351. assert_equal ids.sort, @s.ids('errors')
  352. end
  353. # === dump
  354. # #dump returns a string representation of the storage's content. Warning,
  355. # this is a debug/test method.
  356. #
  357. def test_dump
  358. load_30_errors
  359. dump = @s.dump('errors')
  360. assert_match /^[ -] _id: yy!01\n/, dump
  361. assert_match /^[ -] _id: yy!21\n/, dump
  362. end
  363. # === clear
  364. # #clear clears the storage
  365. #
  366. def test_clear
  367. put_toto_doc
  368. assert_equal 1, @s.get_many('errors').size
  369. @s.clear
  370. assert_equal 0, @s.get_many('errors').size
  371. end
  372. # === remove_process
  373. # Put documents for process 0 and process 1, remove_process(1), check that
  374. # only documents for process 0 are remaining.
  375. #
  376. # Don't forget to deal with trackers and schedules.
  377. #
  378. def test_remove_process
  379. @s.purge_type!('errors')
  380. ts = @s.get_trackers
  381. @s.delete(ts) if ts['_rev']
  382. dboard = Ruote::Dashboard.new(Ruote::Worker.new(@s))
  383. dboard.noisy = ENV['NOISY'] == 'true'
  384. dboard.register :human, Ruote::StorageParticipant
  385. pdef = Ruote.define do
  386. concurrence do
  387. wait '1d'
  388. human
  389. listen :to => 'bob'
  390. error 'nada'
  391. end
  392. end
  393. wfid0 = dboard.launch(pdef)
  394. wfid1 = dboard.launch(pdef)
  395. dboard.wait_for('error_intercepted')
  396. dboard.wait_for('error_intercepted')
  397. assert_equal 12, @s.get_many('expressions').size
  398. assert_equal 2, @s.get_many('schedules').size
  399. assert_equal 2, @s.get_many('workitems').size
  400. assert_equal 2, @s.get_many('errors').size
  401. assert_equal 2, @s.get_trackers['trackers'].size
  402. @s.remove_process(wfid0)
  403. assert_equal 6, @s.get_many('expressions').size
  404. assert_equal 1, @s.get_many('schedules').size
  405. assert_equal 1, @s.get_many('workitems').size
  406. assert_equal 1, @s.get_many('errors').size
  407. assert_equal 1, @s.get_trackers['trackers'].size
  408. ensure
  409. dboard.shutdown rescue nil
  410. end
  411. # === configuration
  412. # Simply getting the engine configuration should work.
  413. #
  414. def test_get_configuration
  415. assert_not_nil @s.get_configuration('engine')
  416. end
  417. # The initial configuration passed when initializing the storage overrides
  418. # any previous configuration.
  419. #
  420. def test_override_configuration
  421. determine_storage('house' => 'taira', 'domain' => 'harima')
  422. s = determine_storage('house' => 'minamoto')
  423. assert_equal 'minamoto', s.get_configuration('engine')['house']
  424. assert_equal nil, s.get_configuration('engine')['domain']
  425. end
  426. # Testing the 'preserve_configuration' option for storage initialization.
  427. #
  428. def test_preserve_configuration
  429. return if @s.class == Ruote::HashStorage
  430. # this test makes no sense with an in-memory hash
  431. determine_storage(
  432. 'house' => 'taira')
  433. s = determine_storage(
  434. 'house' => 'minamoto', 'preserve_configuration' => true)
  435. assert_equal 'taira', s.get_configuration('engine')['house']
  436. # if this test is giving a
  437. # "NoMethodError: undefined method `[]' for nil:NilClass"
  438. # for ruote-dm, comment out the auto_upgrade! block in
  439. # ruote-dm/test/functional_connection.rb
  440. end
  441. # === query workitems
  442. # Query by workitem field.
  443. #
  444. def test_by_field
  445. return unless @s.respond_to?(:by_field)
  446. load_workitems
  447. assert_equal 3, @s.by_field('workitems', 'place', 'kyouto').size
  448. assert_equal 1, @s.by_field('workitems', 'place', 'sendai').size
  449. assert_equal(
  450. Ruote::Workitem, @s.by_field('workitems', 'place', 'sendai').first.class)
  451. end
  452. # Query by participant name.
  453. #
  454. def test_by_participant
  455. return unless @s.respond_to?(:by_participant)
  456. load_workitems
  457. assert_equal 2, @s.by_participant('workitems', 'fujiwara', {}).size
  458. assert_equal 1, @s.by_participant('workitems', 'shingen', {}).size
  459. assert_equal(
  460. Ruote::Workitem, @s.by_participant('workitems', 'shingen', {}).first.class)
  461. end
  462. # General #query_workitems method.
  463. #
  464. def test_query_workitems
  465. return unless @s.respond_to?(:query_workitems)
  466. load_workitems
  467. assert_equal 3, @s.query_workitems('place' => 'kyouto').size
  468. assert_equal 1, @s.query_workitems('place' => 'kyouto', 'at' => 'kamo').size
  469. assert_equal(
  470. Ruote::Workitem, @s.query_workitems('place' => 'kyouto').first.class)
  471. end
  472. # === misc
  473. # Simply make sure the storage (well, at least its "error" type) is empty.
  474. #
  475. def test_starts_empty
  476. assert_equal 0, @s.get_many('errors').size
  477. end
  478. protected
  479. #
  480. # helpers
  481. def load_30_errors
  482. (1..30).to_a.shuffle.collect do |i|
  483. id = sprintf('yy!%0.2d', i)
  484. @s.put(
  485. '_id' => id,
  486. 'type' => 'errors',
  487. 'msg' => "whatever #{i}",
  488. 'fei' => { 'wfid' => id.split('!').last } )
  489. id
  490. end
  491. end
  492. def put_workitem(wfid, participant_name, fields)
  493. @s.put(
  494. 'type' => 'workitems',
  495. '_id' => "wi!0_0!12ff!#{wfid}",
  496. 'participant_name' => participant_name,
  497. 'wfid' => wfid,
  498. 'fields' => fields)
  499. end
  500. def load_workitems
  501. put_workitem(
  502. '20110218-nadanada', 'fujiwara', 'place' => 'kyouto')
  503. put_workitem(
  504. '20110218-nedenada', 'fujiwara', 'place' => 'kyouto', 'at' => 'kamo')
  505. put_workitem(
  506. '20110218-nadanodo', 'taira', 'place' => 'kyouto')
  507. put_workitem(
  508. '20110218-nodonada', 'date', 'place' => 'sendai')
  509. put_workitem(
  510. '20110218-nadanudu', 'shingen', 'place' => 'nagoya')
  511. end
  512. end