DTR 0.0.1

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,281 @@
1
+ require 'yaml'
2
+ require 'dtr/command_line'
3
+
4
+ module DTR
5
+ class AveragePacker
6
+ def pack(test_files, num_packets)
7
+ raise 'No test files given' if test_files.nil? || test_files.empty?
8
+ @packets = Array.new(num_packets) {[]}
9
+ test_files.each_with_index {|tf,i| @packets[i%num_packets] << tf }
10
+
11
+ @packets
12
+ end
13
+ end
14
+
15
+ class RubyRunner
16
+ def run(server, client, test_files, signature)
17
+ test_files.each do |test_file|
18
+ logger.info { "\tRun #{test_file}..." }
19
+ unless File.exist?(test_file.untaint)
20
+ logger.info {"\tSkip test file not exists: #{test_file}"}
21
+ next
22
+ end
23
+
24
+ server_reports = server.reports
25
+ break unless server_reports # if can't get reports, server maybe shutdown
26
+
27
+ if server_reports[test_file]
28
+ logger.info {"\tSkip test file has report on server: #{test_file}"}
29
+ next
30
+ end
31
+ result = client.execute("ruby #{test_file}")
32
+ tests, assertions, failures, errors = parse(result[:stdout])
33
+ report = TestReport.new(test_file, result, tests, assertions, failures, errors)
34
+
35
+ logger.info { report.to_yaml } unless report.succeeded?
36
+
37
+ break unless server.update signature, report
38
+ end
39
+ end
40
+
41
+ SUMMARY_REGEX = /^\s*(\d+) tests, (\d+) assertions, (\d+) failures, (\d+) errors$/
42
+ def parse(stdout)
43
+ summary = stdout.strip.split("\n").reverse.find do |line|
44
+ SUMMARY_REGEX =~ line.strip
45
+ end
46
+ if SUMMARY_REGEX =~ summary
47
+ [$1, $2, $3, $4].collect{|str| str.to_i}
48
+ else
49
+ logger.info {"\tStdout has no tests summary line: #{stdout}"}
50
+ [0, 0, 0, 0]
51
+ end
52
+ end
53
+ end
54
+
55
+ class Server
56
+
57
+ class NoAliveClientsError < StandardError
58
+ end
59
+
60
+ attr_accessor :server_signature, :test_files, :packer, :runner_name, :latest_build_time
61
+
62
+ def initialize(test_files=[])
63
+ @test_files = test_files
64
+ @packer = AveragePacker.new
65
+ @runner_name = RubyRunner.name
66
+ @server_signature = Object.new.to_s
67
+ @report_signature = nil
68
+ @raise_on_no_alive_client = DTROPTIONS[:raise_on_no_alive_client]
69
+ end
70
+
71
+ def latest_build_summary
72
+ raise 'Server never ran!' unless latest_build_time
73
+ %{Finished in #{format('%.1f', latest_build_time)} seconds.
74
+
75
+ #{collect_report(:tests)}, #{collect_report(:assertions)}, #{collect_report(:failures)}, #{collect_report(:errors)}}
76
+ end
77
+
78
+ def clients
79
+ clients_map.values
80
+ end
81
+
82
+ def available_clients
83
+ alive_clients = self.clients.select{|client| client.server_signature == @server_signature}
84
+ raise NoAliveClientsError.new if @raise_on_no_alive_client && alive_clients.empty?
85
+ alive_clients.select{|client| client.idle?}
86
+ end
87
+
88
+ def succeeded?
89
+ complete? && !reports.empty? && reports.values.all?{|report| report.succeeded?}
90
+ end
91
+
92
+ def complete?
93
+ @complete ||= false
94
+ end
95
+
96
+ def run(opt={})
97
+ opt = {:wait_for_reports => true, :setup => false}.merge opt
98
+ run_start(opt)
99
+ @run_once = false
100
+ loop do
101
+ break if complete?
102
+ run_once unless @run_once
103
+ @run_once = DTROPTIONS[:packing_once]
104
+ break if !opt[:wait_for_reports] || complete?
105
+ sleep(DTROPTIONS[:wait_a_moment]) if DTROPTIONS[:wait_a_moment]
106
+ break if complete?
107
+ end
108
+ ensure
109
+ @report_signature = nil if opt[:wait_for_reports]
110
+ self.latest_build_time = Time.now - @build_start_time
111
+ end
112
+
113
+ def reports
114
+ @reports ||= {}
115
+ end
116
+
117
+ def client_setup_logs
118
+ @client_setup_logs ||= {}
119
+ end
120
+
121
+ def add_client(client)
122
+ clients_map[client.name] = client
123
+ client_setup_logs[client.name] = nil
124
+ end
125
+
126
+ def update_setup_log(signature, report)
127
+ with_valid_signature(signature) do
128
+ client_setup_logs[report[:client_name]] = report
129
+ if report[:exit_code] != 0
130
+ clients_map.delete(report[:client_name])
131
+ end
132
+ end
133
+ end
134
+
135
+ def update(signature, report)
136
+ with_valid_signature(signature) do
137
+ self.reports[report.test] = report
138
+ @report_signature = nil if @complete = self.reports.keys.size == @test_files.size
139
+ end
140
+ end
141
+
142
+ private
143
+ def run_start(opt)
144
+ @build_start_time = Time.now
145
+ if test_files.nil? || test_files.empty?
146
+ @complete = true
147
+ return
148
+ end
149
+ @report_signature = @build_start_time.to_s
150
+ @setup_clients = opt[:setup] ? [] : nil
151
+ end
152
+
153
+ def run_once
154
+ clients = available_clients
155
+
156
+ logger.debug { "run tests on #{clients.collect{|c|c.name}.join(', ')}"} unless clients.empty?
157
+ no_report_tests = test_files - reports.keys
158
+ return if clients.empty? || no_report_tests.empty?
159
+
160
+ packets = packer.pack no_report_tests, clients.size
161
+ clients.each do |client|
162
+ if @setup_clients && !@setup_clients.include?(client)
163
+ @setup_clients << client
164
+ client.setup(@report_signature)
165
+ end
166
+ client.run self.runner_name, packets.pop, @report_signature
167
+ end
168
+ end
169
+
170
+ def with_valid_signature(signature)
171
+ if !@report_signature.nil? && signature == @report_signature
172
+ yield
173
+ true
174
+ end
175
+ end
176
+
177
+ def collect_report(type)
178
+ "#{reports.values.collect{|r| r.send(type)}.inject(0){|r, i| r += i}} #{type}"
179
+ end
180
+
181
+ def clients_map
182
+ @clients_map ||= {}
183
+ end
184
+ end
185
+
186
+ class Client
187
+
188
+ RUNNERS = {RubyRunner.name => RubyRunner.new}
189
+
190
+ attr_reader :name
191
+
192
+ def initialize(server, name, setup_cmd=DTROPTIONS[:setup])
193
+ @server = server
194
+ @name = name
195
+ @executing_cmd = nil
196
+ @tmp_dir = DTROPTIONS[:tmp_dir]
197
+ @setup_cmd = setup_cmd
198
+ end
199
+
200
+ def idle?
201
+ @executing_cmd.nil?
202
+ end
203
+
204
+ def run(runner_name, test_files, signature)
205
+ RUNNERS[runner_name].run(@server, self, test_files, signature)
206
+ end
207
+
208
+ def setup(signature)
209
+ result = execute(@setup_cmd)
210
+ @server.update_setup_log(signature, result)
211
+ result
212
+ end
213
+
214
+ def server_signature
215
+ @server.server_signature
216
+ end
217
+
218
+ def completed_cmds
219
+ @completed_cmds ||= []
220
+ end
221
+
222
+ def execute(cmd)
223
+ @executing_cmd = cmd
224
+
225
+ outputs = {:stdout => "#{@tmp_dir}/execution_stdout.log", :stderr => "#{@tmp_dir}/execution_stderr.log"}
226
+ outputs.values.each do |log_file|
227
+ File.delete log_file if File.exist? log_file
228
+ end
229
+
230
+ begin
231
+ CommandLine.execute cmd, outputs
232
+ exit_code = 0
233
+ rescue CommandLine::ExecutionError => e
234
+ exit_code = e.exitstatus
235
+ end
236
+
237
+ outputs.each_key do |key|
238
+ outputs[key] = File.open(outputs[key], 'r') {|f| f.read}
239
+ end
240
+
241
+ self.completed_cmds << cmd
242
+ @executing_cmd = nil
243
+ {:client_name => @name, :stdout => outputs[:stdout], :stderr => outputs[:stderr], :exit_code => exit_code}
244
+ end
245
+ end
246
+
247
+ class TestReport
248
+ attr_reader :test, :stdout, :stderr, :exit_code, :client_name, :tests, :assertions, :failures, :errors
249
+ def initialize(test, execution_result, tests, assertions, failures, errors)
250
+ @test = test
251
+ @client_name = execution_result[:client_name]
252
+ @stdout = execution_result[:stdout]
253
+ @stderr = execution_result[:stderr]
254
+ @exit_code = execution_result[:exit_code]
255
+ @tests = tests
256
+ @assertions = assertions
257
+ @failures = failures
258
+ @errors = errors
259
+ end
260
+
261
+ def succeeded?
262
+ @failures == 0 && @errors == 0
263
+ end
264
+
265
+ def failed?
266
+ @failures > 0
267
+ end
268
+
269
+ def error?
270
+ @errors > 0
271
+ end
272
+
273
+ def ==(another)
274
+ test == another.test
275
+ end
276
+
277
+ def successes
278
+ tests - failures - errors
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,173 @@
1
+ # borrowed (with modifications) from the RSCM project
2
+ require 'rbconfig'
3
+ module Platform
4
+ def family
5
+ target_os = Config::CONFIG["target_os"] or raise 'Cannot determine operating system'
6
+ case target_os
7
+ when /linux/, /Linux/ then 'linux'
8
+ when /32/ then 'mswin32'
9
+ when /darwin/ then 'powerpc-darwin'
10
+ when /cyg/ then 'cygwin'
11
+ when /solaris/ then 'solaris'
12
+ when /freebsd/, /netbsd/ then 'bsd'
13
+ else raise "Unknown OS: #{target_os}"
14
+ end
15
+ end
16
+ module_function :family
17
+
18
+ def user
19
+ family == "mswin32" ? ENV['USERNAME'] : ENV['USER']
20
+ end
21
+ module_function :user
22
+
23
+ def prompt(dir=Dir.pwd)
24
+ prompt = "#{dir.gsub(/\//, File::SEPARATOR)} #{user}$"
25
+ end
26
+ module_function :prompt
27
+
28
+ end
29
+
30
+ module CommandLine
31
+ QUOTE_REPLACEMENT = (Platform.family == "mswin32") ? "\"" : "\\\""
32
+ LESS_THAN_REPLACEMENT = (Platform.family == "mswin32") ? "<" : "\\<"
33
+ class OptionError < StandardError; end
34
+ class ExecutionError < StandardError
35
+ attr_reader :cmd, :dir, :exitstatus, :stderr
36
+ def initialize(cmd, full_cmd, dir, exitstatus, stderr)
37
+ @cmd, @full_cmd, @dir, @exitstatus, @stderr = cmd, full_cmd, dir, exitstatus, stderr
38
+ end
39
+ def to_s
40
+ "\ndir : #{@dir}\n" +
41
+ "command : #{@cmd}\n" +
42
+ "executed command : #{@full_cmd}\n" +
43
+ "exitstatus: #{@exitstatus}\n" +
44
+ "STDERR TAIL START\n#{@stderr}\nSTDERR TAIL END\n"
45
+ end
46
+ end
47
+
48
+ # Executes +cmd+.
49
+ # If the +:stdout+ and +:stderr+ options are specified, a line consisting
50
+ # of a prompt (including +cmd+) will be appended to the respective output streams will be appended
51
+ # to those files, followed by the output itself. Example:
52
+ #
53
+ # CommandLine.execute("echo hello world", {:stdout => "stdout.log", :stderr => "stderr.log"})
54
+ #
55
+ # will result in the following being written to stdout.log:
56
+ #
57
+ # /Users/aslakhellesoy/scm/buildpatterns/repos/damagecontrol/trunk aslakhellesoy$ echo hello world
58
+ # hello world
59
+ #
60
+ # -and to stderr.log:
61
+ # /Users/aslakhellesoy/scm/buildpatterns/repos/damagecontrol/trunk aslakhellesoy$ echo hello world
62
+ #
63
+ # If a block is passed, the stdout io will be yielded to it (as with IO.popen). In this case the output
64
+ # will not be written to the stdout file (even if it's specified):
65
+ #
66
+ # /Users/aslakhellesoy/scm/buildpatterns/repos/damagecontrol/trunk aslakhellesoy$ echo hello world
67
+ # [output captured and therefore not logged]
68
+ #
69
+ # If the exitstatus of the command is different from the value specified by the +:exitstatus+ option
70
+ # (which defaults to 0) then an ExecutionError is raised, its message containing the last 400 bytes of stderr
71
+ # (provided +:stderr+ was specified)
72
+ #
73
+ # You can also specify the +:dir+ option, which will cause the command to be executed in that directory
74
+ # (default is current directory).
75
+ #
76
+ # You can also specify a hash of environment variables in +:env+, which will add additional environment variables
77
+ # to the default environment.
78
+ #
79
+ # Finally, you can specify several commands within one by separating them with '&&' (as you would in a shell).
80
+ # This will result in several lines to be appended to the log (as if you had executed the commands separately).
81
+ #
82
+ # See the unit test for more examples.
83
+ def execute(cmd, options={}, &proc)
84
+ raise "Can't have newline in cmd" if cmd =~ /\n/
85
+ options = {
86
+ :dir => Dir.pwd.untaint,
87
+ :env => {},
88
+ :mode => 'r',
89
+ :exitstatus => 0
90
+ }.merge(options)
91
+
92
+ options[:stdout] = %{"#{File.expand_path(options[:stdout])}"}.untaint if options[:stdout]
93
+ options[:stderr] = %{"#{File.expand_path(options[:stderr])}"}.untaint if options[:stderr]
94
+
95
+
96
+ if options[:dir].nil?
97
+ e(cmd, options, &proc)
98
+ else
99
+ Dir.chdir(options[:dir]) do
100
+ e(cmd, options, &proc)
101
+ end
102
+ end
103
+ end
104
+ module_function :execute
105
+
106
+ private
107
+
108
+ def full_cmd(cmd, options, &proc)
109
+ commands = cmd.split("&&").collect{|c| c.strip}
110
+ stdout_opt = options[:stdout] ? ">> #{options[:stdout]}" : ""
111
+ stderr_opt = options[:stderr] ? "2>> #{options[:stderr]}" : ""
112
+ capture_info_command = (block_given? && options[:stdout])? "echo [output captured and therefore not logged] >> #{options[:stdout]} && " : ""
113
+
114
+ full_cmd = commands.collect do |c|
115
+ escaped_command = c.gsub(/"/, QUOTE_REPLACEMENT).gsub(/</, LESS_THAN_REPLACEMENT)
116
+ stdout_prompt_command = options[:stdout] ? "echo #{Platform.prompt} #{escaped_command} >> #{options[:stdout]} && " : ""
117
+ stderr_prompt_command = options[:stderr] ? "echo #{Platform.prompt} #{escaped_command} >> #{options[:stderr]} && " : ""
118
+ redirected_command = block_given? ? "#{c} #{stderr_opt}" : "#{c} #{stdout_opt} #{stderr_opt}"
119
+
120
+ stdout_prompt_command + capture_info_command + stderr_prompt_command + redirected_command
121
+ end.join(" && ")
122
+ full_cmd.untaint
123
+ end
124
+ module_function :full_cmd
125
+
126
+ def verify_exit_code(cmd, full_cmd, options)
127
+ if($?.exitstatus != options[:exitstatus])
128
+ error_message = "#{options[:stderr]} doesn't exist"
129
+ if options[:stderr] && File.exist?(options[:stderr])
130
+ File.open(options[:stderr]) do |errio|
131
+ begin
132
+ errio.seek(-1200, IO::SEEK_END)
133
+ rescue Errno::EINVAL
134
+ # ignore - it just means we didn't have 400 bytes.
135
+ end
136
+ error_message = errio.read
137
+ end
138
+ end
139
+ raise ExecutionError.new(cmd, full_cmd, options[:dir] || Dir.pwd, $?.exitstatus, error_message)
140
+ end
141
+ end
142
+ module_function :verify_exit_code
143
+
144
+ def e(cmd, options, &proc)
145
+ full_cmd = full_cmd(cmd, options, &proc)
146
+
147
+ options[:env].each{|k,v| ENV[k]=v}
148
+ begin
149
+ STDOUT.puts "#{Platform.prompt} #{cmd}" if options[:stdout].nil?
150
+ IO.popen(full_cmd, options[:mode]) do |io|
151
+ if(block_given?)
152
+ proc.call(io)
153
+ else
154
+ io.each_line do |line|
155
+ STDOUT.puts line if options[:stdout].nil?
156
+ end
157
+ end
158
+ end
159
+ rescue Errno::ENOENT => e
160
+ unless options[:stderr].nil?
161
+ File.open(options[:stderr], "a") {|io| io.write(e.message)}
162
+ else
163
+ STDERR.puts e.message
164
+ STDERR.puts e.backtrace.join("\n")
165
+ end
166
+ raise ExecutionError.new(cmd, full_cmd, options[:dir] || Dir.pwd.untaint, nil, e.message)
167
+ ensure
168
+ verify_exit_code(cmd, full_cmd, options)
169
+ end
170
+ end
171
+ module_function :e
172
+
173
+ end