tinderbox 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ == 1.0.0 / 2007-01-30
2
+
3
+ * Tests gems in a sandbox
4
+ * Submits results to Firebrigade
5
+ * Birthday!
6
+
@@ -0,0 +1,27 @@
1
+ Copyright 2006 Eric Hodel. All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions
5
+ are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright
8
+ notice, this list of conditions and the following disclaimer.
9
+ 2. Redistributions in binary form must reproduce the above copyright
10
+ notice, this list of conditions and the following disclaimer in the
11
+ documentation and/or other materials provided with the distribution.
12
+ 3. Neither the names of the authors nor the names of their contributors
13
+ may be used to endorse or promote products derived from this software
14
+ without specific prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
17
+ OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19
+ ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
20
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
21
+ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
22
+ OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
23
+ BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
24
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
25
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
26
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
+
@@ -0,0 +1,14 @@
1
+ History.txt
2
+ LICENSE.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ bin/tinderbox_gem_build
7
+ bin/tinderbox_gem_run
8
+ lib/tinderbox.rb
9
+ lib/tinderbox/build.rb
10
+ lib/tinderbox/gem_runner.rb
11
+ lib/tinderbox/gem_tinderbox.rb
12
+ test/test_tinderbox_build.rb
13
+ test/test_tinderbox_gem_runner.rb
14
+ test/test_tinderbox_gem_tinderbox.rb
@@ -0,0 +1,33 @@
1
+ = Tinderbox
2
+
3
+ by Eric Hodel
4
+
5
+ http://seattlerb.rubyforge.org/tinderbox
6
+
7
+ == DESCRIPTION:
8
+
9
+ Tinderbox tests projects and tries to make them break by running them on as
10
+ many different platforms as possible.
11
+
12
+ == FEATURES/PROBLEMS:
13
+
14
+ * Tests gems in a sandbox
15
+ * Submits gem test results to http://firebrigade.seattlerb.org
16
+ * Understands test/unit and RSpec
17
+
18
+ == SYNOPSYS:
19
+
20
+ tinderbox_gem_run -u 'my username' -p 'my password' -s tinderbox.example.com
21
+
22
+ == REQUIREMENTS:
23
+
24
+ * RubyGems 0.9.1
25
+ * firebrigade_api
26
+ * RSpec
27
+ * Rake
28
+ * Connection to the internet
29
+
30
+ == INSTALL:
31
+
32
+ * sudo gem install tinderbox
33
+
@@ -0,0 +1,21 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/tinderbox.rb'
6
+
7
+ Hoe.new 'tinderbox', Tinderbox::VERSION do |p|
8
+ p.rubyforge_name = 'seattlerb'
9
+ p.summary = 'Tinderbox says, "I\'m gonna light you on fire."'
10
+ p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
11
+ p.url = p.paragraphs_of('README.txt', 0)[2]
12
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
13
+ p.author = 'Eric Hodel'
14
+ p.email = 'drbrain@segment7.net'
15
+
16
+ p.extra_deps << ['ZenTest', '>= 3.4.0']
17
+ p.extra_deps << ['firebrigade_api', '>= 1.0.0']
18
+ p.extra_deps << ['rspec', '>= 0.7.5.1']
19
+ end
20
+
21
+ # vim: syntax=Ruby
@@ -0,0 +1,14 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ require 'rubygems'
4
+ require 'tinderbox/gem_runner'
5
+
6
+ gem_name = ARGV.shift or raise 'Need gem name'
7
+ gem_version = ARGV.shift or raise 'Need gem version'
8
+
9
+ build = Tinderbox::GemRunner.new(gem_name, gem_version).run
10
+ puts "build succeeded: #{build.successful}"
11
+ puts "build duration: #{build.duration}"
12
+ puts "build log:"
13
+ puts build.log
14
+
@@ -0,0 +1,7 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ require 'rubygems'
4
+ require 'tinderbox/gem_tinderbox'
5
+
6
+ Tinderbox::GemTinderbox.run
7
+
@@ -0,0 +1,34 @@
1
+ ##
2
+ # Tinderbox tests gems in a sandbox. See Tinderbox::GemRunner and
3
+ # Tinderbox::GemTinderbox for further details.
4
+
5
+ module Tinderbox
6
+
7
+ ##
8
+ # This is the version of Tinderbox you are currently running.
9
+
10
+ VERSION = '1.0.0'
11
+
12
+ ##
13
+ # Indicates an error while installing software we're going to test.
14
+
15
+ class InstallError < RuntimeError; end
16
+
17
+ ##
18
+ # Indicates an error while building extensions for a gem we're going to
19
+ # test.
20
+
21
+ class BuildError < InstallError; end
22
+
23
+ ##
24
+ # Indicates an installation that cannot be performed automatically.
25
+
26
+ class ManualInstallError < InstallError; end
27
+
28
+ ##
29
+ # A Struct that holds information about a Build.
30
+
31
+ Build = Struct.new :successful, :duration, :log
32
+
33
+ end
34
+
@@ -0,0 +1,22 @@
1
+ require 'tinderbox'
2
+
3
+ require 'rubygems'
4
+ require 'firebrigade/api'
5
+
6
+ ##
7
+ # A set of Build results.
8
+
9
+ class Tinderbox::Build
10
+
11
+ ##
12
+ # Submit a Build to +host+ as +username+ using +password+ using
13
+ # Firebrigade::API. The Build will be added to project +project_id+ and
14
+ # target +target_id+.
15
+
16
+ def submit(project_id, target_id, host, username, password)
17
+ fa = Firebrigade::API.new host, username, password
18
+ fa.add_build project_id, target_id, successful, duration, log
19
+ end
20
+
21
+ end
22
+
@@ -0,0 +1,369 @@
1
+ require 'tinderbox'
2
+
3
+ require 'English'
4
+ require 'fileutils'
5
+ require 'open-uri'
6
+ require 'rbconfig'
7
+ require 'stringio'
8
+ require 'timeout'
9
+
10
+ require 'rubygems'
11
+ require 'rubygems/remote_installer'
12
+
13
+ ##
14
+ # Tinderbox::GemRunner tests a gem and creates a Tinderbox::Build holding the
15
+ # results of the test run.
16
+ #
17
+ # You can use tinderbox_gem_build to test your gem in a sandbox.
18
+
19
+ class Tinderbox::GemRunner
20
+
21
+ ##
22
+ # Raised when the tinderbox job times out.
23
+
24
+ class RunTimeout < Timeout::Error; end
25
+
26
+ ##
27
+ # Sandbox directory for rubygems
28
+
29
+ attr_reader :sandbox_dir
30
+
31
+ ##
32
+ # Host's gem repository directory
33
+
34
+ attr_reader :host_gem_dir
35
+
36
+ ##
37
+ # Name of the gem to test
38
+
39
+ attr_reader :gem_name
40
+
41
+ ##
42
+ # Version of the gem to test
43
+
44
+ attr_reader :gem_version
45
+
46
+ ##
47
+ # Gemspec of the gem to test
48
+
49
+ attr_reader :gemspec
50
+
51
+ ##
52
+ # Maximum time to wait for run_command to complete
53
+
54
+ attr_accessor :timeout
55
+
56
+ ##
57
+ # Creates a new GemRunner that will test the latest gem named +gem+ using
58
+ # +root+ for the sandbox. If no +root+ is given, ./tinderbox is used for
59
+ # the sandbox.
60
+
61
+ def initialize(gem_name, gem_version, root = nil)
62
+ root = File.join Dir.pwd, 'tinderbox' if root.nil?
63
+ raise ArgumentError, 'root must not be relative' unless root[0] == ?/
64
+ @sandbox_dir = File.expand_path File.join(root, 'sandbox')
65
+ @cache_dir = File.expand_path File.join(root, 'cache')
66
+ FileUtils.mkpath @cache_dir unless File.exist? @cache_dir
67
+
68
+ ENV['GEM_HOME'] = nil
69
+ Gem.clear_paths
70
+
71
+ @host_gem_dir = Gem.dir
72
+ @host_gem_source_index = Gem::SourceInfoCache.new.cache_file
73
+ @gem_name = gem_name
74
+ @gem_version = gem_version
75
+
76
+ @remote_installer = Gem::RemoteInstaller.new :include_dependencies => true,
77
+ :cache_dir => @cache_dir
78
+ @remote_installer.ui = Gem::SilentUI.new
79
+ @gemspec = nil
80
+ @installed_gems = nil
81
+
82
+ @timeout = 120
83
+
84
+ @log = ''
85
+ @duration = 0
86
+ @successful = :not_tested
87
+ end
88
+
89
+ ##
90
+ # The gem's library paths.
91
+
92
+ def gem_lib_paths
93
+ @gemspec.require_paths.join Config::CONFIG['PATH_SEPARATOR']
94
+ end
95
+
96
+ ##
97
+ # Install the gem into the sandbox.
98
+
99
+ def install
100
+ retries = 5
101
+
102
+ begin
103
+ @installed_gems = @remote_installer.install @gem_name, @gem_version
104
+ @gemspec = @installed_gems.first
105
+ "### #{@installed_gems.map { |s| s.full_name }.join "\n### "}"
106
+ rescue Gem::RemoteInstallationCancelled => e
107
+ raise Tinderbox::ManualInstallError,
108
+ "Installation of #{@gem_name}-#{@gem_version} requires manual intervention"
109
+ rescue Gem::Installer::ExtensionBuildError => e
110
+ raise Tinderbox::BuildError, "Unable to build #{@gem_name}-#{@gem_version}:\n\n#{e.message}"
111
+ rescue Gem::InstallError, Gem::GemNotFoundException => e
112
+ FileUtils.rm_rf File.join(@cache_dir, "#{@gem_name}-#{@gem_version}.gem")
113
+ raise Tinderbox::InstallError,
114
+ "Installation of #{@gem_name}-#{@gem_version} failed (#{e.class}):\n\n#{e.message}"
115
+ rescue SystemCallError => e # HACK push into Rubygems
116
+ retries -= 1
117
+ retry if retries >= 0
118
+ raise Tinderbox::InstallError,
119
+ "Installation of #{@gem_name}-#{@gem_version} failed after 5 tries"
120
+ rescue OpenURI::HTTPError => e # HACK push into Rubygems
121
+ raise Tinderbox::InstallError,
122
+ "Could not download #{@gem_name}-#{@gem_version}"
123
+ rescue SystemStackError => e
124
+ raise Tinderbox::InstallError,
125
+ "Installation of #{@gem_name}-#{@gem_version} caused an infinite loop:\n\n\t#{e.backtrace.join "\n\t"}"
126
+ end
127
+ end
128
+
129
+ ##
130
+ # Install the sources gem into the sandbox gem repository.
131
+
132
+ def install_sources
133
+ sources_gem = Dir[File.join(@host_gem_dir, 'cache', 'sources-*gem')].max
134
+
135
+ installer = Gem::Installer.new sources_gem
136
+ installer.install
137
+
138
+ FileUtils.copy @host_gem_source_index, Gem::SourceInfoCache.new.cache_file
139
+ end
140
+
141
+ ##
142
+ # Installs the rake gem into the sandbox
143
+
144
+ def install_rake
145
+ log = []
146
+ log << "!!! HAS Rakefile, DOES NOT DEPEND ON RAKE! NEEDS s.add_dependency 'rake'"
147
+
148
+ retries = 5
149
+
150
+ rake_version = Gem::SourceInfoCache.search(/^rake$/).last.version.to_s
151
+
152
+ begin
153
+ @installed_gems.push(*@remote_installer.install('rake', rake_version))
154
+ log << "### rake installed, even though you claim not to need it"
155
+ rescue Gem::InstallError, Gem::GemNotFoundException => e
156
+ log << "Installation of rake failed (#{e.class}):\n\n#{e.message}"
157
+ rescue SystemCallError => e
158
+ retries -= 1
159
+ retry if retries >= 0
160
+ log << "Installation of rake failed after 5 tries"
161
+ rescue OpenURI::HTTPError => e
162
+ log << "Could not download rake"
163
+ end
164
+
165
+ @log << (log.join("\n") + "\n")
166
+ end
167
+
168
+ ##
169
+ # Installs the RSpec gem into the sandbox
170
+
171
+ def install_rspec(message)
172
+ log = []
173
+ log << "!!! HAS #{message}, DOES NOT DEPEND ON RSPEC! NEEDS s.add_dependency 'rspec'"
174
+
175
+ retries = 5
176
+
177
+ rspec_version = Gem::SourceInfoCache.search(/^rspec$/).last.version.to_s
178
+
179
+ begin
180
+ @installed_gems.push(*@remote_installer.install('rspec', rspec_version))
181
+ log << "### RSpec installed, even though you claim not to need it"
182
+ rescue Gem::InstallError, Gem::GemNotFoundException => e
183
+ log << "Installation of RSpec failed (#{e.class}):\n\n#{e.message}"
184
+ rescue SystemCallError => e
185
+ retries -= 1
186
+ retry if retries >= 0
187
+ log << "Installation of RSpec failed after 5 tries"
188
+ rescue OpenURI::HTTPError => e
189
+ log << "Could not download rspec"
190
+ end
191
+
192
+ @log << (log.join("\n") + "\n")
193
+ end
194
+
195
+ ##
196
+ # Checks to see if #process_status exited successfully, ran at least one
197
+ # assertion or specification and the run finished without error or failure.
198
+
199
+ def passed?(process_status)
200
+ tested = @log =~ /^\d+ tests, \d+ assertions, \d+ failures, \d+ errors$/ ||
201
+ @log =~ /^\d+ specifications?, \d+ failures?$/
202
+ @successful = process_status.exitstatus == 0
203
+
204
+ if not tested and @successful then
205
+ @successful = false
206
+ return tested
207
+ end
208
+
209
+ if @log =~ / (\d+) failures, (\d+) errors/ and ($1 != '0' or $2 != '0') then
210
+ @log << "!!! Project has broken test target, exited with 0 after test failure\n" if @successful
211
+ @successful = false
212
+ elsif @log =~ /\d+ specifications?, (\d+) failures?$/ and $1 != '0' then
213
+ @log << "!!! Project has broken spec target, exited with 0 after spec failure\n" if @successful
214
+ @successful = false
215
+ elsif (@log =~ / 0 assertions/ or @log !~ / \d+ assertions/) and
216
+ (@log =~ /0 specifications/ or @log !~ /\d+ specification/) then
217
+ @successful = false
218
+ @log << "!!! No output indicating success found\n"
219
+ end
220
+
221
+ return tested
222
+ end
223
+
224
+ ##
225
+ # Checks to see if the rake gem was installed by the gem under test
226
+
227
+ def rake_installed?
228
+ raise 'you haven\'t installed anything yet' if @installed_gems.nil?
229
+ @installed_gems.any? { |s| s.name == 'rake' }
230
+ end
231
+
232
+ ##
233
+ # Checks to see if the rspec gem was installed by the gem under test
234
+
235
+ def rspec_installed?
236
+ raise 'you haven\'t installed anything yet' if @installed_gems.nil?
237
+ @installed_gems.any? { |s| s.name == 'rspec' }
238
+ end
239
+
240
+ ##
241
+ # Path to ruby
242
+
243
+ def ruby
244
+ ruby_exe = Config::CONFIG['ruby_install_name'] + Config::CONFIG['EXEEXT']
245
+ File.join Config::CONFIG['bindir'], ruby_exe
246
+ end
247
+
248
+ ##
249
+ # Sets up a sandbox, installs the gem, runs the tests and returns a Build
250
+ # object.
251
+
252
+ def run
253
+ sandbox_cleanup # don't clean up at the end so we can review
254
+ sandbox_setup
255
+ install_sources
256
+
257
+ build = Tinderbox::Build.new
258
+ full_log = []
259
+ run_log = nil
260
+
261
+ full_log << "### installing #{@gem_name}-#{@gem_version} + dependencies"
262
+ full_log << install
263
+
264
+ full_log << "### testing #{@gemspec.full_name}"
265
+ test
266
+ full_log << @log
267
+
268
+ build.duration = @duration
269
+ build.successful = @successful
270
+ build.log = full_log.join "\n"
271
+
272
+ return build
273
+ end
274
+
275
+ ##
276
+ # Runs shell command +command+ and records the command's output and the time
277
+ # it took to run. Returns true if evidence of a test run were found in the
278
+ # command output.
279
+
280
+ def run_command(command)
281
+ start = Time.now
282
+ @log << "### #{command}\n"
283
+ begin
284
+ Timeout.timeout @timeout, RunTimeout do
285
+ @log << `#{command} 2>&1`
286
+ end
287
+ rescue RunTimeout
288
+ @log << "!!! failed to complete in under #{@timeout} seconds\n"
289
+ `ruby -e 'exit 1'` # force $?
290
+ end
291
+ @duration += Time.now - start
292
+
293
+ passed? $CHILD_STATUS
294
+ end
295
+
296
+ ##
297
+ # Cleans up the gem sandbox.
298
+
299
+ def sandbox_cleanup
300
+ FileUtils.remove_dir @sandbox_dir rescue nil
301
+
302
+ raise "#{@sandbox_dir} not empty" if File.exist? @sandbox_dir
303
+ end
304
+
305
+ ##
306
+ # Sets up a new gem sandbox.
307
+
308
+ def sandbox_setup
309
+ raise "#{@sandbox_dir} already exists" if
310
+ File.exist? @sandbox_dir
311
+
312
+ FileUtils.mkpath @sandbox_dir
313
+ FileUtils.mkpath File.join(@sandbox_dir, 'gems')
314
+
315
+ ENV['GEM_HOME'] = @sandbox_dir
316
+ Gem.clear_paths
317
+ end
318
+
319
+ ##
320
+ # Tries a best-effort at running the tests or specifications for a gem. The
321
+ # following commands are tried, and #test stops on the first evidence of a
322
+ # test run.
323
+ #
324
+ # 1. rake test
325
+ # 2. rake spec
326
+ # 3. make test
327
+ # 4. ruby -Ilib -S testrb test
328
+ # 5. spec spec/*
329
+
330
+ def test
331
+ Dir.chdir @gemspec.full_gem_path do
332
+ if File.exist? 'Rakefile' then
333
+ install_rake unless rake_installed?
334
+ return if run_command "#{ruby} -S rake test"
335
+ end
336
+
337
+ if File.exist? 'Rakefile' and `rake -T` =~ /^rake spec/ then
338
+ install_rspec '`rake spec`' unless rspec_installed?
339
+ return if run_command "#{ruby} -S rake spec"
340
+ end
341
+
342
+ if File.exist? 'Makefile' then
343
+ return if run_command 'make test'
344
+ end
345
+
346
+ if File.directory? 'test' then
347
+ return if run_command "#{ruby} -I#{gem_lib_paths} -S #{testrb} test"
348
+ end
349
+
350
+ if File.directory? 'spec' then
351
+ install_rspec 'spec DIRECTORY' unless rake_installed?
352
+ return if run_command "#{ruby} -S spec spec/*"
353
+ end
354
+
355
+ @log << "!!! could not figure out how to test #{@gemspec.full_name}"
356
+ @successful = false
357
+ end
358
+ end
359
+
360
+ ##
361
+ # Path to testrb
362
+
363
+ def testrb
364
+ testrb_exe = 'testrb' + (RUBY_PLATFORM =~ /mswin/ ? '.bat' : '')
365
+ File.join Config::CONFIG['bindir'], testrb_exe
366
+ end
367
+
368
+ end
369
+