test: rot13 simplified
[rbot] / test / test_journal.rb
1 $:.unshift File.join(File.dirname(__FILE__), '../lib')
2
3 module Irc
4 class Bot
5   module Config
6     @@datadir = File.expand_path(File.dirname($0) + '/../data/rbot')
7     @@coredir = File.expand_path(File.dirname($0) + '/../lib/rbot/core') 
8   end
9 end
10 end
11
12 require 'test/unit'
13 require 'rbot/ircbot'
14 require 'rbot/journal'
15
16 require 'benchmark'
17
18 DAY=60*60*24
19
20 class JournalMessageTest < Test::Unit::TestCase
21
22   include Irc::Bot::Journal
23
24   def test_get
25     m = JournalMessage.create('foo', {'bar': 42, 'baz': nil, 'qux': {'quxx': 23}})
26     assert_equal(42, m.get('bar'))
27     assert_raise ArgumentError do
28       m.get('nope')
29     end
30     assert_nil(m.get('nope', nil))
31     assert_nil(m.get('baz'))
32     assert_equal(23, m['qux.quxx'])
33     assert_equal(nil, m['qux.nope'])
34     assert_raise(ArgumentError) { m.get('qux.nope') }
35   end
36
37 end
38
39 class QueryTest < Test::Unit::TestCase
40
41   include Irc::Bot::Journal
42
43   def test_define
44
45     q = Query.define do
46       id 'foo'
47       id 'bar', 'baz'
48       topic 'log.irc.*'
49       topic 'log.core', 'baz'
50       timestamp from: Time.now, to: Time.now + 60 * 10
51       payload 'action': :privmsg, 'alice': 'bob'
52       payload 'channel': '#rbot'
53       payload 'foo.bar': 'baz'
54     end
55     assert_equal(['foo', 'bar', 'baz'], q.id)
56     assert_equal(['log.irc.*', 'log.core', 'baz'], q.topic)
57     assert_equal([:from, :to], q.timestamp.keys)
58     assert_equal(Time, q.timestamp[:to].class)
59     assert_equal(Time, q.timestamp[:from].class)
60     assert_equal({
61       'action': :privmsg, 'alice': 'bob',
62       'channel': '#rbot',
63       'foo.bar': 'baz'
64     }, q.payload)
65
66   end
67
68   def test_topic_matches
69     q = Query.define do
70       topic 'foo'
71     end
72     assert_true(q.topic_matches?('foo'))
73     assert_false(q.topic_matches?('bar'))
74     assert_false(q.topic_matches?('foo.bar'))
75
76     q = Query.define do
77       topic 'foo.bar'
78     end
79     assert_false(q.topic_matches?('foo'))
80     assert_false(q.topic_matches?('bar'))
81     assert_true(q.topic_matches?('foo.bar'))
82
83     q = Query.define do
84       topic 'foo.*'
85     end
86     assert_false(q.topic_matches?('foo'))
87     assert_false(q.topic_matches?('bar'))
88     assert_true(q.topic_matches?('foo.bar'))
89     assert_true(q.topic_matches?('foo.baz'))
90
91     q = Query.define do
92       topic '*.bar'
93     end
94     assert_false(q.topic_matches?('foo'))
95     assert_false(q.topic_matches?('bar'))
96     assert_true(q.topic_matches?('foo.bar'))
97     assert_true(q.topic_matches?('bar.bar'))
98     assert_false(q.topic_matches?('foo.foo'))
99
100     q = Query.define do
101       topic '*.*'
102     end
103     assert_false(q.topic_matches?('foo'))
104     assert_true(q.topic_matches?('foo.bar'))
105
106     q = Query.define do
107       topic 'foo'
108       topic 'bar'
109       topic 'baz.alice.bob.*.foo'
110     end
111     assert_true(q.topic_matches?('foo'))
112     assert_true(q.topic_matches?('bar'))
113     assert_true(q.topic_matches?('baz.alice.bob.asdf.foo'))
114     assert_false(q.topic_matches?('baz.alice.bob..foo'))
115
116   end
117   def test_matches
118     q = Query.define do
119       #id 'foo', 'bar'
120       topic 'log.irc.*', 'log.core'
121       timestamp from: Time.now - DAY, to: Time.now + DAY
122       payload 'action': 'privmsg', 'foo.bar': 'baz'
123     end
124     assert_true(q.matches? JournalMessage.create('log.irc.raw', {'action' => 'privmsg'}))
125     assert_false(q.matches? JournalMessage.create('baz', {}))
126     assert_true(q.matches? JournalMessage.create('log.core', {foo: {bar: 'baz'}}))
127
128     # tests timestamp from/to:
129     assert_true(q.matches? JournalMessage.new(
130       id: 'foo',
131       topic: 'log.core',
132       timestamp: Time.now,
133       payload: {action: 'privmsg'}))
134     assert_false(q.matches? JournalMessage.new(
135       id: 'foo',
136       topic: 'log.core',
137       timestamp: Time.now - DAY*3,
138       payload: {action: 'privmsg'}))
139     assert_false(q.matches? JournalMessage.new(
140       id: 'foo',
141       topic: 'log.core',
142       timestamp: Time.now + DAY*3,
143       payload: {action: 'privmsg'}))
144   end
145
146 end
147
148 class JournalBrokerTest < Test::Unit::TestCase
149
150   include Irc::Bot::Journal
151
152   def test_publish
153     received = []
154     journal = JournalBroker.new(consumer: Proc.new { |message|
155       received << message
156     })
157
158     # publish some messages:
159     journal.publish 'log.irc',
160       source: 'alice', message: '<3 pg'
161     journal.publish 'log.irc',
162       source: 'bob', message: 'mysql > pg'
163     journal.publish 'log.irc',
164       source: 'alice', target: 'bob', action: :kick
165
166     # wait for messages to be consumed:
167     sleep 0.1
168     assert_equal(3, received.length)
169   end
170
171   def test_subscribe
172     received = []
173     journal = JournalBroker.new
174
175     # subscribe to messages for topic foo:
176     sub = journal.subscribe('foo') do |message|
177       received << message
178     end
179
180     # publish some messages:
181     journal.publish 'foo', {}
182     journal.publish 'bar', {}
183     journal.publish 'foo', {}
184
185     # wait for messages to be consumed:
186     sleep 0.1
187     assert_equal(2, received.length)
188
189     received.clear
190
191     journal.publish 'foo', {}
192     sleep 0.1
193     sub.cancel
194     journal.publish 'foo', {}
195     sleep 0.1
196     assert_equal(1, received.length)
197   end
198
199 end
200
201 module JournalStorageTestMixin
202
203   include Irc::Bot::Journal
204
205   def teardown
206     @storage.drop
207   end
208
209   def test_operations
210     # insertion
211     m = JournalMessage.create('log.core', {foo: {bar: 'baz', qux: 42}})
212     @storage.insert(m)
213
214     # query by id
215     res = @storage.find(Query.define { id m.id })
216     assert_equal(1, res.length)
217     assert_equal(m, res.first)
218
219     # check timestamp was returned correctly:
220     assert_equal(m.timestamp.strftime('%Y-%m-%d %H:%M:%S%z'),
221                  res.first.timestamp.strftime('%Y-%m-%d %H:%M:%S%z'))
222
223     # check if payload was returned correctly:
224     assert_equal({'foo' => {'bar' => 'baz', 'qux' => 42}}, res.first.payload)
225
226     # query by topic
227     assert_equal(m, @storage.find(Query.define { topic('log.core') }).first)
228     assert_equal(m, @storage.find(Query.define { topic('log.*') }).first)
229     assert_equal(m, @storage.find(Query.define { topic('*.*') }).first)
230
231     # query by timestamp range
232     assert_equal(1, @storage.find(Query.define {
233       timestamp(from: Time.now-DAY, to: Time.now+DAY) }).length)
234     assert_equal(0, @storage.find(Query.define {
235       timestamp(from: Time.now-DAY*2, to: Time.now-DAY) }).length)
236
237     # query by payload
238     res = @storage.find(Query.define { payload('foo.bar' => 'baz') })
239     assert_equal(m, res.first)
240     res = @storage.find(Query.define { payload('foo.bar' => 'x') })
241     assert_true(res.empty?)
242
243     # without arguments: find and count
244     assert_equal(1, @storage.count)
245     assert_equal(m, @storage.find.first)
246   end
247
248   def test_find
249     # tests limit/offset and block parameters of find()
250     @storage.insert(JournalMessage.create('irclogs', {message: 'foo'}))
251     @storage.insert(JournalMessage.create('irclogs', {message: 'bar'}))
252     @storage.insert(JournalMessage.create('irclogs', {message: 'baz'}))
253     @storage.insert(JournalMessage.create('irclogs', {message: 'qux'}))
254
255     msgs = []
256     @storage.find(Query.define({topic: 'irclogs'}), 2, 1) do |m|
257       msgs << m
258     end
259     assert_equal(2, msgs.length)
260     assert_equal('bar', msgs.first['message'])
261     assert_equal('baz', msgs.last['message'])
262
263     msgs = []
264     @storage.find(Query.define({topic: 'irclogs'})) do |m|
265       msgs << m
266     end
267     assert_equal(4, msgs.length)
268     assert_equal('foo', msgs.first['message'])
269     assert_equal('qux', msgs.last['message'])
270
271   end
272
273   def test_operations_multiple
274     # test operations on multiple messages
275     # insert a bunch:
276     @storage.insert(JournalMessage.create('test.topic', {name: 'one'}))
277     @storage.insert(JournalMessage.create('test.topic', {name: 'two'}))
278     @storage.insert(JournalMessage.create('test.topic', {name: 'three'}))
279     @storage.insert(JournalMessage.create('archived.topic', {name: 'four'},
280       timestamp: Time.now - DAY*100))
281     @storage.insert(JournalMessage.create('complex', {name: 'five', country: {
282       name: 'Italy'
283     }}))
284     @storage.insert(JournalMessage.create('complex', {name: 'six', country: {
285       name: 'Austria'
286     }}))
287
288     # query by topic
289     assert_equal(3, @storage.find(Query.define { topic 'test.*' }).length)
290     # query by payload
291     assert_equal(1, @storage.find(Query.define {
292       payload('country.name' => 'Austria') }).length)
293     # query by timestamp range
294     assert_equal(1, @storage.find(Query.define {
295       timestamp(from: Time.now - DAY*150, to: Time.now - DAY*50) }).length)
296
297     # count with query
298     assert_equal(2, @storage.count(Query.define { topic('complex') }))
299     assert_equal(6, @storage.count)
300     @storage.remove(Query.define { topic('archived.*') })
301     assert_equal(5, @storage.count)
302     @storage.remove
303     assert_equal(0, @storage.count)
304   end
305
306   def test_broker_interface
307     journal = JournalBroker.new(storage: @storage) 
308
309     journal.publish 'irclogs', message: 'foo'
310     journal.publish 'irclogs', message: 'bar'
311     journal.publish 'irclogs', message: 'baz'
312     journal.publish 'irclogs', message: 'qux'
313
314     # wait for messages to be consumed:
315     sleep 0.1
316
317     msgs = []
318     journal.find({topic: 'irclogs'}, 2, 1) do |m|
319       msgs << m
320     end
321     assert_equal(2, msgs.length)
322     assert_equal('bar', msgs.first['message'])
323     assert_equal('baz', msgs.last['message'])
324
325     journal.ensure_payload_index('foo.bar.baz')
326   end
327
328   NUM=100 # 1_000_000
329   def test_benchmark
330     puts
331
332     assert_equal(0, @storage.count)
333     # prepare messages to insert, we benchmark the storage backend not ruby
334     num = 0
335     messages = (0...NUM).map do
336       num += 1
337       JournalMessage.create(
338             'test.topic.num_'+num.to_s, {answer: {number: '42', word: 'forty-two'}})
339     end
340
341     # iter is the number of operations performed WITHIN block
342     def benchmark(label, iter, &block)
343       time = Benchmark.realtime do
344         yield
345       end
346       puts label + ' %d iterations, duration: %.3fms (%.3fms / iteration)' % [iter, time*1000, (time*1000) / iter]
347     end
348
349     benchmark(@storage.class.to_s+'~insert', messages.length) do
350       messages.each { |m|
351         @storage.insert(m)
352       }
353     end
354
355     benchmark(@storage.class.to_s+'~find_by_id', messages.length) do
356       messages.each { |m|
357         @storage.find(Query.define { id m.id })
358       }
359     end
360     benchmark(@storage.class.to_s+'~find_by_topic', messages.length) do
361       messages.each { |m|
362         @storage.find(Query.define { topic m.topic })
363       }
364     end
365     benchmark(@storage.class.to_s+'~find_by_topic_wildcard', messages.length) do
366       messages.each { |m|
367         @storage.find(Query.define { topic m.topic.gsub('topic', '*') })
368       }
369     end
370   end
371
372 end
373
374 begin
375   require 'rbot/journal/postgres.rb'
376   if ENV['PG_URI']
377   class JournalStoragePostgresTest < Test::Unit::TestCase
378
379     include JournalStorageTestMixin
380
381     def setup
382       @storage = Storage::PostgresStorage.new(
383         uri: ENV['PG_URI'] || 'postgresql://localhost/rbot_journal',
384         drop: true)
385     end
386
387     def test_query_to_sql
388       q = Query.define do
389         id 'foo'
390         id 'bar', 'baz'
391         topic 'log.irc.*'
392         topic 'log.core', 'baz'
393         timestamp from: Time.now, to: Time.now + 60 * 10
394         payload 'action': :privmsg, 'alice': 'bob'
395         payload 'channel': '#rbot'
396         payload 'foo.bar': 'baz'
397       end
398       sql = @storage.query_to_sql(q)
399       assert_equal("(id = $1 OR id = $2 OR id = $3) AND (topic ILIKE $4 OR topic ILIKE $5 OR topic ILIKE $6) AND (timestamp >= $7 AND timestamp <= $8) AND (payload->>'action' = $9 OR payload->>'alice' = $10 OR payload->>'channel' = $11 OR payload->'foo'->>'bar' = $12)", sql[0])
400       q = Query.define do
401         id 'foo'
402       end
403       assert_equal('(id = $1)', @storage.query_to_sql(q)[0])
404       q = Query.define do
405         topic 'foo.*.bar'
406       end
407       assert_equal('(topic ILIKE $1)', @storage.query_to_sql(q)[0])
408       assert_equal(['foo.%.bar'], @storage.query_to_sql(q)[1])
409     end
410
411   end
412   else
413     puts 'NOTE: Set PG_URI environment variable to test postgresql storage.'
414   end
415 rescue Exception; end
416
417 begin
418   require 'rbot/journal/mongo.rb'
419   if ENV['MONGO_URI']
420   class JournalStorageMongoTest < Test::Unit::TestCase
421
422     include JournalStorageTestMixin
423
424     def setup
425       @storage = Storage::MongoStorage.new(
426         uri: ENV['MONGO_URI'] || 'mongodb://127.0.0.1:27017/rbot',
427         drop: true)
428     end
429   end
430   else
431     puts 'NOTE: Set MONGO_URI environment variable to test postgresql storage.'
432   end
433 rescue Exception; end