Class: Tinderbox::GemRunner

Inherits:
Object
  • Object
show all
Defined in:
lib/tinderbox/gem_runner.rb

Overview

Tinderbox::GemRunner tests a gem and creates a Tinderbox::Build holding the results of the test run.

You can use tinderbox_gem_build to test your gem in a sandbox.

Defined Under Namespace

Classes: RunTimeout

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(gem_name, gem_version, root = nil) ⇒ GemRunner

Creates a new GemRunner that will test the latest gem named gem using root for the sandbox. If no root is given, ./tinderbox is used for the sandbox.

Raises:

  • (ArgumentError)


61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/tinderbox/gem_runner.rb', line 61

def initialize(gem_name, gem_version, root = nil)
  root = File.join Dir.pwd, 'tinderbox' if root.nil?
  raise ArgumentError, 'root must not be relative' unless root[0] == ?/
  @sandbox_dir = File.expand_path File.join(root, 'sandbox')
  @cache_dir = File.expand_path File.join(root, 'cache')
  FileUtils.mkpath @cache_dir unless File.exist? @cache_dir

  ENV['GEM_HOME'] = nil
  Gem.clear_paths

  @host_gem_dir = Gem.dir
  @host_gem_source_index = Gem::SourceInfoCache.new.cache_file
  @gem_name = gem_name
  @gem_version = gem_version

  @remote_installer = Gem::RemoteInstaller.new :include_dependencies => true,
                                               :cache_dir => @cache_dir
  @remote_installer.ui = Gem::SilentUI.new
  @gemspec = nil
  @installed_gems = nil

  @timeout = 120

  @log = ''
  @duration = 0
  @successful = :not_tested
end

Instance Attribute Details

#gem_nameObject (readonly)

Name of the gem to test



39
40
41
# File 'lib/tinderbox/gem_runner.rb', line 39

def gem_name
  @gem_name
end

#gem_versionObject (readonly)

Version of the gem to test



44
45
46
# File 'lib/tinderbox/gem_runner.rb', line 44

def gem_version
  @gem_version
end

#gemspecObject (readonly)

Gemspec of the gem to test



49
50
51
# File 'lib/tinderbox/gem_runner.rb', line 49

def gemspec
  @gemspec
end

#host_gem_dirObject (readonly)

Host’s gem repository directory



34
35
36
# File 'lib/tinderbox/gem_runner.rb', line 34

def host_gem_dir
  @host_gem_dir
end

#sandbox_dirObject (readonly)

Sandbox directory for rubygems



29
30
31
# File 'lib/tinderbox/gem_runner.rb', line 29

def sandbox_dir
  @sandbox_dir
end

#timeoutObject

Maximum time to wait for run_command to complete



54
55
56
# File 'lib/tinderbox/gem_runner.rb', line 54

def timeout
  @timeout
end

Instance Method Details

#gem_lib_pathsObject

The gem’s library paths.



92
93
94
# File 'lib/tinderbox/gem_runner.rb', line 92

def gem_lib_paths
  @gemspec.require_paths.join Config::CONFIG['PATH_SEPARATOR']
end

#installObject

Install the gem into the sandbox.



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/tinderbox/gem_runner.rb', line 99

def install
  retries = 5

  begin
    @installed_gems = @remote_installer.install @gem_name, @gem_version
    @gemspec = @installed_gems.first
    "### #{@installed_gems.map { |s| s.full_name }.join "\n### "}"
  rescue Gem::RemoteInstallationCancelled => e
    raise Tinderbox::ManualInstallError,
          "Installation of #{@gem_name}-#{@gem_version} requires manual intervention"
  rescue Gem::Installer::ExtensionBuildError => e
    raise Tinderbox::BuildError, "Unable to build #{@gem_name}-#{@gem_version}:\n\n#{e.message}"
  rescue Gem::InstallError, Gem::GemNotFoundException => e
    FileUtils.rm_rf File.join(@cache_dir, "#{@gem_name}-#{@gem_version}.gem")
    raise Tinderbox::InstallError,
          "Installation of #{@gem_name}-#{@gem_version} failed (#{e.class}):\n\n#{e.message}"
  rescue SystemCallError => e # HACK push into Rubygems
    retries -= 1
    retry if retries >= 0
    raise Tinderbox::InstallError,
          "Installation of #{@gem_name}-#{@gem_version} failed after 5 tries"
  rescue OpenURI::HTTPError => e # HACK push into Rubygems
    raise Tinderbox::InstallError,
          "Could not download #{@gem_name}-#{@gem_version}"
  rescue SystemStackError => e
    raise Tinderbox::InstallError,
          "Installation of #{@gem_name}-#{@gem_version} caused an infinite loop:\n\n\t#{e.backtrace.join "\n\t"}"
  end
