xli-dtr 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/lib/dtr/base.rb ADDED
@@ -0,0 +1,172 @@
1
+ # Copyright (c) 2007-2008 Li Xiao
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'pstore'
16
+
17
+ require 'logger'
18
+
19
+ class Array
20
+ def blank?
21
+ empty?
22
+ end
23
+ end
24
+
25
+ class NilClass
26
+ def blank?
27
+ true
28
+ end
29
+ end
30
+
31
+ module DTR
32
+
33
+ MESSAGE_KEY = :message
34
+
35
+ def logger
36
+ return DTROPTIONS[:logger] if DTROPTIONS[:logger]
37
+ DTROPTIONS[:logger] = if DTROPTIONS[:log_level] == Logger::DEBUG
38
+ Logger.new(STDOUT)
39
+ else
40
+ log_file = if File.exist?('log')
41
+ File.join("log", DTROPTIONS[:log_file] || 'dtr.log')
42
+ else
43
+ DTROPTIONS[:log_file] || 'dtr.log'
44
+ end
45
+ Logger.new("./#{log_file}", 1, 5*1024*1024)
46
+ end
47
+ DTROPTIONS[:logger].datetime_format = "%m-%d %H:%M:%S"
48
+ DTROPTIONS[:logger].level = DTROPTIONS[:log_level] || Logger::INFO
49
+ DTROPTIONS[:logger]
50
+ end
51
+
52
+ def debug(message=nil, &block)
53
+ output(:debug, message, &block)
54
+ end
55
+
56
+ def info(message=nil, &block)
57
+ output(:info, message, &block)
58
+ end
59
+
60
+ def error(message=nil, &block)
61
+ output(:error, message, &block)
62
+ end
63
+
64
+ def output(level, msg=nil, &block)
65
+ logger.send(level) do
66
+ message = block_given? ? block.call : msg.to_s
67
+ EnvStore.new << [MESSAGE_KEY, "[#{Process.pid}-#{level.to_s.upcase}] #{message}"] if DTROPTIONS[:run_with_monitor]
68
+ message
69
+ end
70
+ end
71
+
72
+ def silent?
73
+ logger.level == Logger::ERROR
74
+ end
75
+
76
+ def with_monitor
77
+ DTROPTIONS[:run_with_monitor] = true
78
+ EnvStore.new[MESSAGE_KEY] = []
79
+ yield
80
+ rescue Exception => e
81
+ info {"stopping by Exception => #{e.class.name}, message => #{e.message}"}
82
+ wait_times = 0
83
+ until EnvStore.new[MESSAGE_KEY].empty? || wait_times > 14
84
+ wait_times += 1
85
+ sleep(1)
86
+ end
87
+ raise e
88
+ end
89
+
90
+ module_function :debug, :info, :error, :output, :with_monitor, :logger, :silent?
91
+
92
+ class CmdInterrupt < StandardError; end
93
+
94
+ class Cmd
95
+ def self.execute(cmd)
96
+ return true if cmd.nil? || cmd.empty?
97
+ DTR.info {"Executing: #{cmd.inspect}"}
98
+ output = %x[#{cmd} 2>&1]
99
+ DTR.info {"Execution is done, status: #{$?.exitstatus}"}
100
+ DTR.error {"#{cmd.inspect} output:\n#{output}"} if $?.exitstatus != 0
101
+ $?.exitstatus == 0
102
+ end
103
+ end
104
+
105
+ class EnvStore
106
+
107
+ FILE_NAME = '.dtr_env_pstore'
108
+
109
+ def self.destroy
110
+ File.delete(FILE_NAME) if File.exist?(FILE_NAME)
111
+ end
112
+
113
+ def [](key)
114
+ return nil unless File.exist?(FILE_NAME)
115
+
116
+ repository = PStore.new(FILE_NAME)
117
+ repository.transaction(true) do
118
+ repository[key]
119
+ end
120
+ end
121
+
122
+ def []=(key, value)
123
+ repository = PStore.new(FILE_NAME)
124
+ repository.transaction do
125
+ repository[key] = value
126
+ end
127
+ end
128
+
129
+ def <<(key_value)
130
+ key, value = key_value
131
+ repository = PStore.new(FILE_NAME)
132
+ repository.transaction do
133
+ repository[key] = (repository[key] || []) << value
134
+ end
135
+ end
136
+
137
+ def shift(key)
138
+ repository = PStore.new(FILE_NAME)
139
+ repository.transaction do
140
+ if array = repository[key]
141
+ array.shift
142
+ repository[key] = array
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ class WorkingEnv
149
+
150
+ @@current = nil
151
+ def self.refresh
152
+ @@current = self.new
153
+ end
154
+
155
+ def self.current
156
+ @@current
157
+ end
158
+
159
+ def initialize
160
+ files = (defined?($argv_dup) ? $argv_dup : []).dup
161
+ @env = {:libs => $LOAD_PATH.dup, :files => files, :created_at => Time.now.to_s, :dtr_master_env => ENV['DTR_MASTER_ENV'], :identifier => "#{Time.now.to_s}:#{rand}:#{object_id}"}
162
+ end
163
+
164
+ def [](key)
165
+ @env[key]
166
+ end
167
+
168
+ def to_s
169
+ @env.inspect
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,69 @@
1
+ # Copyright (c) 2007-2008 Li Xiao
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "rubygems"
16
+ require 'dtr'
17
+ require 'rake/testtask'
18
+
19
+ module DTR
20
+ class MPTask < Rake::TestTask
21
+ attr_accessor :processes, :runner_options, :start_server
22
+
23
+ def define
24
+ @libs.unshift DTR.lib_path
25
+ lib_path = @libs.join(File::PATH_SEPARATOR)
26
+
27
+ desc "Run tests" + (@name==:test ? "" : " for #{@name}")
28
+ task @name do
29
+ DTR.start_server_daemon_mode if start_server?
30
+ start_runners
31
+ run_code = ''
32
+ begin
33
+ RakeFileUtils.verbose(@verbose) do
34
+ run_code = rake_loader
35
+ @ruby_opts.unshift( "-I#{lib_path}" )
36
+ @ruby_opts.unshift( "-w" ) if @warning
37
+
38
+ ruby @ruby_opts.join(" ") +
39
+ " \"#{run_code}\" " +
40
+ file_list.unshift('dtr/test_unit_injection.rb').collect { |fn| "\"#{fn}\"" }.join(' ') +
41
+ " #{option_list}"
42
+ end
43
+ ensure
44
+ DTR.stop_runners_daemon_mode rescue nil
45
+ if start_server?
46
+ DTR.stop_server_daemon_mode rescue nil
47
+ end
48
+ end
49
+ end
50
+ self
51
+ end
52
+
53
+ def processes
54
+ @processes ? @processes.to_i : 2
55
+ end
56
+
57
+ def start_server?
58
+ defined?(@start_server) ? @start_server : true
59
+ end
60
+
61
+ private
62
+ def start_runners
63
+ return if self.processes.to_i <= 0
64
+ runner_names = []
65
+ self.processes.to_i.times {|i| runner_names << "runner#{i}"}
66
+ %x[dtr -r #{runner_names.join(',')} -D #{runner_options}]
67
+ end
68
+ end
69
+ end
data/lib/dtr/runner.rb ADDED
@@ -0,0 +1,270 @@
1
+ # Copyright (c) 2007-2008 Li Xiao
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'dtr/base'
16
+ require 'dtr/service_provider'
17
+ require 'test/unit'
18
+ require 'drb'
19
+
20
+ class Test::Unit::TestCase
21
+ alias_method :add_error_without_hack, :add_error
22
+ def add_error(exception)
23
+ add_error_without_hack(DTR::RunnerRuntimeException.new(exception))
24
+ end
25
+
26
+ alias_method :add_failure_without_hack, :add_failure
27
+ def add_failure(message, all_locations=caller())
28
+ add_failure_without_hack(DTR.decorate_error_message(message, 'Assertion failure'), all_locations)
29
+ end
30
+ end
31
+
32
+ module DTR
33
+
34
+ def service_provider
35
+ ServiceProvider.new
36
+ end
37
+
38
+ module_function :service_provider
39
+
40
+ class RunnerAgent
41
+
42
+ def self.start(runner_names=["Distributed Test Runner"], setup_cmd=nil)
43
+ DTR.with_monitor do
44
+ new(runner_names, setup_cmd).launch
45
+ end
46
+ end
47
+
48
+ def initialize(runner_names, setup_cmd)
49
+ @runner_names = runner_names.is_a?(Array) ? runner_names : [runner_names.to_s]
50
+ @setup_cmd = setup_cmd || ""
51
+ @runner_pids = []
52
+ @herald = nil
53
+ @working_env_key = :working_env
54
+ @env_store = EnvStore.new
55
+ @agent_pid = Process.pid
56
+ at_exit {
57
+ if Process.pid == @agent_pid
58
+ DTR.info "*** Runner agent is stopping ***"
59
+ kill_all_runners
60
+ if @herald
61
+ Process.kill 'KILL', @herald rescue nil
62
+ DTR.info "=> Herald is killed."
63
+ end
64
+ if @heart
65
+ Process.kill 'KILL', @heart rescue nil
66
+ DTR.info "=> Heartbeat is stopped."
67
+ end
68
+ DTR.info "*** Runner agent stopped ***"
69
+ end
70
+ }
71
+ end
72
+
73
+ def launch
74
+ DTR.info "=> Runner agent started at: #{Dir.pwd}, pid: #{Process.pid}"
75
+ @heart = drb_fork { Heart.new }
76
+ @herald = drb_fork { Herald.new @working_env_key }
77
+ working_env = {}
78
+ @env_store[@working_env_key] = nil
79
+ loop do
80
+ if @env_store[@working_env_key] && working_env[:identifier] != @env_store[@working_env_key][:identifier]
81
+ working_env = @env_store[@working_env_key]
82
+
83
+ DTR.info "=> Got new working environment created at #{working_env[:created_at]}"
84
+
85
+ kill_all_runners
86
+ ENV['DTR_MASTER_ENV'] = working_env[:dtr_master_env]
87
+
88
+ if Cmd.execute(@setup_cmd)
89
+ @runner_names.each do |name|
90
+ @runner_pids << drb_fork { Runner.start name, working_env }
91
+ end
92
+ else
93
+ DTR.info {'No runners started.'}
94
+ end
95
+ end
96
+ sleep(2)
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def kill_all_runners
103
+ unless @runner_pids.blank?
104
+ @runner_pids.each{ |pid| Process.kill 'KILL', pid rescue nil }
105
+ DTR.info "=> All runners(#{@runner_pids.join(", ")}) were killed."
106
+ @runner_pids = []
107
+ end
108
+ end
109
+
110
+ def drb_fork
111
+ Process.fork do
112
+ at_exit {
113
+ DRb.stop_service
114
+ exit!
115
+ }
116
+ begin
117
+ yield
118
+ rescue Interrupt => e
119
+ raise e
120
+ rescue SystemExit => e
121
+ raise e
122
+ rescue Exception => e
123
+ DTR.error "Got an Exception #{e.message}:"
124
+ DTR.error e.backtrace.join("\n")
125
+ raise e
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ class Heart
132
+ def initialize(key=MESSAGE_KEY)
133
+ @key = key
134
+ @env_store = EnvStore.new
135
+ @provider = DTR.service_provider
136
+ beat
137
+ end
138
+
139
+ def beat
140
+ loop do
141
+ begin
142
+ if @env_store[@key].blank?
143
+ @provider.send_message('---/V---')
144
+ else
145
+ while message = @env_store[@key].first
146
+ @provider.send_message(message)
147
+ @env_store.shift(@key)
148
+ end
149
+ end
150
+ sleep_any_way
151
+ rescue => e
152
+ DTR.info "Heart lost DTR Server(#{e.message}), going to sleep 10 sec..."
153
+ @env_store[@key] = []
154
+ sleep_any_way
155
+ end
156
+ end
157
+ end
158
+
159
+ private
160
+ def sleep_any_way
161
+ sleep(10)
162
+ rescue Exception
163
+ end
164
+ end
165
+
166
+ class Herald
167
+
168
+ def initialize(key)
169
+ @key = key
170
+ @env_store = EnvStore.new
171
+ @env_store[@key] = nil
172
+ @provider = DTR.service_provider
173
+ start_off
174
+ end
175
+
176
+ def start_off
177
+ loop do
178
+ DTR.info "=> Herald starts off..."
179
+ begin
180
+ working_env = @provider.working_env
181
+ DTR.debug { "working env: #{working_env.inspect}" }
182
+ if working_env[:files].blank?
183
+ DTR.error "No test files need to load?(working env: #{working_env.inspect})"
184
+ else
185
+ @env_store[@key] = working_env if @env_store[@key].nil? || @env_store[@key][:identifier] != working_env[:identifier]
186
+ @provider.wait_until_teardown
187
+ end
188
+
189
+ sleep(2)
190
+ rescue => e
191
+ DTR.info "Herald lost DTR Server(#{e.message}), going to sleep 5 sec..."
192
+ sleep(5)
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ class Runner
199
+ include DRbUndumped
200
+
201
+ def self.start(name, env)
202
+ DTR.info "#{name}: Initialize working environment..."
203
+ env[:libs].select{ |lib| !$LOAD_PATH.include?(lib) && File.exists?(lib) }.each do |lib|
204
+ $LOAD_PATH << lib
205
+ DTR.debug {"#{name}: appended lib: #{lib}"}
206
+ end
207
+ DTR.info "#{name}: libs loaded"
208
+ DTR.debug {"#{name}: $LOAD_PATH: #{$LOAD_PATH.inspect}"}
209
+
210
+ env[:files].each do |f|
211
+ begin
212
+ load f unless f =~ /^-/
213
+ DTR.debug {"#{name}: loaded #{f}"}
214
+ rescue LoadError => e
215
+ DTR.error "#{name}: No such file to load -- #{f} (Environment: #{env.inspect})"
216
+ end
217
+ end
218
+ DTR.info "#{name}: test files loaded"
219
+
220
+ @provider = DTR.service_provider
221
+
222
+ @provider.provide(self.new(@provider, name, env[:identifier]))
223
+ DTR.info "=> Runner #{name} provided"
224
+ DRb.thread.join if DRb.thread
225
+ end
226
+
227
+ attr_reader :name, :identifier
228
+
229
+ def initialize(provider, name, identifier)
230
+ Test::Unit.run = true
231
+ @name = name
232
+ @provider = provider
233
+ @identifier = identifier
234
+ @started = []
235
+ @run_finished = []
236
+ end
237
+
238
+ def run(test, result, &progress_block)
239
+ DTR.debug {"#{name}: running #{test}..."}
240
+ @started << test.name
241
+ test.run(result, &progress_block)
242
+ rescue DRb::DRbConnError => e
243
+ DTR.info{ "Rescued DRb::DRbConnError(#{e.message}), while running test: #{test.name}. The master process may be stopped." }
244
+ rescue Exception => e
245
+ DTR.error {"Unexpected exception: #{e.message}"}
246
+ DTR.error {e.backtrace.join("\n")}
247
+ result.add_error(Test::Unit::Error.new(test.name, e))
248
+ result.add_run
249
+ progress_block.call(Test::Unit::TestCase::FINISHED, test.name)
250
+ ensure
251
+ DTR.debug {"#{name}: done #{test}"}
252
+ @run_finished << test.name
253
+ @provider.provide(self)
254
+ end
255
+
256
+ def reboot
257
+ DTR.info "#{self} is rebooting. Ran #{@started.size} tests, finished #{@run_finished.size}."
258
+ @provider.provide(self)
259
+ end
260
+
261
+ def shutdown
262
+ DTR.info "#{self} is shutting down. Ran #{@started.size} tests, finished #{@run_finished.size}."
263
+ @provider.stop_service rescue exit!
264
+ end
265
+
266
+ def to_s
267
+ "Runner #{@name}"
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,160 @@
1
+ # Copyright (c) 2007-2008 Li Xiao
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'dtr/base'
16
+ require 'drb'
17
+ require 'rinda/ring'
18
+ require 'rinda/tuplespace'
19
+ require 'socket'
20
+
21
+ module DTR
22
+ def decorate_error_message(msg, source=nil)
23
+ source ? "#{source} from #{Socket.gethostname}: #{msg}" : "From #{Socket.gethostname}: #{msg}"
24
+ end
25
+
26
+ module_function :decorate_error_message
27
+
28
+ class RunnerRuntimeException < StandardError
29
+ def initialize(e)
30
+ super(DTR.decorate_error_message(e.message, e.class.name))
31
+ set_backtrace(e.backtrace)
32
+ end
33
+ end
34
+
35
+ class ServiceProvider
36
+
37
+ def self.broadcast_list=(list)
38
+ EnvStore.new[:broadcast_list] = list
39
+ end
40
+
41
+ def self.port=(port)
42
+ EnvStore.new[:port] = port
43
+ end
44
+
45
+ PORT = 3344
46
+ BROADCAST_LIST = []
47
+
48
+ def initialize
49
+ DTR.info "-- Initializing drb service..."
50
+ env_store = EnvStore.new
51
+ (env_store[:broadcast_list] || ['localhost']).each do |broadcast|
52
+ BROADCAST_LIST << broadcast.untaint
53
+ DTR.info "-- Added broadcast: #{broadcast}"
54
+ end
55
+ DTR.info "-- Server port: #{server_port}"
56
+ DRb.start_service
57
+ end
58
+
59
+ # start DTR server
60
+ def start
61
+ env_store = EnvStore.new
62
+ DTR.info '-- Booting DTR server...'
63
+ Rinda::RingServer.new Rinda::TupleSpace.new, server_port
64
+ DTR.info "-- DTR server started on port #{server_port}"
65
+ #set safe level to 1 here, now, runner can't set to 1, cause test should can do anything
66
+ #......
67
+ $SAFE = 1 unless $DEBUG # disable eval() and friends
68
+ # Wait until the user explicitly kills the server.
69
+ DRb.thread.join
70
+ end
71
+
72
+ def provide(runner)
73
+ renewer = Rinda::SimpleRenewer.new
74
+ tuple = [:name, 'DTR::Runner'.to_sym, runner.freeze, "DTR remote runner #{Process.pid}-#{runner.name}"]
75
+ lookup_ring.write(tuple, renewer)
76
+ end
77
+
78
+ def send_message(message)
79
+ lookup_ring.write [:agent_heartbeat, Socket.gethostname, message, Time.now], 2
80
+ end
81
+
82
+ def lookup_runner
83
+ lookup_ring.take([:name, 'DTR::Runner'.to_sym, nil, nil])[2]
84
+ end
85
+
86
+ def runners
87
+ lookup_ring.read_all([:name, 'DTR::Runner'.to_sym, nil, nil]).collect {|rt| rt[2]}
88
+ end
89
+
90
+ def monitor
91
+ working_env_monitor = lookup_ring.notify(nil, [:working_env, nil])
92
+ Thread.start do
93
+ DTR.info("Current work environment: #{working_env.inspect}")
94
+ working_env_monitor.each { |t| DTR.info t.inspect }
95
+ end
96
+ if DTROPTIONS[:log_level] == Logger::DEBUG
97
+ runner_monitor = lookup_ring.notify(nil, [:name, 'DTR::Runner'.to_sym, nil, nil])
98
+ Thread.start do
99
+ runner_monitor.each { |t| DTR.debug t.inspect }
100
+ end
101
+ end
102
+ agent_heartbeat_monitor = lookup_ring.notify("write", [:agent_heartbeat, nil, nil, nil])
103
+ Thread.start do
104
+ colors = {}
105
+ base = 30
106
+ agent_heartbeat_monitor.each do |t|
107
+ host, message, time = t[1][1..3]
108
+ colors[host] = base+=1 unless colors[host]
109
+ message = "\e[1;31m#{message}\e[0m" if message =~ /-ERROR\]/
110
+ DTR.info "#{time.strftime("[%I:%M:%S%p]")} \e[1;#{colors[host]};1m#{host}\e[0m: #{message}"
111
+ end
112
+ end
113
+ DRb.thread.join
114
+ end
115
+
116
+ def wait_until_teardown
117
+ lookup_ring.notify(nil, [:working_env, nil]).pop
118
+ end
119
+
120
+ def working_env
121
+ lookup_ring.read([:working_env, nil])[1]
122
+ end
123
+
124
+ def setup_working_env(env)
125
+ clear_workspace
126
+ lookup_ring.write [:working_env, env]
127
+ end
128
+
129
+ def teardown_working_env
130
+ clear_workspace
131
+ end
132
+
133
+ def start_service
134
+ DRb.start_service
135
+ end
136
+
137
+ def stop_service
138
+ DRb.stop_service
139
+ end
140
+
141
+ def clear_workspace
142
+ lookup_ring.read_all([:working_env, nil]).size.times do
143
+ lookup_ring.take [:working_env, nil] rescue nil
144
+ end rescue nil
145
+ runners.size.times do
146
+ lookup_runner.shutdown rescue nil
147
+ end
148
+ end
149
+
150
+ private
151
+ def server_port
152
+ env_store = EnvStore.new
153
+ env_store[:port].to_i > 0 ? env_store[:port].to_i : PORT
154
+ end
155
+
156
+ def lookup_ring
157
+ Rinda::TupleSpaceProxy.new(Rinda::RingFinger.new(BROADCAST_LIST, server_port).lookup_ring_any)
158
+ end
159
+ end
160
+ end