ls4 0.9.0

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.
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
+