end

#install_rakeObject

Installs the rake gem into the sandbox



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/tinderbox/gem_runner.rb', line 144

def install_rake
  log = []
  log << "!!! HAS Rakefile, DOES NOT DEPEND ON RAKE!  NEEDS s.add_dependency 'rake'"

  retries = 5

  rake_version = Gem::SourceInfoCache.search(/^rake$/).last.version.to_s

  begin
    @installed_gems.push(*@remote_installer.install('rake', rake_version))
    log << "### rake installed, even though you claim not to need it"
  rescue Gem::InstallError, Gem::GemNotFoundException => e
    log << "Installation of rake failed (#{e.class}):\n\n#{e.message}"
  rescue SystemCallError => e
    retries -= 1
    retry if retries >= 0
    log << "Installation of rake failed after 5 tries"
  rescue OpenURI::HTTPError => e
    log << "Could not download rake"
  end

  @log << (log.join("\n") + "\n")
end

#install_rspec(message) ⇒ Object

Installs the RSpec gem into the sandbox



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/tinderbox/gem_runner.rb', line 171

def install_rspec(message)
  log = []
  log << "!!! HAS #{message}, DOES NOT DEPEND ON RSPEC!  NEEDS s.add_dependency 'rspec'"

  retries = 5

  rspec_version = Gem::SourceInfoCache.search(/^rspec$/).last.version.to_s

  begin
    @installed_gems.push(*@remote_installer.install('rspec', rspec_version))
    log << "### RSpec installed, even though you claim not to need it"
  rescue Gem::InstallError, Gem::GemNotFoundException => e
    log << "Installation of RSpec failed (#{e.class}):\n\n#{e.message}"
  rescue SystemCallError => e
    retries -= 1
    retry if retries >= 0
    log << "Installation of RSpec failed after 5 tries"
  rescue OpenURI::HTTPError => e
    log << "Could not download rspec"
  end

  @log << (log.join("\n") + "\n")
end

#install_sourcesObject

Install the sources gem into the sandbox gem repository.



132
133
134
135
136
137
138
139
# File 'lib/tinderbox/gem_runner.rb', line 132

def install_sources
  sources_gem = Dir[File.join(@host_gem_dir, 'cache', 'sources-*gem')].max

  installer = Gem::Installer.new sources_gem
  installer.install

  FileUtils.copy @host_gem_source_index, Gem::SourceInfoCache.new.cache_file
end

#passed?(process_status) ⇒ Boolean

Checks to see if #process_status exited successfully, ran at least one assertion or specification and the run finished without error or failure.

Returns:

  • (Boolean)


199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/tinderbox/gem_runner.rb', line 199

def passed?(process_status)
  tested = @log =~ /^\d+ tests, \d+ assertions, \d+ failures, \d+ errors$/ ||
           @log =~ /^\d+ specifications?, \d+ failures?$/
  @successful = process_status.exitstatus == 0

  if not tested and @successful then
    @successful = false
    return tested
  end

  if @log =~ / (\d+) failures, (\d+) errors/ and ($1 != '0' or $2 != '0') then
    @log << "!!! Project has broken test target, exited with 0 after test failure\n" if @successful
    @successful = false
  elsif @log =~ /\d+ specifications?, (\d+) failures?$/ and $1 != '0' then
    @log << "!!! Project has broken spec target, exited with 0 after spec failure\n" if @successful
    @successful = false
  elsif (@log =~ / 0 assertions/ or @log !~ / \d+ assertions/) and
        (@log =~ /0 specifications/ or @log !~ /\d+ specification/) then
    @successful = false
    @log << "!!! No output indicating success found\n"
  end

  return tested
end

#rake_installed?Boolean

Checks to see if the rake gem was installed by the gem under test

Returns:

  • (Boolean)


227
228
229
230
# File 'lib/tinderbox/gem_runner.rb', line 227

def rake_installed?
  raise 'you haven\'t installed anything yet' if @installed_gems.nil?
  @installed_gems.any? { |s| s.name == 'rake' }
