bitgirder-platform 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE.txt +176 -0
- data/bin/ensure-test-db +117 -0
- data/bin/install-mysql +375 -0
- data/bin/tomcat7 +569 -0
- data/lib/bitgirder/concurrent.rb +400 -0
- data/lib/bitgirder/core.rb +1235 -0
- data/lib/bitgirder/etl.rb +58 -0
- data/lib/bitgirder/event/file.rb +485 -0
- data/lib/bitgirder/event/logger/testing.rb +140 -0
- data/lib/bitgirder/event/logger.rb +137 -0
- data/lib/bitgirder/event/testing.rb +88 -0
- data/lib/bitgirder/http.rb +255 -0
- data/lib/bitgirder/io/testing.rb +33 -0
- data/lib/bitgirder/io.rb +959 -0
- data/lib/bitgirder/irb.rb +35 -0
- data/lib/bitgirder/mysql.rb +60 -0
- data/lib/bitgirder/ops/java.rb +117 -0
- data/lib/bitgirder/ops/ruby.rb +235 -0
- data/lib/bitgirder/testing.rb +152 -0
- data/lib/doc-gen0.rb +0 -0
- data/lib/doc-gen1.rb +0 -0
- data/lib/doc-gen10.rb +0 -0
- data/lib/doc-gen11.rb +0 -0
- data/lib/doc-gen12.rb +0 -0
- data/lib/doc-gen13.rb +0 -0
- data/lib/doc-gen14.rb +0 -0
- data/lib/doc-gen15.rb +0 -0
- data/lib/doc-gen16.rb +0 -0
- data/lib/doc-gen17.rb +14 -0
- data/lib/doc-gen18.rb +0 -0
- data/lib/doc-gen19.rb +0 -0
- data/lib/doc-gen2.rb +0 -0
- data/lib/doc-gen20.rb +182 -0
- data/lib/doc-gen21.rb +0 -0
- data/lib/doc-gen3.rb +0 -0
- data/lib/doc-gen4.rb +0 -0
- data/lib/doc-gen5.rb +0 -0
- data/lib/doc-gen6.rb +0 -0
- data/lib/doc-gen7.rb +0 -0
- data/lib/doc-gen8.rb +0 -0
- data/lib/doc-gen9.rb +0 -0
- data/lib/mingle/bincodec.rb +512 -0
- data/lib/mingle/codec.rb +54 -0
- data/lib/mingle/http.rb +156 -0
- data/lib/mingle/io/stream.rb +142 -0
- data/lib/mingle/io.rb +160 -0
- data/lib/mingle/json.rb +257 -0
- data/lib/mingle/service.rb +110 -0
- data/lib/mingle-em.rb +92 -0
- data/lib/mingle.rb +2917 -0
- metadata +100 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require 'bitgirder/core'
|
|
2
|
+
|
|
3
|
+
module BitGirder
|
|
4
|
+
module Etl
|
|
5
|
+
|
|
6
|
+
include BitGirder::Core
|
|
7
|
+
|
|
8
|
+
PHASE_EXTRACT = :extract
|
|
9
|
+
PHASE_TRANSFORM = :transform
|
|
10
|
+
PHASE_LOAD = :load
|
|
11
|
+
|
|
12
|
+
class BlockScanner < BitGirderClass
|
|
13
|
+
|
|
14
|
+
bg_attr :records
|
|
15
|
+
bg_attr :block
|
|
16
|
+
|
|
17
|
+
public
|
|
18
|
+
def each_with_id
|
|
19
|
+
@records.each_with_index { |rec, idx| yield( rec, @block.ids[ idx ] ) }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class RecordBlock < BitGirderClass
|
|
24
|
+
|
|
25
|
+
bg_attr :ids
|
|
26
|
+
bg_attr :records
|
|
27
|
+
bg_attr :next_read
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
def impl_initialize
|
|
31
|
+
|
|
32
|
+
id_len = @ids.size
|
|
33
|
+
|
|
34
|
+
@records.each_pair do |coding, recs|
|
|
35
|
+
unless recs.size == @ids.size
|
|
36
|
+
raise "Block has #{@ids.size} ids but #{@recs.size} records " \
|
|
37
|
+
"for coding #@coding"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
public
|
|
43
|
+
def size
|
|
44
|
+
@ids.size
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
public
|
|
48
|
+
def coding( nm )
|
|
49
|
+
|
|
50
|
+
recs =
|
|
51
|
+
( @records[ nm ] or raise "Block has no records for coding: #{nm}" )
|
|
52
|
+
|
|
53
|
+
BlockScanner.new( :records => recs, :block => self )
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
require 'bitgirder/core'
|
|
2
|
+
include BitGirder::Core
|
|
3
|
+
|
|
4
|
+
require 'bitgirder/io'
|
|
5
|
+
|
|
6
|
+
require 'stringio'
|
|
7
|
+
|
|
8
|
+
module BitGirder
|
|
9
|
+
module Event
|
|
10
|
+
module File
|
|
11
|
+
|
|
12
|
+
include BitGirder::Io
|
|
13
|
+
|
|
14
|
+
FILE_MAGIC = "3vEntF!L"
|
|
15
|
+
FILE_VERSION = "event-file20120703"
|
|
16
|
+
|
|
17
|
+
class EventFileIo < BitGirderClass
|
|
18
|
+
|
|
19
|
+
bg_attr :codec
|
|
20
|
+
|
|
21
|
+
HEADER_SIZE = 4
|
|
22
|
+
|
|
23
|
+
DEFAULT_BUFFER_SIZE = Io::DataSize.as_instance( "4m" )
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class OpenResult < BitGirderClass
|
|
27
|
+
|
|
28
|
+
bg_attr :io
|
|
29
|
+
bg_attr :is_reopen, :default => false
|
|
30
|
+
bg_attr :pos, :default => 0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class WriteError < StandardError; end
|
|
34
|
+
|
|
35
|
+
class ClosedError < StandardError; end
|
|
36
|
+
|
|
37
|
+
class EventFileWriter < EventFileIo
|
|
38
|
+
|
|
39
|
+
DEFAULT_ROTATE_SIZE = Io::DataSize.as_instance( "64m" )
|
|
40
|
+
|
|
41
|
+
bg_attr :file_factory
|
|
42
|
+
|
|
43
|
+
bg_attr :rotate_size,
|
|
44
|
+
:processor => Io::DataSize,
|
|
45
|
+
:default => DEFAULT_ROTATE_SIZE
|
|
46
|
+
|
|
47
|
+
bg_attr :buffer_size, :processor => Io::DataSize, :required => false
|
|
48
|
+
|
|
49
|
+
bg_attr :event_handler, :required => false
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
def impl_initialize
|
|
53
|
+
|
|
54
|
+
@buffer_size ||= [ @rotate_size, DEFAULT_BUFFER_SIZE ].min
|
|
55
|
+
|
|
56
|
+
if @buffer_size > @rotate_size
|
|
57
|
+
raise ArgumentError,
|
|
58
|
+
"Buffer size #@buffer_size > rotate size #@rotate_size"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@conv = Io::BinaryConverter.new( :order => Io::ORDER_LITTLE_ENDIAN )
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
public
|
|
65
|
+
def closed?
|
|
66
|
+
@closed
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
def send_event( ev, *argv )
|
|
71
|
+
|
|
72
|
+
if @event_handler && @event_handler.respond_to?( ev )
|
|
73
|
+
@event_handler.send( ev, *argv )
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
def new_string_io
|
|
79
|
+
RubyVersions.when_19x( StringIO.new ) do |io|
|
|
80
|
+
io.set_encoding( Encoding::BINARY )
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
def reset_bin_writer
|
|
86
|
+
|
|
87
|
+
@bin =
|
|
88
|
+
Io::BinaryWriter.new(
|
|
89
|
+
:io => StringIO.new( @buf ),
|
|
90
|
+
:order => Io::ORDER_LITTLE_ENDIAN
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
def write_file_header
|
|
96
|
+
|
|
97
|
+
@bin.write_full( FILE_MAGIC )
|
|
98
|
+
@bin.write_utf8( FILE_VERSION )
|
|
99
|
+
|
|
100
|
+
if ( hdr_len = @bin.pos ) >= ( rs = @rotate_size.bytes )
|
|
101
|
+
|
|
102
|
+
@closed = true
|
|
103
|
+
|
|
104
|
+
raise WriteError,
|
|
105
|
+
"File header length #{hdr_len} >= rotate size #{rs}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
@buf_remain -= @bin.pos
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
def update_remain_counts( file_remain )
|
|
113
|
+
|
|
114
|
+
@file_remain = file_remain
|
|
115
|
+
@buf_remain = [ @file_remain, @buffer_size.bytes ].min
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
def ensure_io
|
|
120
|
+
|
|
121
|
+
@buf ||= ( "\x00" * @buffer_size.bytes )
|
|
122
|
+
|
|
123
|
+
unless @io
|
|
124
|
+
|
|
125
|
+
open_res = @file_factory.open_file
|
|
126
|
+
@io = open_res.io
|
|
127
|
+
|
|
128
|
+
update_remain_counts( @rotate_size.bytes - open_res.pos )
|
|
129
|
+
reset_bin_writer
|
|
130
|
+
write_file_header unless open_res.is_reopen
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
def close_file
|
|
136
|
+
|
|
137
|
+
if @io
|
|
138
|
+
|
|
139
|
+
@file_factory.close_file( @io )
|
|
140
|
+
@buf = @bin.io.string
|
|
141
|
+
@io, @bin, @file_remain, @buf_remain = nil, nil, -1, -1
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
def impl_flush
|
|
147
|
+
|
|
148
|
+
completes_file = @file_remain <= @buffer_size.bytes
|
|
149
|
+
|
|
150
|
+
@buf.slice!( @bin.io.pos, @buf.size )
|
|
151
|
+
|
|
152
|
+
@io.write( @buf )
|
|
153
|
+
send_event( :wrote_buffer, @buf.size )
|
|
154
|
+
|
|
155
|
+
# Order matters: need to use @buf before reset_bin_writer
|
|
156
|
+
update_remain_counts( @file_remain - @buf.size )
|
|
157
|
+
reset_bin_writer
|
|
158
|
+
|
|
159
|
+
close_file if completes_file || @closed
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
def write_ev_header( str_io, dest )
|
|
164
|
+
dest.write( @conv.write_int32( str_io.size ) )
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
def write_serialized_event( str_io, dest )
|
|
169
|
+
|
|
170
|
+
write_ev_header( str_io, dest )
|
|
171
|
+
dest.write( str_io.string[ 0, str_io.size ] )
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
def write_overflow( str_io, io_sz )
|
|
176
|
+
|
|
177
|
+
impl_flush
|
|
178
|
+
|
|
179
|
+
if io_sz > @buffer_size.bytes
|
|
180
|
+
|
|
181
|
+
if io_sz > @rotate_size.bytes
|
|
182
|
+
raise WriteError,
|
|
183
|
+
"Record is too large for rotate size #@rotate_size"
|
|
184
|
+
else
|
|
185
|
+
write_serialized_event( str_io, @io )
|
|
186
|
+
end
|
|
187
|
+
else
|
|
188
|
+
impl_write_event( str_io, io_sz )
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
private
|
|
193
|
+
def impl_write_event( str_io, io_sz )
|
|
194
|
+
|
|
195
|
+
ensure_io
|
|
196
|
+
|
|
197
|
+
if io_sz > @buf_remain
|
|
198
|
+
write_overflow( str_io, io_sz )
|
|
199
|
+
else
|
|
200
|
+
write_serialized_event( str_io, @bin )
|
|
201
|
+
if ( @buf_remain -= io_sz ) == 0 then impl_flush end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
public
|
|
206
|
+
def write_event( ev )
|
|
207
|
+
|
|
208
|
+
raise ClosedError if @closed
|
|
209
|
+
|
|
210
|
+
str_io = new_string_io
|
|
211
|
+
@codec.encode_event( ev, str_io )
|
|
212
|
+
|
|
213
|
+
io_sz = str_io.pos + HEADER_SIZE
|
|
214
|
+
|
|
215
|
+
impl_write_event( str_io, io_sz )
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
public
|
|
219
|
+
def close
|
|
220
|
+
|
|
221
|
+
@closed = true
|
|
222
|
+
impl_flush if @io
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
class EventFileLogger < BitGirderClass
|
|
227
|
+
|
|
228
|
+
require 'thread'
|
|
229
|
+
|
|
230
|
+
bg_attr :writer
|
|
231
|
+
|
|
232
|
+
@@shutdown_sentinel = Object.new
|
|
233
|
+
|
|
234
|
+
private
|
|
235
|
+
def impl_initialize
|
|
236
|
+
@queue = Queue.new
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
private
|
|
240
|
+
def process_queue
|
|
241
|
+
|
|
242
|
+
until ( ev = @queue.pop ) == @@shutdown_sentinel
|
|
243
|
+
@writer.write_event( ev )
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
@writer.close
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
public
|
|
250
|
+
def start
|
|
251
|
+
@worker = Thread.start { process_queue }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
public
|
|
255
|
+
def event_logged( ev )
|
|
256
|
+
@queue << ev
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
public
|
|
260
|
+
def shutdown
|
|
261
|
+
|
|
262
|
+
@queue << @@shutdown_sentinel
|
|
263
|
+
@worker.join
|
|
264
|
+
|
|
265
|
+
nil
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def self.start( *argv )
|
|
269
|
+
self.new( *argv ).tap { |l| l.start }
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
class EventFileExistsError < StandardError; end
|
|
274
|
+
|
|
275
|
+
class NoOpCodec < BitGirderClass
|
|
276
|
+
|
|
277
|
+
public
|
|
278
|
+
def encode_event( io ); end
|
|
279
|
+
|
|
280
|
+
public
|
|
281
|
+
def decode_event( io, len )
|
|
282
|
+
Io.read_full( io, len )
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
class PathGenerator < BitGirderClass
|
|
287
|
+
|
|
288
|
+
bg_attr :format
|
|
289
|
+
|
|
290
|
+
public
|
|
291
|
+
def generate( t = Time.now )
|
|
292
|
+
|
|
293
|
+
millis = ( t.to_f * 1000 ).to_i
|
|
294
|
+
millis_hex = sprintf( "%016x", millis )
|
|
295
|
+
|
|
296
|
+
eval( @format, binding )
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
map_instance_of( String ) { |s| self.new( :format => s ) }
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
class EventFileFactory < BitGirderClass
|
|
303
|
+
|
|
304
|
+
bg_attr :dir
|
|
305
|
+
|
|
306
|
+
bg_attr :path_generator
|
|
307
|
+
|
|
308
|
+
bg_attr :event_handler, :required => false
|
|
309
|
+
|
|
310
|
+
# We can find a way to let callers customize this later with a block or
|
|
311
|
+
# regex as needed
|
|
312
|
+
private
|
|
313
|
+
def find_reopen_target
|
|
314
|
+
Dir.glob( "#@dir/**/*" ).max
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
private
|
|
318
|
+
def reopen_target_corrupt( f, e )
|
|
319
|
+
|
|
320
|
+
if ( eh = @event_handler ) && eh.respond_to?( :reopen_target_corrupt )
|
|
321
|
+
eh.send( :reopen_target_corrupt, f, e )
|
|
322
|
+
else
|
|
323
|
+
warn( e, "Reopen target #{f} was invalid; skipping to next file" )
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
private
|
|
328
|
+
def init_reopen_target( f )
|
|
329
|
+
|
|
330
|
+
::File.open( f, "r+b" ) do |io|
|
|
331
|
+
|
|
332
|
+
rd = EventFileReader.new( :codec => NoOpCodec.new, :io => io )
|
|
333
|
+
|
|
334
|
+
trunc_at = 0
|
|
335
|
+
|
|
336
|
+
until io.eof? || f == nil do
|
|
337
|
+
begin
|
|
338
|
+
rd.read_event
|
|
339
|
+
trunc_at = io.pos
|
|
340
|
+
rescue FileFormatError => e
|
|
341
|
+
reopen_target_corrupt( f, e )
|
|
342
|
+
f = nil
|
|
343
|
+
rescue EOFError
|
|
344
|
+
io.truncate( trunc_at )
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
f
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
private
|
|
353
|
+
def init_reopen
|
|
354
|
+
|
|
355
|
+
if @reopen_targ = find_reopen_target
|
|
356
|
+
@reopen_targ = init_reopen_target( @reopen_targ )
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
private
|
|
361
|
+
def gen_path
|
|
362
|
+
|
|
363
|
+
base = case pg = @path_generator
|
|
364
|
+
when Proc then pg.call
|
|
365
|
+
when PathGenerator then pg.generate
|
|
366
|
+
else raise TypeError, "Unhandled path generator: #{pg.class}"
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
"#@dir/#{base}"
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
public
|
|
373
|
+
def open_file
|
|
374
|
+
|
|
375
|
+
if @reopen_targ
|
|
376
|
+
|
|
377
|
+
io = ::File.open( @reopen_targ, "a+b" )
|
|
378
|
+
@reopen_targ = nil
|
|
379
|
+
|
|
380
|
+
OpenResult.new(
|
|
381
|
+
:io => io,
|
|
382
|
+
:is_reopen => true,
|
|
383
|
+
:pos => Io.fsize( io )
|
|
384
|
+
)
|
|
385
|
+
else
|
|
386
|
+
if ::File.exist?( path = gen_path )
|
|
387
|
+
raise EventFileExistsError, "File already exists: #{path}"
|
|
388
|
+
else
|
|
389
|
+
OpenResult.new( :io => ::File.open( path, "wb" ) )
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
public
|
|
395
|
+
def close_file( io )
|
|
396
|
+
io.close
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def self.open( opts )
|
|
400
|
+
self.new( opts ).tap { |ff| ff.send( :init_reopen ) }
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
class FileFormatError < StandardError; end
|
|
405
|
+
|
|
406
|
+
class FileVersionError < FileFormatError; end
|
|
407
|
+
|
|
408
|
+
class FileMagicError < FileFormatError; end
|
|
409
|
+
|
|
410
|
+
class EventFileReader < EventFileIo
|
|
411
|
+
|
|
412
|
+
include Enumerable
|
|
413
|
+
|
|
414
|
+
bg_attr :io
|
|
415
|
+
|
|
416
|
+
private
|
|
417
|
+
def impl_initialize
|
|
418
|
+
|
|
419
|
+
super
|
|
420
|
+
|
|
421
|
+
@bin = Io::BinaryReader.new(
|
|
422
|
+
:io => @io,
|
|
423
|
+
:order => Io::ORDER_LITTLE_ENDIAN
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
@read_file_header = false
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
private
|
|
430
|
+
def read_file_header
|
|
431
|
+
|
|
432
|
+
begin
|
|
433
|
+
unless ( magic = @bin.read_full( 8 ) ) == FILE_MAGIC
|
|
434
|
+
raise FileMagicError,
|
|
435
|
+
"Unrecognized file magic: #{magic.inspect}"
|
|
436
|
+
end
|
|
437
|
+
rescue EOFError
|
|
438
|
+
raise FileMagicError, "Missing or incomplete file magic"
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
unless ( ver_str = @bin.read_utf8 ) == FILE_VERSION
|
|
442
|
+
raise FileVersionError,
|
|
443
|
+
"Unrecognized file version: #{ver_str.inspect}"
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
@read_file_header = true
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# Returns the next [ ev, loc ] pair if there is one, EOFException if
|
|
450
|
+
# incomplete, and nil if this is the first read of a file which contains
|
|
451
|
+
# only a valid header.
|
|
452
|
+
public
|
|
453
|
+
def read_event
|
|
454
|
+
|
|
455
|
+
read_file_header unless @read_file_header
|
|
456
|
+
return nil if @io.eof?
|
|
457
|
+
|
|
458
|
+
loc = @bin.pos
|
|
459
|
+
|
|
460
|
+
len = @bin.read_int32
|
|
461
|
+
buf = @bin.read_full( len )
|
|
462
|
+
|
|
463
|
+
ev = @codec.decode_event( StringIO.new( buf, "r" ), len )
|
|
464
|
+
|
|
465
|
+
[ ev, loc ]
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
public
|
|
469
|
+
def each_with_loc
|
|
470
|
+
|
|
471
|
+
until @io.eof?
|
|
472
|
+
ev, loc = *( read_event )
|
|
473
|
+
yield( ev, loc ) if loc
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
public
|
|
478
|
+
def each
|
|
479
|
+
each_with_loc { |ev, loc| yield( ev ) }
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
require 'bitgirder/event/logger'
|
|
2
|
+
require 'bitgirder/event/testing'
|
|
3
|
+
|
|
4
|
+
require 'bitgirder/core'
|
|
5
|
+
include BitGirder::Core
|
|
6
|
+
|
|
7
|
+
require 'bitgirder/testing'
|
|
8
|
+
|
|
9
|
+
require 'thread'
|
|
10
|
+
|
|
11
|
+
module BitGirder
|
|
12
|
+
module Event
|
|
13
|
+
module Logger
|
|
14
|
+
|
|
15
|
+
# Provides helpers for testing event flow in an application. In addition to
|
|
16
|
+
# testing the basic operation of an event listener (does it serialize correctly,
|
|
17
|
+
# filter appropriately, etc), thoroughly tested applications will want to add
|
|
18
|
+
# coverge of specific events.
|
|
19
|
+
#
|
|
20
|
+
# For example, suppose an application expects to log an event upon every login
|
|
21
|
+
# attempt, including the user id, login completion time, and result (_success_,
|
|
22
|
+
# <i>unknown user</i>, <i>bad password</i>, etc). This data might be used in
|
|
23
|
+
# realtime to look for ongoing attacks on an account, or over time to track user
|
|
24
|
+
# engagement. In any event, the events are an important part of the application,
|
|
25
|
+
# but an inadvertent change in the login handling code might cause logins to not
|
|
26
|
+
# be logged. This module helps make it easy to include assertions about event
|
|
27
|
+
# delivery right alongside other assertions (that a login succeeded or failed as
|
|
28
|
+
# expected).
|
|
29
|
+
#
|
|
30
|
+
module Testing
|
|
31
|
+
|
|
32
|
+
class CodecRoundtripper < BitGirderClass
|
|
33
|
+
|
|
34
|
+
bg_attr :codec
|
|
35
|
+
bg_attr :listener, :required => false
|
|
36
|
+
|
|
37
|
+
public
|
|
38
|
+
def event_logged( ev )
|
|
39
|
+
|
|
40
|
+
ev = BitGirder::Event::Testing.roundtrip( ev, @codec )
|
|
41
|
+
@listener.event_logged( ev ) if @listener
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# A simple event listener (see BitGirder::EventLogger) which accumulates all
|
|
46
|
+
# events into an unbounded list. Test classses should make sure to remove this
|
|
47
|
+
# listener from its associated engine after assertions are complete, either
|
|
48
|
+
# explicitly or via ensure_removed, lest it continue to amass large amounts of
|
|
49
|
+
# unneeded events.
|
|
50
|
+
class EventAccumulator < BitGirderClass
|
|
51
|
+
|
|
52
|
+
include BitGirder::Testing::AssertMethods
|
|
53
|
+
|
|
54
|
+
bg_attr :engine
|
|
55
|
+
|
|
56
|
+
# Creates a new instance associated with the given engine
|
|
57
|
+
def impl_initialize
|
|
58
|
+
|
|
59
|
+
@mut = Mutex.new
|
|
60
|
+
@events = []
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Adds +ev+ to this instance's event list.
|
|
64
|
+
public
|
|
65
|
+
def event_logged( ev )
|
|
66
|
+
@mut.synchronize { @events << ev }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Gets a snapshot of the events accumulated so far.
|
|
70
|
+
public
|
|
71
|
+
def events
|
|
72
|
+
@mut.synchronize { Array.new( @events ) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
public
|
|
76
|
+
def assert_logged( expct = nil, &blk )
|
|
77
|
+
|
|
78
|
+
if expct
|
|
79
|
+
if blk
|
|
80
|
+
raise "Illegal combination of expect val and block"
|
|
81
|
+
else
|
|
82
|
+
blk = lambda { |ev| ev == expct }
|
|
83
|
+
end
|
|
84
|
+
else
|
|
85
|
+
raise "Block missing" unless blk
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
assert events.find( &blk ), "Block did not match any events"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Executes an arbitrary block, ensuring that this instance is removed from
|
|
92
|
+
# the engine with which it is associated whether or not the block completes
|
|
93
|
+
# normally. This method returns the block's result or raises its exception.
|
|
94
|
+
#
|
|
95
|
+
# This instance is itself provided to the block, enabling (in conjunction
|
|
96
|
+
# with EventAccumulator.create()) test code to easily and reliably wrap an
|
|
97
|
+
# entire test:
|
|
98
|
+
#
|
|
99
|
+
# def test_login_success
|
|
100
|
+
#
|
|
101
|
+
# EventAccumulator.create( ev_eng ).ensure_removed do |acc|
|
|
102
|
+
#
|
|
103
|
+
# # Do a login and first check that it succeeds as wanted
|
|
104
|
+
# login_res = do_login( "ezra", "somepass" )
|
|
105
|
+
# assert( login_res.ok? )
|
|
106
|
+
#
|
|
107
|
+
# # Now also check that login event was generated
|
|
108
|
+
# ev_expct = { :event => :login, :user => "ezra", :result => :ok }
|
|
109
|
+
# acc.assert_logged { |ev| ev == ev_expct }
|
|
110
|
+
#
|
|
111
|
+
# ... # Possibly more test code, cleanup, etc
|
|
112
|
+
# end
|
|
113
|
+
# end
|
|
114
|
+
#
|
|
115
|
+
public
|
|
116
|
+
def ensure_removed
|
|
117
|
+
begin
|
|
118
|
+
yield( self )
|
|
119
|
+
ensure
|
|
120
|
+
@engine.remove_listener( self )
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Convenience method to create, register, and return an instance which
|
|
125
|
+
# accumulates events from the given EventLogger::Engine
|
|
126
|
+
def self.create( *argv )
|
|
127
|
+
self.new( *argv ).tap { |acc| acc.engine.add_listener( acc ) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.while_accumulating( *argv )
|
|
131
|
+
|
|
132
|
+
acc = self.create( *argv )
|
|
133
|
+
acc.ensure_removed { yield( acc ) }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|