ls4 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. data/AUTHORS +1 -0
  2. data/COPYING +661 -0
  3. data/ChangeLog +9 -0
  4. data/NOTICE +8 -0
  5. data/README.rdoc +61 -0
  6. data/bin/ls4-cs +3 -0
  7. data/bin/ls4-ds +3 -0
  8. data/bin/ls4-gw +3 -0
  9. data/bin/ls4-standalone +3 -0
  10. data/bin/ls4cmd +3 -0
  11. data/bin/ls4ctl +3 -0
  12. data/bin/ls4rpc +3 -0
  13. data/bin/ls4stat +3 -0
  14. data/bin/ls4top +3 -0
  15. data/lib/ls4/command/cmd.rb +241 -0
  16. data/lib/ls4/command/cs.rb +190 -0
  17. data/lib/ls4/command/ctl.rb +278 -0
  18. data/lib/ls4/command/ds.rb +335 -0
  19. data/lib/ls4/command/gw.rb +256 -0
  20. data/lib/ls4/command/rpc.rb +172 -0
  21. data/lib/ls4/command/standalone.rb +318 -0
  22. data/lib/ls4/command/stat.rb +244 -0
  23. data/lib/ls4/command/top.rb +291 -0
  24. data/lib/ls4/default.rb +26 -0
  25. data/lib/ls4/lib/cclog.rb +220 -0
  26. data/lib/ls4/lib/ebus.rb +553 -0
  27. data/lib/ls4/lib/vbcode.rb +228 -0
  28. data/lib/ls4/logic/fault_detector.rb +212 -0
  29. data/lib/ls4/logic/membership.rb +253 -0
  30. data/lib/ls4/logic/node.rb +66 -0
  31. data/lib/ls4/logic/okey.rb +45 -0
  32. data/lib/ls4/logic/tsv_data.rb +81 -0
  33. data/lib/ls4/logic/weight.rb +166 -0
  34. data/lib/ls4/service/balance.rb +62 -0
  35. data/lib/ls4/service/base.rb +29 -0
  36. data/lib/ls4/service/bus.rb +37 -0
  37. data/lib/ls4/service/config.rb +63 -0
  38. data/lib/ls4/service/config_cs.rb +33 -0
  39. data/lib/ls4/service/config_ds.rb +56 -0
  40. data/lib/ls4/service/config_gw.rb +42 -0
  41. data/lib/ls4/service/data_client.rb +122 -0
  42. data/lib/ls4/service/data_server.rb +168 -0
  43. data/lib/ls4/service/data_server_url.rb +83 -0
  44. data/lib/ls4/service/gateway.rb +375 -0
  45. data/lib/ls4/service/gateway_ro.rb +91 -0
  46. data/lib/ls4/service/gw_http.rb +821 -0
  47. data/lib/ls4/service/heartbeat.rb +182 -0
  48. data/lib/ls4/service/log.rb +81 -0
  49. data/lib/ls4/service/master_select.rb +148 -0
  50. data/lib/ls4/service/mds.rb +292 -0
  51. data/lib/ls4/service/mds_cache.rb +294 -0
  52. data/lib/ls4/service/mds_cache_mem.rb +63 -0
  53. data/lib/ls4/service/mds_cache_memcached.rb +65 -0
  54. data/lib/ls4/service/mds_ha.rb +176 -0
  55. data/lib/ls4/service/mds_memcache.rb +209 -0
  56. data/lib/ls4/service/mds_tc.rb +508 -0
  57. data/lib/ls4/service/mds_tt.rb +472 -0
  58. data/lib/ls4/service/membership.rb +331 -0
  59. data/lib/ls4/service/process.rb +90 -0
  60. data/lib/ls4/service/rpc.rb +50 -0
  61. data/lib/ls4/service/rpc_cs.rb +101 -0
  62. data/lib/ls4/service/rpc_ds.rb +96 -0
  63. data/lib/ls4/service/rpc_gw.rb +255 -0
  64. data/lib/ls4/service/rts.rb +94 -0
  65. data/lib/ls4/service/rts_file.rb +76 -0
  66. data/lib/ls4/service/rts_memory.rb +55 -0
  67. data/lib/ls4/service/slave.rb +132 -0
  68. data/lib/ls4/service/stat.rb +91 -0
  69. data/lib/ls4/service/stat_cs.rb +25 -0
  70. data/lib/ls4/service/stat_ds.rb +40 -0
  71. data/lib/ls4/service/stat_gw.rb +25 -0
  72. data/lib/ls4/service/storage.rb +116 -0
  73. data/lib/ls4/service/storage_dir.rb +201 -0
  74. data/lib/ls4/service/sync.rb +206 -0
  75. data/lib/ls4/service/time_check.rb +80 -0
  76. data/lib/ls4/service/ulog.rb +159 -0
  77. data/lib/ls4/service/ulog_file.rb +398 -0
  78. data/lib/ls4/service/ulog_memory.rb +53 -0
  79. data/lib/ls4/service/weight.rb +134 -0
  80. data/lib/ls4/version.rb +5 -0
  81. data/test/01_add_get_remove.rt +84 -0
  82. data/test/02_read.rt +61 -0
  83. data/test/03_getd_readd.rt +69 -0
  84. data/test/04_version_time.rt +170 -0
  85. data/test/05_version_name.rt +161 -0
  86. data/test/06_http_get_set_remove_1.rt +119 -0
  87. data/test/07_http_get_set_remove_2.rt +116 -0
  88. data/test/08_read_only_time.rt +177 -0
  89. data/test/09_read_only_name.rt +173 -0
  90. data/test/10_http_get_set_remove_3.rt +73 -0
  91. data/test/11_mds_cache_memcached.rt +88 -0
  92. data/test/12_mds_cache_local_memory.rt +86 -0
  93. data/test/13_memcache_mds.rt +84 -0
  94. data/test/14_delete.rt +63 -0
  95. data/test/15_standalone.rt +71 -0
  96. data/test/chukan.rb +516 -0
  97. data/test/common.rb +250 -0
  98. data/test/load_test.rb +79 -0
  99. data/test/load_test_offload.rb +86 -0
  100. metadata +295 -0
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH << File.dirname(__FILE__)
3
+ require 'common'
4
+
5
+ LOOP = (ARGV[0] || ENV["LOOP"] || (ENV["HEAVY"] ? 20 : 3)).to_i
6
+ SIZE = (ARGV[1] || 10).to_i
7
+ NUM = (ARGV[2] || 50).to_i
8
+
9
+ mds = start_mds
10
+ cs = start_cs
11
+ ds0 = start_ds(0, 0)
12
+ ds1 = start_ds(1, 0)
13
+ ds2 = start_ds(2, 1)
14
+ ds3 = start_ds(3, 1)
15
+
16
+ cs.show_nodes
17
+ cs.show_version
18
+
19
+ gw = start_gw
20
+
21
+ pid = Process.pid
22
+ keyf = "#{pid}-key%d"
23
+ _data = "@"*SIZE
24
+
25
+ test "run normally" do
26
+ c = gw.client
27
+
28
+ LOOP.times {|o|
29
+ NUM.times do |i|
30
+ key = keyf % i
31
+ _attrs = {"loop"=>o.to_s, "attr#{i}"=>i.to_s}
32
+
33
+ test 'add' do
34
+ c.call(:add, key, _data, _attrs)
35
+ end
36
+ end
37
+
38
+ NUM.times do |i|
39
+ key = keyf % i
40
+
41
+ test "delete" do
42
+ deleted = c.call(:delete, key)
43
+ test_equals true, deleted, 'deleted == true'
44
+ end
45
+ end
46
+
47
+ NUM.times do |i|
48
+ key = keyf % i
49
+
50
+ test 'deleted get' do
51
+ data, attrs = c.call(:get, key)
52
+ test_equals nil, data, 'get_data _data is deleted'
53
+ test_equals nil, attrs, 'get_attrs _attrs is deleted'
54
+ end
55
+ end
56
+ }
57
+ end
58
+
59
+ cs.show_items
60
+ cs.show_stat
61
+
62
+ term_all(ds0, ds1, ds2, ds3, gw, mds, cs)
63
+
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH << File.dirname(__FILE__)
3
+ require 'common'
4
+
5
+ LOOP = (ARGV[0] || ENV["LOOP"] || (ENV["HEAVY"] ? 20 : 3)).to_i
6
+ SIZE = (ARGV[1] || 10).to_i
7
+ NUM = (ARGV[2] || 50).to_i
8
+
9
+ standalone = start_standalone
10
+
11
+ pid = Process.pid
12
+ keyf = "#{pid}-key%d"
13
+ _data = "@"*SIZE
14
+
15
+ test "run normally" do
16
+ c = standalone.client
17
+
18
+ LOOP.times {|o|
19
+ NUM.times do |i|
20
+ key = keyf % i
21
+ _attrs = {"loop"=>o.to_s, "attr#{i}"=>i.to_s}
22
+
23
+ test 'add' do
24
+ c.call(:add, key, _data, _attrs)
25
+ end
26
+ end
27
+
28
+ NUM.times do |i|
29
+ key = keyf % i
30
+ _attrs = {"loop"=>o.to_s, "attr#{i}"=>i.to_s}
31
+
32
+ test 'get' do
33
+ data, attrs = c.call(:get, key)
34
+ test_equals _data, data, 'get _data == data'
35
+ test_equals _attrs, attrs, 'get _attrs == attrs'
36
+ end
37
+
38
+ test 'get_data' do
39
+ data = c.call(:get_data, key)
40
+ test_equals _data, data, 'get_data _data == data'
41
+ end
42
+
43
+ test 'get_attrs' do
44
+ attrs = c.call(:get_attrs, key)
45
+ test_equals _attrs, attrs, 'get_attrs _attrs == attrs'
46
+ end
47
+ end
48
+
49
+ NUM.times do |i|
50
+ key = keyf % i
51
+
52
+ test "remove" do
53
+ removed = c.call(:remove, key)
54
+ test_equals true, removed, 'removed == true'
55
+ end
56
+ end
57
+
58
+ NUM.times do |i|
59
+ key = keyf % i
60
+
61
+ test 'removed get' do
62
+ data, attrs = c.call(:get, key)
63
+ test_equals nil, data, 'get_data _data is removed'
64
+ test_equals nil, attrs, 'get_attrs _attrs is removed'
65
+ end
66
+ end
67
+ }
68
+ end
69
+
70
+ term_all(standalone)
71
+
@@ -0,0 +1,516 @@
1
+ #
2
+ # Chukan automation library for distributed systems
3
+ #
4
+ # Copyright (c) 2009 FURUHASHI Sadayuki
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+ #
24
+
25
+
26
+ ####
27
+ ## Basic usage
28
+ ##
29
+ =begin
30
+ #!/usr/bin/env ruby
31
+ require 'chukan'
32
+ include Chukan # include Chukan
33
+
34
+ srv = spawn("server -arg1 -arg2") # run 'server' command
35
+ # with '-arg1 -arg2' arguments
36
+ srv.stdout_join("started") # wait until the server outputs "started"
37
+
38
+ cli = spawn("client -arg1 -arg2") # run 'client' command with some arguments
39
+ srv.stdout_join("connected") # wait until the server outputs "connected"
40
+
41
+ cli.kill # send SIGKILL signal to the client
42
+ cli.join # wait until the client is really dead
43
+ srv.stderr_join(/disconnected/) # stderr and regexp are also available
44
+
45
+ srv.stdin.write "status\n" # input "status\n" to the server
46
+ srv.stdout_join("done") # wait until the server outputs "done"
47
+
48
+ if srv.stdout.read =~ /^client:/ # read output of the server
49
+ puts "** TEST FAILED **" # this library is usable for tests
50
+ # see also "Unit test" example below
51
+ end
52
+ =end
53
+
54
+
55
+ ####
56
+ ## Remote process execution
57
+ ##
58
+ =begin
59
+ #!/usr/bin/env ruby
60
+ require 'chukan'
61
+ include Chukan # include Chukan
62
+
63
+ mac = remote("mymac.local") # login to the remote host using ssh and run
64
+ # commands on the host
65
+ # use ssh-agent if your key is encrypted
66
+ mac.cd("work/myproject") # run on "work/myproject" directory
67
+
68
+ linux = remote("192.168.10.2", "myname", ".id_rsa_linux")
69
+ # user name and path of the key is optional
70
+
71
+ cli_on_mac = mac.spawn("client -arg1") # run 'client' on the remote host
72
+ cli_on_linux = linux.spawn("client -arg1")
73
+
74
+ cli_on_mac.stdout_join("started") # signals and I/Os are also available
75
+ =end
76
+
77
+
78
+ ####
79
+ ## Unit test
80
+ ##
81
+ =begin
82
+ #!/usr/bin/env ruby
83
+ require 'chukan'
84
+ include Chukan::Test # include Chukan::Test
85
+
86
+ test "load mylibrary" do # Chukan::Test provides 'test' and 'run' methods
87
+ require "mylibrary" # test will fail if the block returns nil or false,
88
+ # or an exception is raised
89
+ end
90
+
91
+ run {|b| # 'run' iterates YAML documents written after
92
+ # __END__ line
93
+ test "score <= 100", :TODO do # second argument of 'test' is :TODO or :SKIP
94
+ b.score <= 100 # which is useful for Test Anything Protocol
95
+ end # (TAP) processor like 'prove'
96
+ }
97
+
98
+ __END__
99
+ --- # YAML documents are here
100
+ name: test A
101
+ user: a-san
102
+ score: 10
103
+ ---
104
+ name: test B
105
+ user: b-san
106
+ score: 100
107
+ =end
108
+
109
+
110
+ require 'stringio'
111
+ require 'strscan'
112
+ require 'monitor'
113
+ require 'fcntl'
114
+
115
+
116
+ module Chukan
117
+ IO_BUFFER_LIMIT = 1024*1024
118
+
119
+ class LocalProcess
120
+ def initialize(*cmdline)
121
+ @cmdline = cmdline.map {|x| x.to_s }
122
+ @status = nil
123
+ @shortname = cmdline.join(' ').split(/\s/)
124
+ @shortname[0] = File.basename(@shortname[0])
125
+ @shortname = @shortname.join(' ')[0, 12]
126
+ start
127
+ end
128
+
129
+ attr_reader :cmdline
130
+ attr_reader :stdin, :stdout, :stderr
131
+ attr_reader :pid
132
+ attr_reader :status
133
+
134
+ def join
135
+ @status = Process.waitpid2(@pid)[1]
136
+ @stdout_reader.join
137
+ @stderr_reader.join
138
+ @killer.killed
139
+ reason = @status.inspect
140
+ if m = reason.match(/\,([^\>]*)\>/)
141
+ reason = m[1]
142
+ end
143
+ $stderr.puts "#{@msg_prefix}#{reason}"
144
+ @status
145
+ end
146
+
147
+ def stdout_join(pattern, &block)
148
+ io_join(@stdout, pattern, &block)
149
+ end
150
+
151
+ def stderr_join(pattern, &block)
152
+ io_join(@stderr, pattern, &block)
153
+ end
154
+
155
+ def signal(sig)
156
+ Process.kill(sig, @pid) rescue nil
157
+ self
158
+ end
159
+
160
+ def kill
161
+ signal(:SIGKILL)
162
+ end
163
+
164
+ def term
165
+ signal(:SIGTERM)
166
+ end
167
+
168
+ def hup
169
+ signal(:SIGHUP)
170
+ end
171
+
172
+ def set_display(shortname)
173
+ if shortname.length <= 12
174
+ @msg_prefix[0..-1] = "[%-12s %6d] " % [shortname, @pid]
175
+ elsif shortname.length < 19
176
+ @msg_prefix[0..-1] = "[%-19s] " % [shortname]
177
+ else
178
+ @msg_prefix[0..-1] = "[#{shortname}] "
179
+ end
180
+ self
181
+ end
182
+
183
+ private
184
+ def io_join(io, pattern, &block)
185
+ if pattern.is_a?(String)
186
+ pattern = Regexp.new(Regexp.escape(pattern))
187
+ end
188
+ if block
189
+ io.synchronize {
190
+ io.read
191
+ }
192
+ yield
193
+ end
194
+ match = nil
195
+ io.synchronize {
196
+ until match = io.scanner.scan_until(pattern)
197
+ if io.closed_write?
198
+ raise EOFError.new("io closed: #{pattern.inspect}")
199
+ end
200
+ io.cond.wait
201
+ end
202
+ }
203
+ match
204
+ end
205
+
206
+ private
207
+ def start
208
+ stdin, @stdin = IO.pipe
209
+ @pout, pout = IO.pipe
210
+ @perr, perr = IO.pipe
211
+ @pid = fork
212
+ unless @pid
213
+ @stdin.close
214
+ @pout.close
215
+ @perr.close
216
+ $stdin.reopen(stdin) rescue nil
217
+ $stdout.reopen(pout) rescue nil
218
+ $stderr.reopen(perr) rescue nil
219
+ exec *cmdline
220
+ exit 127
221
+ end
222
+ stdin.close
223
+ pout.close
224
+ perr.close
225
+ @stdin.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
226
+ @pout.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
227
+ @perr.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
228
+
229
+ @msg_prefix = "[%-12s %6d] " % [@shortname, @pid]
230
+ $stdout.puts "#{@msg_prefix}#{@cmdline.join(' ')}"
231
+
232
+ @stdout, @stdout_reader = self.class.start_scan(@pout, $stdout, @msg_prefix)
233
+ @stderr, @stderr_reader = self.class.start_scan(@perr, $stderr, @msg_prefix)
234
+
235
+ @killer = ZombieKiller.define_finalizer(self, @pid)
236
+ end
237
+
238
+ def self.start_scan(pipe, out, msg_prefix)
239
+ io = StringIO.new
240
+ io.extend(MonitorMixin)
241
+ cond = io.new_cond
242
+ scanner = StringScanner.new(io.string)
243
+ (class<<io; self; end).instance_eval do
244
+ define_method(:cond) { cond }
245
+ define_method(:scanner) { scanner }
246
+ end
247
+
248
+ reader = Thread.start(pipe, io,
249
+ out, msg_prefix,
250
+ &method(:reader_thread))
251
+
252
+ return io, reader
253
+ end
254
+
255
+ def self.reader_thread(src, dst, msgout, msg_prefix)
256
+ buf = ""
257
+ line = ''
258
+ begin
259
+ while src.sysread(1024, buf)
260
+ dst.synchronize {
261
+ dst.string << buf
262
+ if dst.string.size > IO_BUFFER_LIMIT
263
+ cut = dst.string.size - IO_BUFFER_LIMIT
264
+ dst.string.slice!(0, cut)
265
+ dst.pos = (dst.pos > cut) ?
266
+ dst.pos - cut : 0
267
+ dst.scanner.pos = (dst.scanner.pos > cut) ?
268
+ dst.scanner.pos - cut : 0
269
+ end
270
+ dst.cond.signal
271
+ }
272
+ line << buf
273
+ line.gsub!(/.*\n/) {|l|
274
+ msgout.puts "#{msg_prefix}#{l}"
275
+ msgout.flush
276
+ ""
277
+ }
278
+ end
279
+ rescue
280
+ nil
281
+ ensure
282
+ src.close
283
+ dst.synchronize {
284
+ dst.close_write
285
+ dst.cond.signal
286
+ }
287
+ unless line.empty?
288
+ msgout.puts "#{msg_prefix}#{line}"
289
+ end
290
+ end
291
+ end
292
+ end
293
+
294
+
295
+ class ZombieKiller
296
+ def initialize(pid)
297
+ @pid = pid
298
+ end
299
+ def killed
300
+ @pid = nil
301
+ end
302
+ attr_reader :pid
303
+
304
+ def self.define_finalizer(obj, pid)
305
+ killer = self.new(pid)
306
+ ObjectSpace.define_finalizer(obj, self.finalizer(killer))
307
+ killer
308
+ end
309
+
310
+ def self.finalizer(killer)
311
+ proc {
312
+ if pid = killer.pid
313
+ [:SIGTERM, :SIGKILL].each {|sig|
314
+ Process.kill(sig, pid)
315
+ break if 10.times {
316
+ begin
317
+ if Process.waitpid(pid, Process::WNOHANG)
318
+ break true
319
+ end
320
+ sleep 0.1
321
+ rescue
322
+ break true
323
+ end
324
+ nil
325
+ }
326
+ }
327
+ end
328
+ }
329
+ end
330
+ end
331
+
332
+
333
+ class RemoteProcess < LocalProcess
334
+ def initialize(remote, *cmdline, &block)
335
+ @remote = remote
336
+
337
+ cmdline_real = ["echo","$$","&&","exec"] + cmdline
338
+ super(*remote.command(*cmdline_real), &block)
339
+
340
+ @shortname = File.basename(cmdline.first.split(/\s/,2).first)[0, 7] +
341
+ "@"+remote.host[0,5]
342
+
343
+ stdout_join("\n")
344
+ @rpid = stdout.gets.to_i
345
+ end
346
+ attr_reader :rpid
347
+
348
+ def signal(sig)
349
+ system(*@remote.command("kill -#{sig} #{@rpid}"))
350
+ self
351
+ end
352
+ end
353
+
354
+
355
+ class Remote
356
+ def initialize(host, user = nil, key = nil)
357
+ @host = host
358
+ @user = user
359
+ @key = key
360
+ @dir = nil
361
+ end
362
+ attr_reader :host
363
+
364
+ def cd(dir = nil)
365
+ @dir = dir
366
+ self
367
+ end
368
+
369
+ def command(*cmdline)
370
+ ssh = ENV["SSH"] || "ssh"
371
+ cmd = [ssh, "-o", "Batchmode yes"]
372
+ cmd += ["-i", @key] if @key
373
+ if @user
374
+ cmd.push "#{@user}:#{@host}"
375
+ else
376
+ cmd.push @host
377
+ end
378
+ if @dir
379
+ cmd += ["cd", @dir, "&&"]
380
+ end
381
+ cmd + cmdline.map {|x| x.to_s }
382
+ end
383
+
384
+ def spawn(*cmdline, &block)
385
+ RemoteProcess.new(self, *cmdline, &block)
386
+ end
387
+ end
388
+
389
+
390
+ def spawn(*cmdline, &block)
391
+ LocalProcess.new(*cmdline, &block)
392
+ end
393
+
394
+ def remote(host, user = nil, key = nil)
395
+ Remote.new(host, user, key)
396
+ end
397
+
398
+
399
+ module Test
400
+ @@start = nil
401
+ @@cases = Hash.new {|hash,key| hash[key] = [0,0,0] }
402
+ @@count = 0
403
+ @@data = nil
404
+
405
+ if ENV["TERM"] =~ /color/i && $stdout.stat.chardev?
406
+ SEPARATOR = ""
407
+ module Color
408
+ SUCCESS = "\e[0;32m"
409
+ FAIL = "\e[1;33m"
410
+ ERROR = "\e[0;31m"
411
+ NORMAL = "\e[00m"
412
+ end
413
+ else
414
+ SEPARATOR = "\n"
415
+ module Color
416
+ SUCCESS = ""
417
+ FAIL = ""
418
+ ERROR = ""
419
+ NORMAL = ""
420
+ end
421
+ end
422
+
423
+ def self.report
424
+ proc {
425
+ finish = Time.now
426
+ puts "\n1..#{@@count}"
427
+ $stderr.puts "Finished in #{finish - @@start} seconds."
428
+ $stderr.puts ""
429
+ succes = @@cases.to_a.inject(0) {|r,(n,c)| r + c[0] }
430
+ failure = @@cases.to_a.inject(0) {|r,(n,c)| r + c[1] }
431
+ error = @@cases.to_a.inject(0) {|r,(n,c)| r + c[2] }
432
+ $stderr.puts "#{@@cases.size} tests, " +
433
+ "#{succes+failure+error} assertions, " +
434
+ "#{Color::FAIL}#{failure} failures, " +
435
+ "#{Color::ERROR}#{error} errors" +
436
+ "#{Color::NORMAL}"
437
+ }
438
+ end
439
+
440
+ def self.included(mod)
441
+ unless @@start
442
+ ObjectSpace.define_finalizer(mod, report)
443
+ @@start = Time.now
444
+ end
445
+ end
446
+
447
+ def test(name = nil, directive = nil, &block)
448
+ yield
449
+ tap(Color::SUCCESS, "ok", @@count+=1, name, directive)
450
+ @@cases[name][0] += 1
451
+ #if yield
452
+ # tap(Color::SUCCESS, "ok", @@count+=1, name, directive)
453
+ # @@cases[name][0] += 1
454
+ #else
455
+ # tap(Color::FAIL, "not ok", @@count+=1, name, directive)
456
+ # print_backtrace(caller, "test failed")
457
+ # @@cases[name][1] += 1
458
+ #end
459
+ rescue Exception
460
+ tap(Color::ERROR, "not ok", @@count+=1, name, directive)
461
+ print_backtrace($!.backtrace, "#{$!} (#{$!.class})")
462
+ @@cases[name][2] += 1
463
+ ensure
464
+ print Color::NORMAL
465
+ end
466
+
467
+ def test_equals(should, actual, name = nil, directive = nil)
468
+ if should == actual
469
+ tap(Color::SUCCESS, "ok", @@count+=1, name, directive)
470
+ @@cases[name][0] += 1
471
+ else
472
+ tap(Color::FAIL, "not ok", @@count+=1, name, directive)
473
+ @@cases[name][1] += 1
474
+ print_backtrace(caller, "test failed: expected #{should.inspect} but got #{actual.inspect}")
475
+ end
476
+ end
477
+
478
+ def data
479
+ require 'yaml'
480
+ @@data ||= YAML.load_stream(DATA.read.gsub(/(^\t+)/) {
481
+ ' ' * $+.length
482
+ }).documents.map {|obj|
483
+ obj.each_pair {|k,v|
484
+ (class<<obj; self; end).instance_eval do
485
+ define_method(k) { v }
486
+ end rescue nil
487
+ } rescue obj
488
+ }
489
+ end
490
+
491
+ def run(&block)
492
+ data.each {|d|
493
+ yield d
494
+ }
495
+ end
496
+
497
+ private
498
+ def tap(color, stat, count, name, directive = nil)
499
+ if directive
500
+ directive = " # #{directive.to_s.upcase}"
501
+ end
502
+ puts "#{color}#{SEPARATOR}#{stat} #{count} - #{name}#{directive}"
503
+ end
504
+
505
+ def print_backtrace(trace, msg)
506
+ $stderr.puts "#{trace.shift}: #{msg}"
507
+ trace.each {|c|
508
+ unless c.to_s.include?(__FILE__)
509
+ $stderr.puts "\tfrom #{c}"
510
+ end
511
+ }
512
+ end
513
+ end
514
+
515
+ end
516
+