end

#rspec_installed?Boolean

Checks to see if the rspec gem was installed by the gem under test

Returns:

  • (Boolean)


235
236
237
238
# File 'lib/tinderbox/gem_runner.rb', line 235

def rspec_installed?
  raise 'you haven\'t installed anything yet' if @installed_gems.nil?
  @installed_gems.any? { |s| s.name == 'rspec' }
end

#rubyObject

Path to ruby



243
244
245
246
# File 'lib/tinderbox/gem_runner.rb', line 243

def ruby
  ruby_exe = Config::CONFIG['ruby_install_name'] + Config::CONFIG['EXEEXT']
  File.join Config::CONFIG['bindir'], ruby_exe
end

#runObject

Sets up a sandbox, installs the gem, runs the tests and returns a Build object.



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/tinderbox/gem_runner.rb', line 252

def run
  sandbox_cleanup # don't clean up at the end so we can review
  sandbox_setup
  install_sources

  build = Tinderbox::Build.new
  full_log = []
  run_log = nil

  full_log << "### installing #{@gem_name}-#{@gem_version} + dependencies"
  full_log << install

  full_log << "### testing #{@gemspec.full_name}"
  test
  full_log << @log

  build.duration = @duration
  build.successful = @successful
  build.log = full_log.join "\n"

  return build
end

#run_command(command) ⇒ Object

Runs shell command command and records the command’s output and the time it took to run. Returns true if evidence of a test run were found in the command output.



280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/tinderbox/gem_runner.rb', line 280

def run_command(command)
  start = Time.now
  @log << "### #{command}\n"
  begin
    Timeout.timeout @timeout, RunTimeout do
      @log << `#{command} 2>&1`
    end
  rescue RunTimeout
    @log << "!!! failed to complete in under #{@timeout} seconds\n"
    `ruby -e 'exit 1'` # force $?
  end
  @duration += Time.now - start

  passed? $CHILD_STATUS
end

#sandbox_cleanupObject

Cleans up the gem sandbox.



299
300
301
302
303
# File 'lib/tinderbox/gem_runner.rb', line 299

def sandbox_cleanup
  FileUtils.remove_dir @sandbox_dir rescue nil

  raise "#{@sandbox_dir} not empty" if File.exist? @sandbox_dir
end

#sandbox_setupObject

Sets up a new gem sandbox.



308
309
310
311
312
313
314
315
316
317
# File 'lib/tinderbox/gem_runner.rb', line 308

def sandbox_setup
  raise "#{@sandbox_dir} already exists" if
    File.exist? @sandbox_dir

  FileUtils.mkpath @sandbox_dir
  FileUtils.mkpath File.join(@sandbox_dir, 'gems')

  ENV['GEM_HOME'] = @sandbox_dir
  Gem.clear_paths
end

#testObject

Tries a best-effort at running the tests or specifications for a gem. The following commands are tried, and #test stops on the first evidence of a test run.

  1. rake test

  2. rake spec

  3. make test

  4. ruby -Ilib -S testrb test

  5. spec spec/*



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/tinderbox/gem_runner.rb', line 330

def test
  Dir.chdir @gemspec.full_gem_path do
    if File.exist? 'Rakefile' then
      install_rake unless rake_installed?
      return if run_command "#{ruby} -S rake test"
    end

    if File.exist? 'Rakefile' and `rake -T` =~ /^rake spec/ then
      install_rspec '`rake spec`' unless rspec_installed?
      return if run_command "#{ruby} -S rake spec"
    end

    if File.exist? 'Makefile' then
      return if run_command 'make test'
    end

    if File.directory? 'test' then
      return if run_command "#{ruby} -I#{gem_lib_paths} -S #{testrb} test"
    end

    if File.directory? 'spec' then
      install_rspec 'spec DIRECTORY' unless rake_installed?
      return if run_command "#{ruby} -S spec spec/*"
    end

    @log << "!!! could not figure out how to test #{@gemspec.full_name}"
    @successful = false
  end
end

#testrbObject

Path to testrb



363
364
365
366
# File 'lib/tinderbox/gem_runner.rb', line 363

def testrb
  testrb_exe = 'testrb' + (RUBY_PLATFORM =~ /mswin/ ? '.bat' : '')
  File.join Config::CONFIG['bindir'], testrb_exe
end