Permalink
Browse files

Merge remote-tracking branch 'leonid-shevtsov/master'

  • Loading branch information...
2 parents 11654ef + 7cde9f7 commit 19906a3306c5d262692f18ba9c51c78fd6bc8464 @gshakhn committed Mar 30, 2012
Showing with 136 additions and 33 deletions.
  1. +7 −0 .travis.yml
  2. +5 −0 CHANGELOG
  3. +13 −2 README.md
  4. +1 −1 headless.gemspec
  5. +40 −11 lib/headless.rb
  6. +2 −0 lib/headless/cli_util.rb
  7. +55 −8 spec/headless_spec.rb
  8. +0 −3 spec/spec_helper.rb
  9. +13 −8 spec/video_recorder_spec.rb
View
@@ -0,0 +1,7 @@
+language: ruby
+rvm:
+ - 1.8.7
+ - 1.9.2
+ - 1.9.3
+ - ree
+script: "rspec"
View
@@ -1,3 +1,8 @@
+## 0.3.1 (2012-03-29)
+
+* added autopicking of display number, if the requested one is already taken
+* fixed plenty of bugs thanks to @recursive, @gshakhn, @masatomo and @mabotelh
+
## 0.2.2 (2011-09-01)
* improve detection of ffmpeg process (from https://github.com/alanshields/headless)
View
@@ -1,13 +1,15 @@
-# Headless
+# Headless [![Travis CI status](https://secure.travis-ci.org/leonid-shevtsov/headless.png)](http://travis-ci.org/leonid-shevtsov/headless)
-Headless is a Ruby interface for Xvfb. It allows you to create a headless display straight from Ruby code, hiding some low-level action.
+Headless is *the* Ruby interface for Xvfb. It allows you to create a headless display straight from Ruby code, hiding some low-level action.
It can also capture images and video from the virtual framebuffer.
I created it so I can run Selenium tests in Cucumber without any shell scripting. Even more, you can go headless only when you run tests against Selenium.
Other possible uses include pdf generation with `wkhtmltopdf`, or screenshotting.
Documentation is available at [rdoc.info](http://rdoc.info/projects/leonid-shevtsov/headless)
+[Changelog](https://github.com/leonid-shevtsov/headless/blob/master/CHANGELOG)
+
## Installation
On Debian/Ubuntu:
@@ -57,6 +59,15 @@ Running cucumber headless is now as simple as adding a before and after hook in
headless.start
end
+## Cucumber with wkhtmltopdf
+
+_Note: this is true for other programs which may use headless at the same time as cucumber is running_
+
+When wkhtmltopdf is using Headless, and cucumber is invoking a block of code which uses a headless session, make sure to override the default display of cucumber to retain browser focus. Assuming wkhtmltopdf is using the default display of 99, make sure to set the display to a value != 99 in `features/support/env.rb` file. This may be the cause of `Connection refused - connect(2) (Errno::ECONNREFUSED)`.
+
+ headless = Headless.new(:display => '100')
+ headless.start
+
## Capturing video
Video is captured using `ffmpeg`. You can install it on Debian/Ubuntu via `sudo apt-get install ffmpeg` or on OS X via `brew install ffmpeg`. You can capture video continuously or capture scenarios separately. Here is typical use case:
View
@@ -3,7 +3,7 @@ spec = Gem::Specification.new do |s|
s.email = 'leonid@shevtsov.me'
s.name = 'headless'
- s.version = '0.2.2'
+ s.version = '0.3.1'
s.summary = 'Ruby headless display interface'
s.description = <<-EOF
View
@@ -42,6 +42,9 @@
#++
class Headless
+ DEFAULT_DISPLAY_NUMBER = 99
+ DEFAULT_DISPLAY_DIMENSIONS = '1280x1024x24'
+
class Exception < RuntimeError
end
@@ -55,26 +58,21 @@ class Exception < RuntimeError
#
# List of available options:
# * +display+ (default 99) - what display number to listen to;
- # * +reuse+ (default true) - if given display server already exists, should we use it or fail miserably?
+ # * +reuse+ (default true) - if given display server already exists, should we use it or try another?
+ # * +autopick+ (default true is display number isn't explicitly set) - if Headless should automatically pick a display, or fail if the given one is not available.
# * +dimensions+ (default 1280x1024x24) - display dimensions and depth. Not all combinations are possible, refer to +man Xvfb+.
# * +destroy_at_exit+ (default true) - if a display is started but not stopped, should it be destroyed when the script finishes?
def initialize(options = {})
CliUtil.ensure_application_exists!('Xvfb', 'Xvfb not found on your system')
- @display = options.fetch(:display, 99).to_i
+ @display = options.fetch(:display, DEFAULT_DISPLAY_NUMBER).to_i
+ @autopick_display = options.fetch(:autopick, !options.key?(:display))
@reuse_display = options.fetch(:reuse, true)
- @dimensions = options.fetch(:dimensions, '1280x1024x24')
+ @dimensions = options.fetch(:dimensions, DEFAULT_DISPLAY_DIMENSIONS)
@video_capture_options = options.fetch(:video, {})
@destroy_at_exit = options.fetch(:destroy_at_exit, true)
- #TODO more logic here, autopicking the display number
- if @reuse_display
- launch_xvfb unless xvfb_running?
- elsif xvfb_running?
- raise Headless::Exception.new("Display :#{display} is already taken and reuse=false")
- else
- launch_xvfb
- end
+ attach_xvfb
end
# Switches to the headless server
@@ -125,6 +123,35 @@ def take_screenshot(file_path)
private
+ def attach_xvfb
+ # TODO this loop isn't elegant enough
+ success = false
+ while !success && @display<10000
+ begin
+ if !xvfb_running?
+ launch_xvfb
+ success=true
+ else
+ success = @reuse_display
+ end
+ rescue Errno::EPERM
+ # No permission to read pid file
+ success = false
+ end
+
+ # TODO this is crufty
+ if @autopick_display
+ @display += 1 unless success
+ else
+ break
+ end
+ end
+
+ unless success
+ raise Headless::Exception.new("Display :#{display} is already taken and reuse=false")
+ end
+ end
+
def launch_xvfb
#TODO error reporting
result = system "#{CliUtil.path_to("Xvfb")} :#{display} -screen 0 #{dimensions} -ac >/dev/null 2>&1 &"
@@ -147,7 +174,9 @@ def hook_at_exit
unless @at_exit_hook_installed
@at_exit_hook_installed = true
at_exit do
+ exit_status = $!.status if $!.is_a?(SystemExit)
destroy if @destroy_at_exit
+ exit exit_status if exit_status
end
end
end
View
@@ -50,6 +50,8 @@ def self.kill_process(pid_filename, options={})
Process.wait pid if options[:wait]
rescue Errno::ESRCH
# no such process; assume it's already killed
+ rescue Errno::ECHILD
+ # Process.wait tried to wait on a dead process
end
end
View
@@ -1,12 +1,12 @@
-require 'spec_helper'
+require 'headless'
describe Headless do
before do
ENV['DISPLAY'] = ":31337"
stub_environment
end
- context "instaniation" do
+ context "instantiation" do
context "when Xvfb is not installed" do
before do
Headless::CliUtil.stub!(:application_exists?).and_return(false)
@@ -33,16 +33,63 @@
context "when Xvfb is already running" do
before do
- Headless::CliUtil.stub!(:read_pid).and_return(31337)
+ Headless::CliUtil.stub!(:read_pid).with('/tmp/.X99-lock').and_return(31337)
+ Headless::CliUtil.stub!(:read_pid).with('/tmp/.X100-lock').and_return(nil)
end
- it "raises an error if reuse display is not allowed" do
- lambda { Headless.new(:reuse => false) }.should raise_error(Headless::Exception)
+ context "and display reuse is allowed" do
+ let(:options) { {:reuse => true} }
+
+ it "should reuse the existing Xvfb" do
+ Headless.new(options).display.should == 99
+ end
+ end
+
+ context "and display reuse is not allowed" do
+ let(:options) { {:reuse => false} }
+
+ it "should pick the next available display number" do
+ Headless.new(options).display.should == 100
+ end
+
+ context "and display number is explicitly set" do
+ let(:options) { {:reuse => false, :display => 99} }
+
+ it "should fail with an exception" do
+ lambda { Headless.new(options) }.should raise_error(Headless::Exception)
+ end
+
+ context "and autopicking is allowed" do
+ let(:options) { {:reuse => false, :display => 99, :autopick => true} }
+
+ it "should pick the next available display number" do
+ Headless.new(options).display.should == 100
+ end
+ end
+ end
+ end
+ end
+
+ context 'when Xvfb is started, but by another user' do
+ before do
+ Headless::CliUtil.stub!(:read_pid).with('/tmp/.X99-lock') { raise Errno::EPERM }
+ Headless::CliUtil.stub!(:read_pid).with('/tmp/.X100-lock').and_return(nil)
end
- it "doesn't raise an error if reuse display is allowed" do
- lambda { Headless.new(:reuse => true) }.should_not raise_error(Headless::Exception)
- lambda { Headless.new }.should_not raise_error(Headless::Exception)
+ context "and display autopicking is not allowed" do
+ let(:options) { {:autopick => false} }
+
+ it "should fail with and exception" do
+ lambda { Headless.new(options) }.should raise_error(Headless::Exception)
+ end
+ end
+
+ context "and display autopicking is allowed" do
+ let(:options) { {:autopick => true} }
+
+ it "should pick the next display number" do
+ Headless.new(options).display.should == 100
+ end
end
end
end
View
@@ -1,3 +0,0 @@
-$LOAD_PATH << File.expand_path(File.join('..', 'lib'), File.dirname(__FILE__))
-
-require 'headless'
@@ -1,11 +1,11 @@
-require 'spec_helper'
+require 'headless'
describe Headless::VideoRecorder do
before do
stub_environment
end
- describe "instaniation" do
+ describe "instantiation" do
before do
Headless::CliUtil.stub!(:application_exists?).and_return(false)
end
@@ -34,25 +34,30 @@
end
context "stopping video recording" do
+ let(:tmpfile) { '/tmp/ci.mov' }
+ let(:filename) { '/tmp/test.mov' }
+ let(:pidfile) { '/tmp/pid' }
+
subject do
- recorder = Headless::VideoRecorder.new(99, "1024x768x32", :pid_file_path => "/tmp/pid", :tmp_file_path => "/tmp/ci.mov")
+ recorder = Headless::VideoRecorder.new(99, "1024x768x32", :pid_file_path => pidfile, :tmp_file_path => tmpfile)
recorder.start_capture
recorder
end
describe "using #stop_and_save" do
it "stops video recording and saves file" do
- Headless::CliUtil.should_receive(:kill_process).with("/tmp/pid", :wait => true)
- FileUtils.should_receive(:mv).with("/tmp/ci.mov", "/tmp/test.mov")
+ Headless::CliUtil.should_receive(:kill_process).with(pidfile, :wait => true)
+ File.should_receive(:exists?).with(tmpfile).and_return(true)
+ FileUtils.should_receive(:mv).with(tmpfile, filename)
- subject.stop_and_save("/tmp/test.mov")
+ subject.stop_and_save(filename)
end
end
describe "using #stop_and_discard" do
it "stops video recording and deletes temporary file" do
- Headless::CliUtil.should_receive(:kill_process).with("/tmp/pid", :wait => true)
- FileUtils.should_receive(:rm).with("/tmp/ci.mov")
+ Headless::CliUtil.should_receive(:kill_process).with(pidfile, :wait => true)
+ FileUtils.should_receive(:rm).with(tmpfile)
subject.stop_and_discard
end

0 comments on commit 19906a3

Please sign in to comment.