client_spec.rb 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. require File.dirname(__FILE__) + '/../init'
  2. class SphinxSpecError < StandardError; end
  3. module SphinxFixtureHelper
  4. def sphinx_fixture(name)
  5. `php #{File.dirname(__FILE__)}/fixtures/#{name}.php`
  6. end
  7. end
  8. module SphinxApiCall
  9. def create_sphinx
  10. @sphinx = Sphinx::Client.new
  11. @sock = mock('TCPSocket')
  12. @sphinx.stub!(:Connect).and_return(@sock)
  13. @sphinx.stub!(:GetResponse).and_raise(SphinxSpecError)
  14. return @sphinx
  15. end
  16. def safe_call
  17. yield
  18. rescue SphinxSpecError
  19. end
  20. end
  21. describe 'The Connect method of Sphinx::Client' do
  22. before(:each) do
  23. @sphinx = Sphinx::Client.new
  24. @sock = mock('TCPSocket')
  25. end
  26. it 'should establish TCP connection to the server and initialize session' do
  27. TCPSocket.should_receive(:new).with('localhost', 9312).and_return(@sock)
  28. @sock.should_receive(:recv).with(4).and_return([1].pack('N'))
  29. @sock.should_receive(:send).with([1].pack('N'), 0)
  30. @sphinx.send(:Connect).should be(@sock)
  31. end
  32. it 'should raise exception when searchd protocol is not 1+' do
  33. TCPSocket.should_receive(:new).with('localhost', 9312).and_return(@sock)
  34. @sock.should_receive(:recv).with(4).and_return([0].pack('N'))
  35. @sock.should_receive(:close)
  36. lambda { @sphinx.send(:Connect) }.should raise_error(Sphinx::SphinxConnectError)
  37. @sphinx.GetLastError.should == 'expected searchd protocol version 1+, got version \'0\''
  38. end
  39. it 'should raise exception on connection error' do
  40. TCPSocket.should_receive(:new).with('localhost', 9312).and_raise(Errno::EBADF)
  41. lambda { @sphinx.send(:Connect) }.should raise_error(Sphinx::SphinxConnectError)
  42. @sphinx.GetLastError.should == 'connection to localhost:9312 failed'
  43. end
  44. it 'should use custom host and port' do
  45. @sphinx.SetServer('anotherhost', 55555)
  46. TCPSocket.should_receive(:new).with('anotherhost', 55555).and_raise(Errno::EBADF)
  47. lambda { @sphinx.send(:Connect) }.should raise_error(Sphinx::SphinxConnectError)
  48. end
  49. end
  50. describe 'The GetResponse method of Sphinx::Client' do
  51. before(:each) do
  52. @sphinx = Sphinx::Client.new
  53. @sock = mock('TCPSocket')
  54. @sock.should_receive(:close)
  55. end
  56. it 'should receive response' do
  57. @sock.should_receive(:recv).with(8).and_return([Sphinx::Client::SEARCHD_OK, 1, 4].pack('n2N'))
  58. @sock.should_receive(:recv).with(4).and_return([0].pack('N'))
  59. @sphinx.send(:GetResponse, @sock, 1)
  60. end
  61. it 'should raise exception on zero-sized response' do
  62. @sock.should_receive(:recv).with(8).and_return([Sphinx::Client::SEARCHD_OK, 1, 0].pack('n2N'))
  63. lambda { @sphinx.send(:GetResponse, @sock, 1) }.should raise_error(Sphinx::SphinxResponseError)
  64. end
  65. it 'should raise exception when response is incomplete' do
  66. @sock.should_receive(:recv).with(8).and_return([Sphinx::Client::SEARCHD_OK, 1, 4].pack('n2N'))
  67. @sock.should_receive(:recv).with(4).and_raise(EOFError)
  68. lambda { @sphinx.send(:GetResponse, @sock, 1) }.should raise_error(Sphinx::SphinxResponseError)
  69. end
  70. it 'should set warning message when SEARCHD_WARNING received' do
  71. @sock.should_receive(:recv).with(8).and_return([Sphinx::Client::SEARCHD_WARNING, 1, 14].pack('n2N'))
  72. @sock.should_receive(:recv).with(14).and_return([5].pack('N') + 'helloworld')
  73. @sphinx.send(:GetResponse, @sock, 1).should == 'world'
  74. @sphinx.GetLastWarning.should == 'hello'
  75. end
  76. it 'should raise exception when SEARCHD_ERROR received' do
  77. @sock.should_receive(:recv).with(8).and_return([Sphinx::Client::SEARCHD_ERROR, 1, 9].pack('n2N'))
  78. @sock.should_receive(:recv).with(9).and_return([1].pack('N') + 'hello')
  79. lambda { @sphinx.send(:GetResponse, @sock, 1) }.should raise_error(Sphinx::SphinxInternalError)
  80. @sphinx.GetLastError.should == 'searchd error: hello'
  81. end
  82. it 'should raise exception when SEARCHD_RETRY received' do
  83. @sock.should_receive(:recv).with(8).and_return([Sphinx::Client::SEARCHD_RETRY, 1, 9].pack('n2N'))
  84. @sock.should_receive(:recv).with(9).and_return([1].pack('N') + 'hello')
  85. lambda { @sphinx.send(:GetResponse, @sock, 1) }.should raise_error(Sphinx::SphinxTemporaryError)
  86. @sphinx.GetLastError.should == 'temporary searchd error: hello'
  87. end
  88. it 'should raise exception when unknown status received' do
  89. @sock.should_receive(:recv).with(8).and_return([65535, 1, 9].pack('n2N'))
  90. @sock.should_receive(:recv).with(9).and_return([1].pack('N') + 'hello')
  91. lambda { @sphinx.send(:GetResponse, @sock, 1) }.should raise_error(Sphinx::SphinxUnknownError)
  92. @sphinx.GetLastError.should == 'unknown status code: \'65535\''
  93. end
  94. it 'should set warning when server is older than client' do
  95. @sock.should_receive(:recv).with(8).and_return([Sphinx::Client::SEARCHD_OK, 1, 9].pack('n2N'))
  96. @sock.should_receive(:recv).with(9).and_return([1].pack('N') + 'hello')
  97. @sphinx.send(:GetResponse, @sock, 5)
  98. @sphinx.GetLastWarning.should == 'searchd command v.0.1 older than client\'s v.0.5, some options might not work'
  99. end
  100. end
  101. describe 'The Query method of Sphinx::Client' do
  102. include SphinxFixtureHelper
  103. include SphinxApiCall
  104. before(:each) do
  105. @sphinx = create_sphinx
  106. end
  107. it 'should generate valid request with default parameters' do
  108. expected = sphinx_fixture('default_search')
  109. @sock.should_receive(:send).with(expected, 0)
  110. @sphinx.Query('query') rescue nil?
  111. end
  112. it 'should generate valid request with default parameters and index' do
  113. expected = sphinx_fixture('default_search_index')
  114. @sock.should_receive(:send).with(expected, 0)
  115. @sphinx.Query('query', 'index') rescue nil?
  116. end
  117. it 'should generate valid request with limits' do
  118. expected = sphinx_fixture('limits')
  119. @sock.should_receive(:send).with(expected, 0)
  120. @sphinx.SetLimits(10, 20)
  121. @sphinx.Query('query') rescue nil?
  122. end
  123. it 'should generate valid request with limits and max number to retrieve' do
  124. expected = sphinx_fixture('limits_max')
  125. @sock.should_receive(:send).with(expected, 0)
  126. @sphinx.SetLimits(10, 20, 30)
  127. @sphinx.Query('query') rescue nil?
  128. end
  129. it 'should generate valid request with limits and cutoff to retrieve' do
  130. expected = sphinx_fixture('limits_cutoff')
  131. @sock.should_receive(:send).with(expected, 0)
  132. @sphinx.SetLimits(10, 20, 30, 40)
  133. @sphinx.Query('query') rescue nil?
  134. end
  135. it 'should generate valid request with max query time specified' do
  136. expected = sphinx_fixture('max_query_time')
  137. @sock.should_receive(:send).with(expected, 0)
  138. @sphinx.SetMaxQueryTime(1000)
  139. @sphinx.Query('query') rescue nil?
  140. end
  141. describe 'with match' do
  142. [ :all, :any, :phrase, :boolean, :extended, :fullscan, :extended2 ].each do |match|
  143. it "should generate valid request for SPH_MATCH_#{match.to_s.upcase}" do
  144. expected = sphinx_fixture("match_#{match}")
  145. @sock.should_receive(:send).with(expected, 0)
  146. @sphinx.SetMatchMode(Sphinx::Client::const_get("SPH_MATCH_#{match.to_s.upcase}"))
  147. @sphinx.Query('query') rescue nil?
  148. end
  149. end
  150. end
  151. describe 'with rank' do
  152. [ :proximity_bm25, :bm25, :none, :wordcount, :proximity ].each do |rank|
  153. it "should generate valid request for SPH_RANK_#{rank.to_s.upcase}" do
  154. expected = sphinx_fixture("ranking_#{rank}")
  155. @sock.should_receive(:send).with(expected, 0)
  156. @sphinx.SetRankingMode(Sphinx::Client.const_get("SPH_RANK_#{rank.to_s.upcase}"))
  157. @sphinx.Query('query') rescue nil?
  158. end
  159. end
  160. end
  161. describe 'with sorting' do
  162. [ :attr_desc, :relevance, :attr_asc, :time_segments, :extended, :expr ].each do |mode|
  163. it "should generate valid request for SPH_SORT_#{mode.to_s.upcase}" do
  164. expected = sphinx_fixture("sort_#{mode}")
  165. @sock.should_receive(:send).with(expected, 0)
  166. @sphinx.SetSortMode(Sphinx::Client.const_get("SPH_SORT_#{mode.to_s.upcase}"), mode == :relevance ? '' : 'sortby')
  167. @sphinx.Query('query') rescue nil?
  168. end
  169. end
  170. end
  171. it 'should generate valid request with weights' do
  172. expected = sphinx_fixture('weights')
  173. @sock.should_receive(:send).with(expected, 0)
  174. @sphinx.SetWeights([10, 20, 30, 40])
  175. @sphinx.Query('query') rescue nil?
  176. end
  177. it 'should generate valid request with field weights' do
  178. expected = sphinx_fixture('field_weights')
  179. @sock.should_receive(:send).with(expected, 0)
  180. @sphinx.SetFieldWeights({'field1' => 10, 'field2' => 20})
  181. @sphinx.Query('query') rescue nil?
  182. end
  183. it 'should generate valid request with index weights' do
  184. expected = sphinx_fixture('index_weights')
  185. @sock.should_receive(:send).with(expected, 0)
  186. @sphinx.SetIndexWeights({'index1' => 10, 'index2' => 20})
  187. @sphinx.Query('query') rescue nil?
  188. end
  189. it 'should generate valid request with ID range' do
  190. expected = sphinx_fixture('id_range')
  191. @sock.should_receive(:send).with(expected, 0)
  192. @sphinx.SetIDRange(10, 20)
  193. @sphinx.Query('query') rescue nil?
  194. end
  195. it 'should generate valid request with ID range and 64-bit ints' do
  196. expected = sphinx_fixture('id_range64')
  197. @sock.should_receive(:send).with(expected, 0)
  198. @sphinx.SetIDRange(8589934591, 17179869183)
  199. @sphinx.Query('query') rescue nil?
  200. end
  201. it 'should generate valid request with values filter' do
  202. expected = sphinx_fixture('filter')
  203. @sock.should_receive(:send).with(expected, 0)
  204. @sphinx.SetFilter('attr', [10, 20, 30])
  205. @sphinx.Query('query') rescue nil?
  206. end
  207. it 'should generate valid request with two values filters' do
  208. expected = sphinx_fixture('filters')
  209. @sock.should_receive(:send).with(expected, 0)
  210. @sphinx.SetFilter('attr2', [40, 50])
  211. @sphinx.SetFilter('attr1', [10, 20, 30])
  212. @sphinx.Query('query') rescue nil?
  213. end
  214. it 'should generate valid request with values filter excluded' do
  215. expected = sphinx_fixture('filter_exclude')
  216. @sock.should_receive(:send).with(expected, 0)
  217. @sphinx.SetFilter('attr', [10, 20, 30], true)
  218. @sphinx.Query('query') rescue nil?
  219. end
  220. it 'should generate valid request with values filter range' do
  221. expected = sphinx_fixture('filter_range')
  222. @sock.should_receive(:send).with(expected, 0)
  223. @sphinx.SetFilterRange('attr', 10, 20)
  224. @sphinx.Query('query') rescue nil?
  225. end
  226. it 'should generate valid request with two filter ranges' do
  227. expected = sphinx_fixture('filter_ranges')
  228. @sock.should_receive(:send).with(expected, 0)
  229. @sphinx.SetFilterRange('attr2', 30, 40)
  230. @sphinx.SetFilterRange('attr1', 10, 20)
  231. @sphinx.Query('query') rescue nil?
  232. end
  233. it 'should generate valid request with filter range excluded' do
  234. expected = sphinx_fixture('filter_range_exclude')
  235. @sock.should_receive(:send).with(expected, 0)
  236. @sphinx.SetFilterRange('attr', 10, 20, true)
  237. @sphinx.Query('query') rescue nil?
  238. end
  239. it 'should generate valid request with signed int64-based filter range' do
  240. expected = sphinx_fixture('filter_range_int64')
  241. @sock.should_receive(:send).with(expected, 0)
  242. @sphinx.SetFilterRange('attr1', -10, 20)
  243. @sphinx.SetFilterRange('attr2', -1099511627770, 1099511627780)
  244. safe_call { @sphinx.Query('query') }
  245. end
  246. it 'should generate valid request with float filter range' do
  247. expected = sphinx_fixture('filter_float_range')
  248. @sock.should_receive(:send).with(expected, 0)
  249. @sphinx.SetFilterFloatRange('attr', 10.5, 20.3)
  250. @sphinx.Query('query') rescue nil?
  251. end
  252. it 'should generate valid request with float filter excluded' do
  253. expected = sphinx_fixture('filter_float_range_exclude')
  254. @sock.should_receive(:send).with(expected, 0)
  255. @sphinx.SetFilterFloatRange('attr', 10.5, 20.3, true)
  256. @sphinx.Query('query') rescue nil?
  257. end
  258. it 'should generate valid request with different filters' do
  259. expected = sphinx_fixture('filters_different')
  260. @sock.should_receive(:send).with(expected, 0)
  261. @sphinx.SetFilterRange('attr1', 10, 20, true)
  262. @sphinx.SetFilter('attr3', [30, 40, 50])
  263. @sphinx.SetFilterRange('attr1', 60, 70)
  264. @sphinx.SetFilter('attr2', [80, 90, 100], true)
  265. @sphinx.SetFilterFloatRange('attr1', 60.8, 70.5)
  266. @sphinx.Query('query') rescue nil?
  267. end
  268. it 'should generate valid request with geographical anchor point' do
  269. expected = sphinx_fixture('geo_anchor')
  270. @sock.should_receive(:send).with(expected, 0)
  271. @sphinx.SetGeoAnchor('attrlat', 'attrlong', 20.3, 40.7)
  272. @sphinx.Query('query') rescue nil?
  273. end
  274. describe 'with group by' do
  275. [ :day, :week, :month, :year, :attr, :attrpair ].each do |groupby|
  276. it "should generate valid request for SPH_GROUPBY_#{groupby.to_s.upcase}" do
  277. expected = sphinx_fixture("group_by_#{groupby}")
  278. @sock.should_receive(:send).with(expected, 0)
  279. @sphinx.SetGroupBy('attr', Sphinx::Client::const_get("SPH_GROUPBY_#{groupby.to_s.upcase}"))
  280. @sphinx.Query('query') rescue nil?
  281. end
  282. end
  283. it 'should generate valid request for SPH_GROUPBY_DAY with sort' do
  284. expected = sphinx_fixture('group_by_day_sort')
  285. @sock.should_receive(:send).with(expected, 0)
  286. @sphinx.SetGroupBy('attr', Sphinx::Client::SPH_GROUPBY_DAY, 'somesort')
  287. @sphinx.Query('query') rescue nil?
  288. end
  289. it 'should generate valid request with count-distinct attribute' do
  290. expected = sphinx_fixture('group_distinct')
  291. @sock.should_receive(:send).with(expected, 0)
  292. @sphinx.SetGroupBy('attr', Sphinx::Client::SPH_GROUPBY_DAY)
  293. @sphinx.SetGroupDistinct('attr')
  294. @sphinx.Query('query') rescue nil?
  295. end
  296. end
  297. it 'should generate valid request with retries count specified' do
  298. expected = sphinx_fixture('retries')
  299. @sock.should_receive(:send).with(expected, 0)
  300. @sphinx.SetRetries(10)
  301. @sphinx.Query('query') rescue nil?
  302. end
  303. it 'should generate valid request with retries count and delay specified' do
  304. expected = sphinx_fixture('retries_delay')
  305. @sock.should_receive(:send).with(expected, 0)
  306. @sphinx.SetRetries(10, 20)
  307. @sphinx.Query('query') rescue nil?
  308. end
  309. it 'should generate valid request for SetOverride' do
  310. expected = sphinx_fixture('set_override')
  311. @sock.should_receive(:send).with(expected, 0)
  312. @sphinx.SetOverride('attr1', Sphinx::Client::SPH_ATTR_INTEGER, { 10 => 20 })
  313. @sphinx.SetOverride('attr2', Sphinx::Client::SPH_ATTR_FLOAT, { 11 => 30.3 })
  314. @sphinx.SetOverride('attr3', Sphinx::Client::SPH_ATTR_BIGINT, { 12 => 1099511627780 })
  315. @sphinx.Query('query') rescue nil?
  316. end
  317. it 'should generate valid request for SetSelect' do
  318. expected = sphinx_fixture('select')
  319. @sock.should_receive(:send).with(expected, 0)
  320. @sphinx.SetSelect('attr1, attr2')
  321. @sphinx.Query('query') rescue nil?
  322. end
  323. end
  324. describe 'The RunQueries method of Sphinx::Client' do
  325. include SphinxFixtureHelper
  326. before(:each) do
  327. @sphinx = Sphinx::Client.new
  328. @sock = mock('TCPSocket')
  329. @sphinx.stub!(:Connect).and_return(@sock)
  330. @sphinx.stub!(:GetResponse).and_raise(Sphinx::SphinxError)
  331. end
  332. it 'should generate valid request for multiple queries' do
  333. expected = sphinx_fixture('miltiple_queries')
  334. @sock.should_receive(:send).with(expected, 0)
  335. @sphinx.SetRetries(10, 20)
  336. @sphinx.AddQuery('test1')
  337. @sphinx.SetGroupBy('attr', Sphinx::Client::SPH_GROUPBY_DAY)
  338. @sphinx.AddQuery('test2') rescue nil?
  339. @sphinx.RunQueries rescue nil?
  340. end
  341. end
  342. describe 'The BuildExcerpts method of Sphinx::Client' do
  343. include SphinxFixtureHelper
  344. before(:each) do
  345. @sphinx = Sphinx::Client.new
  346. @sock = mock('TCPSocket')
  347. @sphinx.stub!(:Connect).and_return(@sock)
  348. @sphinx.stub!(:GetResponse).and_raise(Sphinx::SphinxError)
  349. end
  350. it 'should generate valid request with default parameters' do
  351. expected = sphinx_fixture('excerpt_default')
  352. @sock.should_receive(:send).with(expected, 0)
  353. @sphinx.BuildExcerpts(['10', '20'], 'index', 'word1 word2') rescue nil?
  354. end
  355. it 'should generate valid request with custom parameters' do
  356. expected = sphinx_fixture('excerpt_custom')
  357. @sock.should_receive(:send).with(expected, 0)
  358. @sphinx.BuildExcerpts(['10', '20'], 'index', 'word1 word2', { 'before_match' => 'before',
  359. 'after_match' => 'after',
  360. 'chunk_separator' => 'separator',
  361. 'limit' => 10 }) rescue nil?
  362. end
  363. it 'should generate valid request with flags' do
  364. expected = sphinx_fixture('excerpt_flags')
  365. @sock.should_receive(:send).with(expected, 0)
  366. @sphinx.BuildExcerpts(['10', '20'], 'index', 'word1 word2', { 'exact_phrase' => true,
  367. 'single_passage' => true,
  368. 'use_boundaries' => true,
  369. 'weight_order' => true }) rescue nil?
  370. end
  371. end
  372. describe 'The BuildKeywords method of Sphinx::Client' do
  373. include SphinxFixtureHelper
  374. include SphinxApiCall
  375. before(:each) do
  376. @sphinx = create_sphinx
  377. end
  378. it 'should generate valid request' do
  379. expected = sphinx_fixture('keywords')
  380. @sock.should_receive(:send).with(expected, 0)
  381. safe_call { @sphinx.BuildKeywords('test', 'index', true) }
  382. end
  383. end
  384. describe 'The UpdateAttributes method of Sphinx::Client' do
  385. include SphinxFixtureHelper
  386. include SphinxApiCall
  387. before(:each) do
  388. @sphinx = create_sphinx
  389. end
  390. it 'should generate valid request' do
  391. expected = sphinx_fixture('update_attributes')
  392. @sock.should_receive(:send).with(expected, 0)
  393. safe_call { @sphinx.UpdateAttributes('index', ['group'], { 123 => [456] }) }
  394. end
  395. it 'should generate valid request for MVA' do
  396. expected = sphinx_fixture('update_attributes_mva')
  397. @sock.should_receive(:send).with(expected, 0)
  398. safe_call { @sphinx.UpdateAttributes('index', ['group', 'category'], { 123 => [ [456, 789], [1, 2, 3] ] }, true) }
  399. end
  400. end