dtr 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,88 @@
1
+ require 'rbconfig'
2
+ require 'find'
3
+ require 'ftools'
4
+
5
+ include Config
6
+
7
+ $ruby = CONFIG['ruby_install_name']
8
+
9
+ ##
10
+ # Install a binary file. We patch in on the way through to
11
+ # insert a #! line. If this is a Unix install, we name
12
+ # the command (for example) 'dtr' and let the shebang line
13
+ # handle running it. Under windows, we add a '.rb' extension
14
+ # and let file associations to their stuff
15
+ #
16
+
17
+ def installBIN(from, opfile)
18
+
19
+ tmp_dir = nil
20
+ for t in [".", "/tmp", "c:/temp", $bindir]
21
+ stat = File.stat(t) rescue next
22
+ if stat.directory? and stat.writable?
23
+ tmp_dir = t
24
+ break
25
+ end
26
+ end
27
+
28
+ fail "Cannot find a temporary directory" unless tmp_dir
29
+ tmp_file = File.join(tmp_dir, "_tmp")
30
+
31
+ File.open(from) do |ip|
32
+ File.open(tmp_file, "w") do |op|
33
+ ruby = File.join($realbindir, $ruby)
34
+ op.puts "#!#{ruby}"
35
+ op.write ip.read
36
+ end
37
+ end
38
+
39
+ opfile += ".rb" if CONFIG["target_os"] =~ /mswin/i
40
+ File::install(tmp_file, File.join($bindir, opfile), 0755, true)
41
+ File::unlink(tmp_file)
42
+ end
43
+
44
+ $sitedir = CONFIG["sitelibdir"]
45
+ unless $sitedir
46
+ version = CONFIG["MAJOR"]+"."+CONFIG["MINOR"]
47
+ $libdir = File.join(CONFIG["libdir"], "ruby", version)
48
+ $sitedir = $:.find {|x| x =~ /site_ruby/}
49
+ if !$sitedir
50
+ $sitedir = File.join($libdir, "site_ruby")
51
+ elsif $sitedir !~ Regexp.quote(version)
52
+ $sitedir = File.join($sitedir, version)
53
+ end
54
+ end
55
+
56
+ $bindir = CONFIG["bindir"]
57
+
58
+ $realbindir = $bindir
59
+
60
+ bindir = CONFIG["bindir"]
61
+ if (destdir = ENV['DESTDIR'])
62
+ $bindir = destdir + $bindir
63
+ $sitedir = destdir + $sitedir
64
+
65
+ File::makedirs($bindir)
66
+ File::makedirs($sitedir)
67
+ end
68
+
69
+ dtr_dest = File.join($sitedir, "dtr")
70
+ File::makedirs(dtr_dest, true)
71
+ File::chmod(0755, dtr_dest)
72
+
73
+ # The library files
74
+
75
+ files = Dir.chdir('lib') { Dir['**/*.rb', '**/*.rake'] }
76
+
77
+ for fn in files
78
+ fn_dir = File.dirname(fn)
79
+ target_dir = File.join($sitedir, fn_dir)
80
+ if ! File.exist?(target_dir)
81
+ File.makedirs(target_dir)
82
+ end
83
+ File::install(File.join('lib', fn), File.join($sitedir, fn), 0644, true)
84
+ end
85
+
86
+ # and the executable
87
+
88
+ installBIN("bin/dtr", "dtr")
@@ -0,0 +1,111 @@
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
+ DTRVERSION="0.0.3"
16
+ DTROPTIONS = {} unless defined?(DTROPTIONS)
17
+
18
+ require 'fileutils'
19
+
20
+ module DTR
21
+
22
+ def start_server
23
+ require 'dtr/service_provider'
24
+ ServiceProvider.new.start
25
+ end
26
+
27
+ def start_runners
28
+ launch_runners(DTROPTIONS[:names], DTROPTIONS[:setup])
29
+ end
30
+
31
+ def launch_runners(names, setup=nil)
32
+ require 'dtr/runner'
33
+ names = names || "DTR(#{Time.now})"
34
+ DTR::RunnerAgent.start(names, setup)
35
+ end
36
+
37
+ def lib_path
38
+ File.expand_path(File.dirname(__FILE__))
39
+ end
40
+
41
+ def broadcast_list=(list)
42
+ require 'dtr/service_provider'
43
+ ServiceProvider.broadcast_list = list
44
+ end
45
+
46
+ def port=(port)
47
+ require 'dtr/service_provider'
48
+ ServiceProvider.port = port
49
+ end
50
+
51
+ def runners
52
+ require 'dtr/service_provider'
53
+ ServiceProvider.new.runners
54
+ end
55
+
56
+ def monitor
57
+ require 'dtr/service_provider'
58
+ DTROPTIONS[:log_file] = 'dtr_monitor.log'
59
+ ServiceProvider.new.monitor
60
+ end
61
+
62
+ def stop_runners_daemon_mode
63
+ with_daemon_gem do
64
+ Daemons.run_proc "dtr_runners", :ARGV => ['stop']
65
+ end
66
+ end
67
+
68
+ def start_runners_daemon_mode
69
+ with_daemon_gem do
70
+ FileUtils.rm_rf('dtr_runners.output')
71
+ pwd = Dir.pwd
72
+ Daemons.run_proc "dtr_runners", :ARGV => ['start'], :backtrace => true do
73
+ Dir.chdir(pwd) do
74
+ start_runners
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ def start_server_daemon_mode
81
+ with_daemon_gem do
82
+ Daemons.run_proc "dtr_server", :ARGV => ['start'] do
83
+ start_server
84
+ end
85
+ end
86
+ sleep(2)
87
+ end
88
+
89
+ def stop_server_daemon_mode
90
+ with_daemon_gem do
91
+ Daemons.run_proc "dtr_server", :ARGV => ['stop']
92
+ end
93
+ end
94
+
95
+ def with_daemon_gem
96
+ require "rubygems"
97
+ begin
98
+ require "daemons"
99
+ rescue LoadError
100
+ raise "The daemons gem must be installed"
101
+ end
102
+ yield
103
+ end
104
+
105
+ def clean_server
106
+ require 'dtr/service_provider'
107
+ ServiceProvider.new.clear_workspace
108
+ end
109
+
110
+ module_function :start_server, :start_runners, :launch_runners, :lib_path, :broadcast_list=, :runners, :with_daemon_gem, :start_runners_daemon_mode, :stop_runners_daemon_mode, :start_server_daemon_mode, :stop_server_daemon_mode, :monitor, :port=, :clean_server
111
+ 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 'pstore'
16
+
17
+ require 'logger'
18
+
19
+ module DTR
20
+
21
+ MESSAGE_KEY = :message
22
+
23
+ def logger
24
+ return DTROPTIONS[:logger] if DTROPTIONS[:logger]
25
+ DTROPTIONS[:logger] = if DTROPTIONS[:log_level] == Logger::DEBUG
26
+ Logger.new(STDOUT)
27
+ else
28
+ log_file = if File.exist?('log')
29
+ File.join("log", DTROPTIONS[:log_file] || 'dtr.log')
30
+ else
31
+ DTROPTIONS[:log_file] || 'dtr.log'
32
+ end
33
+ Logger.new(log_file, 1, 5*1024*1024)
34
+ end
35
+ DTROPTIONS[:logger].datetime_format = "%m-%d %H:%M:%S"
36
+ DTROPTIONS[:logger].level = DTROPTIONS[:log_level] || Logger::INFO
37
+ DTROPTIONS[:logger]
38
+ end
39
+
40
+ def debug(message=nil, &block)
41
+ output(:debug, message, &block)
42
+ end
43
+
44
+ def info(message=nil, &block)
45
+ output(:info, message, &block)
46
+ end
47
+
48
+ def error(message=nil, &block)
49
+ output(:error, message, &block)
50
+ end
51
+
52
+ def output(level, msg=nil, &block)
53
+ logger.send(level) do
54
+ message = block_given? ? block.call : msg.to_s
55
+ EnvStore.new << [MESSAGE_KEY, "[#{Process.pid}-#{level.to_s.upcase}] #{message}"] if DTROPTIONS[:run_with_monitor]
56
+ message
57
+ end
58
+ end
59
+
60
+ def silent?
61
+ logger.level == Logger::ERROR
62
+ end
63
+
64
+ def with_monitor
65
+ DTROPTIONS[:run_with_monitor] = true
66
+ EnvStore.new[MESSAGE_KEY] = []
67
+ yield
68
+ rescue Exception => e
69
+ info {"stopping by Exception => #{e.class.name}, message => #{e.message}"}
70
+ wait_times = 0
71
+ until EnvStore.new[MESSAGE_KEY].empty? || wait_times > 14
72
+ wait_times += 1
73
+ sleep(1)
74
+ end
75
+ raise e
76
+ end
77
+
78
+ module_function :debug, :info, :error, :output, :with_monitor, :logger, :silent?
79
+
80
+ class CmdInterrupt < StandardError; end
81
+
82
+ class Cmd
83
+ def self.execute(cmd)
84
+ return true if cmd.nil? || cmd.empty?
85
+ DTR.info {"Executing: #{cmd.inspect}"}
86
+ output = %x[#{cmd} 2>&1]
87
+ DTR.info {"Execution is done, status: #{$?.exitstatus}"}
88
+ DTR.error {"#{cmd.inspect} output:\n#{output}"} if $?.exitstatus != 0
89
+ $?.exitstatus == 0
90
+ end
91
+ end
92
+
93
+ class EnvStore
94
+
95
+ FILE_NAME = '.dtr_env_pstore'
96
+
97
+ def self.destroy
98
+ File.delete(FILE_NAME) if File.exist?(FILE_NAME)
99
+ end
100
+
101
+ def [](key)
102
+ return nil unless File.exist?(FILE_NAME)
103
+
104
+ repository = PStore.new(FILE_NAME)
105
+ repository.transaction(true) do
106
+ repository[key]
107
+ end
108
+ end
109
+
110
+ def []=(key, value)
111
+ repository = PStore.new(FILE_NAME)
112
+ repository.transaction do
113
+ repository[key] = value
114
+ end
115
+ end
116
+
117
+ def <<(key_value)
118
+ key, value = key_value
119
+ repository = PStore.new(FILE_NAME)
120
+ repository.transaction do
121
+ repository[key] = (repository[key] || []) << value
122
+ end
123
+ end
124
+
125
+ def shift(key)
126
+ repository = PStore.new(FILE_NAME)
127
+ repository.transaction do
128
+ if array = repository[key]
129
+ array.shift
130
+ repository[key] = array
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ class WorkingEnv
137
+
138
+ @@current = nil
139
+ def self.refresh
140
+ @@current = self.new
141
+ end
142
+
143
+ def self.current
144
+ @@current
145
+ end
146
+
147
+ def initialize
148
+ files = (defined?($argv_dup) ? $argv_dup : []).dup
149
+ @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}"}
150
+ end
151
+
152
+ def [](key)
153
+ @env[key]
154
+ end
155
+
156
+ def to_s
157
+ @env.inspect
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,68 @@
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
+ runner_names = []
64
+ self.processes.to_i.times {|i| runner_names << "runner#{i}"}
65
+ %x[dtr -r #{runner_names.join(',')} -D #{runner_options}]
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,265 @@
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.empty?
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].empty?
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].empty?
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 shutdown
257
+ DTR.info "#{self} is shutting down. Ran #{@started.size} tests, finished #{@run_finished.size}."
258
+ @provider.stop_service rescue exit!
259
+ end
260
+
261
+ def to_s
262
+ "Runner #{@name}"
263
+ end
264
+ end
265
+ end