tinderbox 1.0.0

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.
@@ -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
+