all_seeing_eye 0.0.1

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.
@@ -0,0 +1,26 @@
1
+ class AllSeeingEye
2
+ def self.configuration(location = nil)
3
+ location ||= 'config'
4
+ @@configuration ||= if defined?(Rails)
5
+ YAML::load_file("#{Rails.root}/#{location}/all_seeing_eye.yml")
6
+ else
7
+ YAML::load_file("./#{location}/all_seeing_eye.yml")
8
+ end
9
+ @@configuration
10
+ end
11
+
12
+ def self.redis(options = {})
13
+ options = {:host => 'localhost', :port => 6379}.merge(options)
14
+ @@redis ||= Redis.new(:host => options[:host], :port => options[:port])
15
+ end
16
+ end
17
+
18
+ require 'redis'
19
+ require 'yaml'
20
+ require 'chronic'
21
+ require 'rbtree'
22
+ require 'all_seeing_eye/model'
23
+ require 'all_seeing_eye/models/request'
24
+ require 'all_seeing_eye/integrations/rails2' if defined?(Rails)
25
+ require 'active_support'
26
+ require 'active_support/all'
data/test/base_test.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'test_helper'
2
+
3
+ context 'with the base class' do
4
+ test 'load the configuration' do
5
+ assert AllSeeingEye.configuration['model']
6
+ end
7
+
8
+ test 'connect to Redis' do
9
+ assert_equal '1', AllSeeingEye.redis.info["connected_clients"]
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ timeout: 0.01
2
+ model:
3
+ ip:
4
+ object: request
5
+ method: .remote_ip
6
+ uri:
7
+ object: request
8
+ method: .request_uri
9
+ browser:
10
+ object: request
11
+ method: .env["HTTP_USER_AGENT"]
12
+ controller:
13
+ object: params
14
+ method: '[:controller]'
15
+ params:
16
+ action:
17
+ object: params
18
+ method: '[:action]'
19
+ status:
20
+ object: response
21
+ method: .status
@@ -0,0 +1,115 @@
1
+ # Redis configuration file example
2
+
3
+ # By default Redis does not run as a daemon. Use 'yes' if you need it.
4
+ # Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
5
+ daemonize yes
6
+
7
+ # When run as a daemon, Redis write a pid file in /var/run/redis.pid by default.
8
+ # You can specify a custom pid file location here.
9
+ pidfile ./test/redis-test.pid
10
+
11
+ # Accept connections on the specified port, default is 6379
12
+ port 9736
13
+
14
+ # If you want you can bind a single interface, if the bind option is not
15
+ # specified all the interfaces will listen for connections.
16
+ #
17
+ # bind 127.0.0.1
18
+
19
+ # Close the connection after a client is idle for N seconds (0 to disable)
20
+ timeout 300
21
+
22
+ # Save the DB on disk:
23
+ #
24
+ # save <seconds> <changes>
25
+ #
26
+ # Will save the DB if both the given number of seconds and the given
27
+ # number of write operations against the DB occurred.
28
+ #
29
+ # In the example below the behaviour will be to save:
30
+ # after 900 sec (15 min) if at least 1 key changed
31
+ # after 300 sec (5 min) if at least 10 keys changed
32
+ # after 60 sec if at least 10000 keys changed
33
+ save 900 1
34
+ save 300 10
35
+ save 60 10000
36
+
37
+ # The filename where to dump the DB
38
+ dbfilename dump.rdb
39
+
40
+ # For default save/load DB in/from the working directory
41
+ # Note that you must specify a directory not a file name.
42
+ dir ./test/
43
+
44
+ # Set server verbosity to 'debug'
45
+ # it can be one of:
46
+ # debug (a lot of information, useful for development/testing)
47
+ # notice (moderately verbose, what you want in production probably)
48
+ # warning (only very important / critical messages are logged)
49
+ loglevel debug
50
+
51
+ # Specify the log file name. Also 'stdout' can be used to force
52
+ # the demon to log on the standard output. Note that if you use standard
53
+ # output for logging but daemonize, logs will be sent to /dev/null
54
+ logfile stdout
55
+
56
+ # Set the number of databases. The default database is DB 0, you can select
57
+ # a different one on a per-connection basis using SELECT <dbid> where
58
+ # dbid is a number between 0 and 'databases'-1
59
+ databases 16
60
+
61
+ ################################# REPLICATION #################################
62
+
63
+ # Master-Slave replication. Use slaveof to make a Redis instance a copy of
64
+ # another Redis server. Note that the configuration is local to the slave
65
+ # so for example it is possible to configure the slave to save the DB with a
66
+ # different interval, or to listen to another port, and so on.
67
+
68
+ # slaveof <masterip> <masterport>
69
+
70
+ ################################## SECURITY ###################################
71
+
72
+ # Require clients to issue AUTH <PASSWORD> before processing any other
73
+ # commands. This might be useful in environments in which you do not trust
74
+ # others with access to the host running redis-server.
75
+ #
76
+ # This should stay commented out for backward compatibility and because most
77
+ # people do not need auth (e.g. they run their own servers).
78
+
79
+ # requirepass foobared
80
+
81
+ ################################### LIMITS ####################################
82
+
83
+ # Set the max number of connected clients at the same time. By default there
84
+ # is no limit, and it's up to the number of file descriptors the Redis process
85
+ # is able to open. The special value '0' means no limts.
86
+ # Once the limit is reached Redis will close all the new connections sending
87
+ # an error 'max number of clients reached'.
88
+
89
+ # maxclients 128
90
+
91
+ # Don't use more memory than the specified amount of bytes.
92
+ # When the memory limit is reached Redis will try to remove keys with an
93
+ # EXPIRE set. It will try to start freeing keys that are going to expire
94
+ # in little time and preserve keys with a longer time to live.
95
+ # Redis will also try to remove objects from free lists if possible.
96
+ #
97
+ # If all this fails, Redis will start to reply with errors to commands
98
+ # that will use more memory, like SET, LPUSH, and so on, and will continue
99
+ # to reply to most read-only commands like GET.
100
+ #
101
+ # WARNING: maxmemory can be a good idea mainly if you want to use Redis as a
102
+ # 'state' server or cache, not as a real DB. When Redis is used as a real
103
+ # database the memory usage will grow over the weeks, it will be obvious if
104
+ # it is going to use too much memory in the long run, and you'll have the time
105
+ # to upgrade. With maxmemory after the limit is reached you'll start to get
106
+ # errors for write operations, and this may even lead to DB inconsistency.
107
+
108
+ # maxmemory <bytes>
109
+
110
+ ############################### ADVANCED CONFIG ###############################
111
+
112
+ # Glue small output buffers together in order to send small replies in a
113
+ # single TCP packet. Uses a bit more CPU but most of the times it is a win
114
+ # in terms of number of queries per second. Use 'yes' if unsure.
115
+ glueoutputbuf yes
@@ -0,0 +1,306 @@
1
+ require 'test_helper'
2
+
3
+ context 'class methods without setup' do
4
+ teardown do
5
+ AllSeeingEye.redis.flushall
6
+ end
7
+
8
+ test 'models know their own name' do
9
+ assert_equal 'model', AllSeeingEye::Model.model_name
10
+ end
11
+
12
+ test 'models correctly find their next ID' do
13
+ id = AllSeeingEye::Model.next_id
14
+ (id + 1..(id + 11)).each {|n|assert_equal AllSeeingEye::Model.next_id, n}
15
+ end
16
+
17
+ test 'models should get fields' do
18
+ assert_equal AllSeeingEye::Model.fields,
19
+ {"uri"=>{"method"=>".request_uri", "object"=>"request"}, "action"=>{"method"=>"[:action]", "object"=>"params"}, "ip"=>{"method"=>".remote_ip", "object"=>"request"}, "controller"=>{"method"=>"[:controller]", "params"=>nil, "object"=>"params"}, "browser"=>{"method"=>".env[\"HTTP_USER_AGENT\"]", "object"=>"request"}, "status"=>{"method"=>".status", "object"=>"response"}}
20
+ end
21
+
22
+ test 'models should get field names' do
23
+ assert_equal ["uri", "action", "ip", "controller", "browser", "status", "created_at", "time_spent"],
24
+ AllSeeingEye::Model.field_names
25
+ end
26
+
27
+ test 'return a new model object' do
28
+ time = Time.now
29
+ obj = AllSeeingEye::Model.create(:uri => '/test/', :ip => '127.0.0.1', :created_at => time)
30
+
31
+ assert_equal '/test/', obj.uri
32
+ assert_equal '127.0.0.1', obj.ip
33
+ assert_equal time, obj.created_at
34
+ assert_equal nil, obj.action
35
+ end
36
+
37
+ test 'create a model object' do
38
+ obj = AllSeeingEye::Model.create(:uri => '/test/', :ip => '127.0.0.1')
39
+
40
+ assert_equal '/test/', obj.uri
41
+ assert_equal '127.0.0.1', obj.ip
42
+ assert_equal Marshal.dump(obj), AllSeeingEye.redis["allseeingeye:model:#{AllSeeingEye::Model.next_id - 1}"]
43
+ assert_equal '1', AllSeeingEye.redis.zscore("allseeingeye:model:fields:uri", '/test/')
44
+ assert_equal '1', AllSeeingEye.redis.zscore("allseeingeye:model:fields:ip", '127.0.0.1')
45
+ assert_equal [obj.id.to_s], AllSeeingEye.redis.sort("allseeingeye:model:fields:uri:/test/", :by => 'nosort')
46
+ assert_equal [obj.id.to_s], AllSeeingEye.redis.sort("allseeingeye:model:fields:ip:127.0.0.1", :by => 'nosort')
47
+ end
48
+
49
+ test 'unmarshal an object with load' do
50
+ time = Time.now
51
+ raw = AllSeeingEye::Model.create(:uri => '/test/', :ip => '127.0.0.1', :created_at => time)
52
+ dump = Marshal.dump(raw)
53
+ obj = AllSeeingEye::Model.load(dump)
54
+
55
+ assert_equal '/test/', obj.uri
56
+ assert_equal '127.0.0.1', obj.ip
57
+ assert_equal time, obj.created_at
58
+ assert_equal raw.id, obj.id
59
+ end
60
+
61
+ test 'find an object by its ID' do
62
+ time = Time.now
63
+ obj = AllSeeingEye::Model.create(:uri => '/test/', :ip => '127.0.0.1', :created_at => time)
64
+ found = AllSeeingEye::Model.find(obj.id)
65
+
66
+ [:uri, :ip, :created_at].each {|f| assert_equal obj.send(f), found.send(f)}
67
+ end
68
+
69
+ test 'find all objects' do
70
+ obj1 = AllSeeingEye::Model.create(:uri => '/test/', :ip => '127.0.0.1', :created_at => Time.now)
71
+ obj2 = AllSeeingEye::Model.create(:uri => '/test/', :ip => '127.0.0.1', :created_at => Time.now)
72
+ obj3 = AllSeeingEye::Model.create(:uri => '/test/', :ip => '127.0.0.1', :created_at => Time.now)
73
+
74
+ assert_equal [obj1.id, obj2.id, obj3.id], AllSeeingEye::Model.all.collect(&:id)
75
+ end
76
+
77
+ test 'find the count of all objects' do
78
+ obj1 = AllSeeingEye::Model.create(:uri => '/test/', :ip => '127.0.0.1', :created_at => Time.now)
79
+ obj2 = AllSeeingEye::Model.create(:uri => '/test/', :ip => '127.0.0.1', :created_at => Time.now)
80
+ obj3 = AllSeeingEye::Model.create(:uri => '/test/', :ip => '127.0.0.1', :created_at => Time.now)
81
+
82
+ assert_equal 3, AllSeeingEye::Model.count
83
+ end
84
+ end
85
+
86
+ context 'class methods with setup' do
87
+ setup do
88
+ @time = Time.now
89
+
90
+ @obj1 = AllSeeingEye::Model.create(:uri => '/', :ip => '127.0.0.1', :created_at => @time - 10 * 60)
91
+ @obj2 = AllSeeingEye::Model.create(:uri => '/test/', :ip => '127.0.0.1', :created_at => @time - 5 * 60)
92
+ @obj3 = AllSeeingEye::Model.create(:uri => '/test/', :ip => '127.0.0.1', :created_at => @time - 3 * 60)
93
+ @obj4 = AllSeeingEye::Model.create(:uri => '/test/', :ip => '192.168.0.1', :created_at => @time - 1 * 60)
94
+ @obj5 = AllSeeingEye::Model.create(:uri => '/test/', :ip => '127.0.0.1', :created_at => @time + 2 * 60)
95
+ @obj6 = AllSeeingEye::Model.create(:uri => '/test/', :ip => '192.168.0.1', :created_at => @time + 2 * 60)
96
+ end
97
+
98
+ teardown do
99
+ AllSeeingEye.redis.flushall
100
+ end
101
+
102
+ test 'find all objects in a date range with a start and a stop' do
103
+ results = AllSeeingEye::Model.all(:start => @time - 11 * 60, :stop => @time - 4 * 60)
104
+
105
+ assert_equal 2, results.count
106
+ assert_equal @obj1.id, results[0].id
107
+ assert_equal @obj2.id, results[1].id
108
+ end
109
+
110
+ test 'find all objects in a date range with only a start' do
111
+ results = AllSeeingEye::Model.all(:start => @time)
112
+
113
+ assert_equal 2, results.count
114
+ assert_equal @obj5.id, results[0].id
115
+ assert_equal @obj6.id, results[1].id
116
+ end
117
+
118
+ test 'find all objects in a date range with only a stop' do
119
+ results = AllSeeingEye::Model.all(:stop => @time)
120
+
121
+ assert_equal 4, results.count
122
+ assert_equal @obj1.id, results[0].id
123
+ assert_equal @obj2.id, results[1].id
124
+ assert_equal @obj3.id, results[2].id
125
+ assert_equal @obj4.id, results[3].id
126
+ end
127
+
128
+ test 'find model objects by a string field' do
129
+ results = AllSeeingEye::Model.find_by_field('uri', :value => '/test/')
130
+
131
+ assert_equal 5, results.count
132
+ assert results.collect(&:id).include?(@obj2.id)
133
+ assert results.collect(&:id).include?(@obj3.id)
134
+ assert results.collect(&:id).include?(@obj4.id)
135
+ assert results.collect(&:id).include?(@obj5.id)
136
+ assert results.collect(&:id).include?(@obj6.id)
137
+ end
138
+
139
+ test 'find model objects by a string field with a start' do
140
+ results = AllSeeingEye::Model.find_by_field('uri', :value => '/test/', :start => @time - 4 * 60)
141
+
142
+ assert_equal 4, results.count
143
+ assert results.collect(&:id).include?(@obj3.id)
144
+ assert results.collect(&:id).include?(@obj4.id)
145
+ assert results.collect(&:id).include?(@obj5.id)
146
+ assert results.collect(&:id).include?(@obj6.id)
147
+ end
148
+
149
+ test 'find model objects by a string field with a stop' do
150
+ results = AllSeeingEye::Model.find_by_field('uri', :value => '/test/', :stop => @time)
151
+
152
+ assert_equal 3, results.count
153
+ assert results.collect(&:id).include?(@obj2.id)
154
+ assert results.collect(&:id).include?(@obj3.id)
155
+ assert results.collect(&:id).include?(@obj4.id)
156
+ end
157
+
158
+ test 'find model objects by a string field with a start and a stop' do
159
+ results = AllSeeingEye::Model.find_by_field('uri', :value => '/test/', :start => @time - 4 * 60, :stop => @time)
160
+
161
+ assert_equal 2, results.count
162
+ assert results.collect(&:id).include?(@obj3.id)
163
+ assert results.collect(&:id).include?(@obj4.id)
164
+ end
165
+
166
+ test 'find model objects by a date field' do
167
+ results = AllSeeingEye::Model.find_by_field('created_at', :start => @obj1.created_at, :stop => @obj4.created_at)
168
+
169
+ assert_equal 4, results.count
170
+ assert_equal results.first.id, @obj1.id
171
+ assert_equal results.last.id, @obj4.id
172
+ end
173
+
174
+ test 'count model objects' do
175
+ assert_equal 2, AllSeeingEye::Model.count(:start => @time - 11 * 60, :stop => @time - 4 * 60)
176
+ assert_equal 2, AllSeeingEye::Model.count(:start => @time + 2 * 60, :stop => @time + 2 * 60)
177
+ assert_equal 4, AllSeeingEye::Model.count(:stop => @time)
178
+ assert_equal 2, AllSeeingEye::Model.count(:start => @time)
179
+ assert_equal 6, AllSeeingEye::Model.count
180
+ end
181
+
182
+ test 'return the first object' do
183
+ assert_equal @obj1.id, AllSeeingEye::Model.first.id
184
+ end
185
+
186
+ test 'return the last object' do
187
+ assert_equal @obj6.id, AllSeeingEye::Model.last.id
188
+ end
189
+
190
+ test 'return count in a field' do
191
+ counts = AllSeeingEye::Model.count_by_field('uri')
192
+
193
+ assert_equal '/test/', counts.first.first
194
+ assert_equal 5, counts.first.last
195
+ assert_equal '/', counts.last.first
196
+ assert_equal 1, counts.last.last
197
+ end
198
+
199
+ test 'search with Chronic' do
200
+ results = AllSeeingEye::Model.search('last friday to five minutes ago')
201
+
202
+ assert_equal 2, results.size
203
+ assert_equal @obj1.id, results[0].id
204
+ assert_equal @obj2.id, results[1].id
205
+ end
206
+
207
+ test 'search by a specific field' do
208
+ results = AllSeeingEye::Model.search('uri:/test/')
209
+
210
+ assert_equal 5, results.count
211
+ assert results.collect(&:id).include?(@obj2.id)
212
+ assert results.collect(&:id).include?(@obj3.id)
213
+ assert results.collect(&:id).include?(@obj4.id)
214
+ assert results.collect(&:id).include?(@obj5.id)
215
+ assert results.collect(&:id).include?(@obj6.id)
216
+ end
217
+
218
+ test 'search without a specific field' do
219
+ results = AllSeeingEye::Model.search('192.168.0.1')
220
+
221
+ assert_equal 2, results.count
222
+ assert results.collect(&:id).include?(@obj4.id)
223
+ assert results.collect(&:id).include?(@obj6.id)
224
+ end
225
+ end
226
+
227
+ context 'many objects' do
228
+ setup do
229
+ @time = Time.now
230
+
231
+ 2000.times do |n|
232
+ instance_variable_set("@obj#{n}".to_sym, AllSeeingEye::Model.create(:uri => '/', :ip => '127.0.0.1', :created_at => @time - (n * (1999 - n)).minutes))
233
+ end
234
+ end
235
+
236
+ test 'conglomerate an array successfully' do
237
+ results = AllSeeingEye::Model.all
238
+ counts = AllSeeingEye::Model.conglomerate(results)
239
+
240
+ assert_equal 500, counts.count
241
+ assert_equal 2000, counts.inject(0) {|sum, k| sum + k.last}
242
+ assert_equal @obj999.created_at.to_i, counts.first.first
243
+ end
244
+ end
245
+
246
+ context 'instance methods' do
247
+ teardown do
248
+ AllSeeingEye.redis.flushall
249
+ end
250
+
251
+ test 'assign all attr_accessors from fields at initialization' do
252
+ obj = AllSeeingEye::Model.new
253
+
254
+ AllSeeingEye::Model.field_names.each {|f| assert obj.respond_to?(f.to_sym); assert obj.respond_to?("#{f}=".to_sym)}
255
+ end
256
+
257
+ test 'assign an object an ID if it does not have one' do
258
+ obj = AllSeeingEye::Model.new
259
+
260
+ assert_equal nil, obj.instance_variable_get(:@id)
261
+ assert_equal 1, obj.id
262
+ assert_equal 1, obj.instance_variable_get(:@id)
263
+ end
264
+
265
+ test 'save a new model object' do
266
+ obj = AllSeeingEye::Model.new(:uri => '/test/', :ip => '127.0.0.1')
267
+
268
+ assert obj.save
269
+ assert_equal '/test/', obj.uri
270
+ assert_equal '127.0.0.1', obj.ip
271
+ assert_equal Marshal.dump(obj), AllSeeingEye.redis["allseeingeye:model:#{AllSeeingEye::Model.next_id - 1}"]
272
+ assert_equal '1', AllSeeingEye.redis.zscore("allseeingeye:model:fields:uri", '/test/')
273
+ assert_equal '1', AllSeeingEye.redis.zscore("allseeingeye:model:fields:ip", '127.0.0.1')
274
+ assert_equal [obj.id.to_s], AllSeeingEye.redis.sort("allseeingeye:model:fields:uri:/test/", :by => 'nosort')
275
+ end
276
+
277
+ test 'save a new model object with date attributes' do
278
+ time = Time.now
279
+ obj = AllSeeingEye::Model.new(:created_at => time)
280
+
281
+ assert obj.save
282
+ assert_equal time, obj.created_at
283
+ assert_equal Marshal.dump(obj), AllSeeingEye.redis["allseeingeye:model:#{AllSeeingEye::Model.next_id - 1}"]
284
+ assert_equal time.to_i.to_s, AllSeeingEye.redis.zscore("allseeingeye:model:fields:created_at", obj.id)
285
+ end
286
+
287
+ test 'ignore empty and nil attributes in model object creation' do
288
+ time = Time.now
289
+ obj = AllSeeingEye::Model.new(:created_at => time, :uri => '/test/', :ip => nil, :action => '')
290
+
291
+ assert obj.save
292
+ assert_equal time, obj.created_at
293
+ assert_equal '/test/', obj.uri
294
+ assert_equal nil, obj.ip
295
+ assert_equal '', obj.action
296
+ assert_equal Marshal.dump(obj), AllSeeingEye.redis["allseeingeye:model:#{AllSeeingEye::Model.next_id - 1}"]
297
+ assert_equal time.to_i.to_s, AllSeeingEye.redis.zscore("allseeingeye:model:fields:created_at", obj.id)
298
+ assert_equal '1', AllSeeingEye.redis.zscore("allseeingeye:model:fields:uri", '/test/')
299
+ assert_equal nil, AllSeeingEye.redis.zscore("allseeingeye:model:fields:ip", nil)
300
+ assert_equal nil, AllSeeingEye.redis.zscore("allseeingeye:model:fields:action", '')
301
+ assert_equal [obj.id.to_s], AllSeeingEye.redis.sort("allseeingeye:model:fields:uri:/test/", :by => 'nosort')
302
+ assert_equal [], AllSeeingEye.redis.sort("allseeingeye:model:fields:ip:", :by => 'nosort')
303
+ assert_equal [], AllSeeingEye.redis.sort("allseeingeye:model:fields:action:", :by => 'nosort')
304
+ end
305
+
306
+ end
@@ -0,0 +1,57 @@
1
+ require 'rubygems'
2
+
3
+ dir = File.dirname(File.expand_path(__FILE__))
4
+ $LOAD_PATH.unshift dir + '/../lib'
5
+ require 'test/unit'
6
+ require 'yaml'
7
+ require 'all_seeing_eye'
8
+
9
+ if !system("which redis-server")
10
+ puts '', "** can't find `redis-server` in your path"
11
+ puts "** try running `sudo rake install`"
12
+ abort ''
13
+ end
14
+
15
+ at_exit do
16
+ next if $!
17
+
18
+ if defined?(MiniTest)
19
+ exit_code = MiniTest::Unit.new.run(ARGV)
20
+ else
21
+ exit_code = Test::Unit::AutoRunner.run
22
+ end
23
+
24
+ pid = `ps -A -o pid,command | grep [r]edis-test`.split(" ")[0]
25
+ puts "Killing test redis server..."
26
+ `rm -f #{dir}/dump.rdb`
27
+ Process.kill("KILL", pid.to_i)
28
+ exit exit_code
29
+ end
30
+
31
+ puts "Starting redis for testing at localhost:9736..."
32
+ `redis-server #{dir}/fixtures/redis-test.conf`
33
+ AllSeeingEye.redis(:host => 'localhost', :port => 9736)
34
+
35
+ AllSeeingEye.configuration('test/fixtures')
36
+
37
+ ##
38
+ # test/spec/mini 3
39
+ # http://gist.github.com/25455
40
+ # chris@ozmm.org
41
+ #
42
+ def context(*args, &block)
43
+ return super unless (name = args.first) && block
44
+ require 'test/unit'
45
+ klass = Class.new(defined?(ActiveSupport::TestCase) ? ActiveSupport::TestCase : Test::Unit::TestCase) do
46
+ def self.test(name, &block)
47
+ define_method("test_#{name.gsub(/\W/,'_')}", &block) if block
48
+ end
49
+ def self.xtest(*args) end
50
+ def self.setup(&block) define_method(:setup, &block) end
51
+ def self.teardown(&block) define_method(:teardown, &block) end
52
+ end
53
+ (class << klass; self end).send(:define_method, :name) { name.gsub(/\W/,'_') }
54
+ klass.class_eval &block
55
+ # XXX: In 1.8.x, not all tests will run unless anonymous classes are kept in scope.
56
+ ($test_classes ||= []) << klass
57
+ end