xli-dtr 0.0.4

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/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