headless 1.0.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ecdab81182658752d292ec13079deae78ed26241
4
- data.tar.gz: 2f61437e9fe159aa2160e45263f731e20a43ffb8
3
+ metadata.gz: 36a92b0fed73ea273bf7cc4201b545be19d539e2
4
+ data.tar.gz: 29feae5dcdb2be2fa1d5147e009910d4c7f7c044
5
5
  SHA512:
6
- metadata.gz: ca8f638bd43c1ddbda26171974b2c4bd11e0508d5c973c5ce35553658e8e647df8bb9027fa62e31c00fde382bf8f87ebd352d762904e3bddefc653169b8b5731
7
- data.tar.gz: af8d46f3c14dbc2d0f8bea5bdfc04e7b2e917f878ac208abcf91f30c3bbc5f66582540c488b8a89b97f76527f4da0e825e0a6d6e6af715160d78b3ad4d7283a2
6
+ metadata.gz: 0a2c40f5cad61642e952565e3ab9666f00a4fe40eaee09ec99eb2facfff76bc4a87976cd2ca42dea02cf5b185d7d4f4d2a812ec44eb9c5a43ed016f9cbef12f6
7
+ data.tar.gz: ce8ea2ace8823b3507bcef65da7194f57b0fbce038f2fe0eef4f7a7dfbb69e7de678e4172c4761736297eb4b7d05f1e3128c93c6ee95cd6d42e81a18050fe773
@@ -1,7 +1,9 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.8.7
4
- - 1.9.2
5
3
  - 1.9.3
6
- - ree
4
+ - 2.0
5
+ - 2.2
6
+ before_install:
7
+ - "sudo apt-get update"
8
+ - "sudo apt-get install -y firefox xvfb libav-tools"
7
9
  script: "rspec"
data/CHANGELOG CHANGED
@@ -1,3 +1,14 @@
1
+ ## 2.0.0 (2015-04-23)
2
+
3
+ * Rewritten Xvfb launch using Process.spawn and avoiding a shell
4
+ * Do not manually remove X11 lock file when stopping Xvfb; this isn’t conventional. Should eliminate some errors with not being able to find Xvfb
5
+ * More informative error messages
6
+ * Detect situation when Xvfb can’t listen to any sockets and raise corresponding error.
7
+ * If video recorder provider is libav, use avconv binary instead of ffmpeg
8
+ * Fixes to video recorder launch options (from @gpavlidi, @abotalov, @ynagorny, @WeAreFarmGeek)
9
+ * Customize launch timeout (from @ShockwaveNN)
10
+ * Properly working integration tests
11
+
1
12
  ## 1.0.2 (2014-06-03)
2
13
 
3
14
  * pass options correctly to ffmpeg (from @abotalov)
data/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # Headless [![Travis CI status](https://secure.travis-ci.org/leonid-shevtsov/headless.png)](http://travis-ci.org/leonid-shevtsov/headless)
2
2
 
3
- Headless is *the* Ruby interface for Xvfb. It allows you to create a headless display straight from Ruby code, hiding some low-level action.
4
- It can also capture images and video from the virtual framebuffer.
3
+ Headless is *the* Ruby interface for Xvfb. It allows you to create a headless display straight from Ruby code, hiding the low-level action.
4
+ It can also capture images and video from the virtual framebuffer. For example, you can record screenshots and screencasts of your failing integration specs.
5
5
 
6
6
  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.
7
7
  Other possible uses include pdf generation with `wkhtmltopdf`, or screenshotting.
8
8
 
9
- Documentation is available at [rdoc.info](http://rdoc.info/projects/leonid-shevtsov/headless)
9
+ Documentation is available at [rubydoc.info](http://www.rubydoc.info/gems/headless)
10
10
 
11
11
  [Changelog](https://github.com/leonid-shevtsov/headless/blob/master/CHANGELOG)
12
12
 
@@ -120,19 +120,58 @@ After do |scenario|
120
120
  end
121
121
  ```
122
122
 
123
+ ### Video options
124
+
125
+ When initiating Headless you may pass a hash with video options.
126
+
127
+ ```ruby
128
+ headless = Headless.new(:video => { :frame_rate => 12, :codec => 'libx264' })
129
+ ```
130
+
131
+ Available options:
132
+
133
+ * :codec - codec to be used by ffmpeg
134
+ * :frame_rate - frame rate of video capture
135
+ * :provider - ffmpeg provider - either :libav (default) or :ffmpeg
136
+ * :pid_file_path - path to ffmpeg pid file, default: "/tmp/.headless_ffmpeg_#{@display}.pid"
137
+ * :tmp_file_path - path to tmp video file, default: "/tmp/.headless_ffmpeg_#{@display}.mov"
138
+ * :log_file_path - ffmpeg log file, default: "/dev/null"
139
+ * :extra - array of extra ffmpeg options, default: []
140
+
123
141
  ## Taking screenshots
124
142
 
125
- Images are captured using `import` utility which is part of `imagemagick` library. You can install it on Ubuntu via `sudo apt-get install imagemagick`. You can call `headless.take_screenshot` at any time. You have to supply full path to target file. File format is determined by supplied file extension.
143
+ Call `headless.take_screenshot` to take a screenshot. It needs two arguments:
126
144
 
127
- ## Contributors
145
+ - file_path - path where the image should be stored
146
+ - options - options, that can be:
147
+ :using - :imagemagick or :xwd, :imagemagick is default, if :imagemagick is used, image format is determined by file_path extension
128
148
 
129
- * [Igor Afonov](http://iafonov.github.com) - video and screenshot capturing functionality.
149
+ Screenshots can be taken by either using `import` (part of `imagemagick` library) or `xwd` utility.
130
150
 
131
- ---
151
+ `import` captures a screenshot and saves it in the format of the specified file. It is convenient but not too fast as
152
+ it has to do the encoding synchronously.
132
153
 
133
- © 2011 Leonid Shevtsov, released under the MIT license
154
+ `xwd` will capture a screenshot very fast and store it in its own format, which can then be converted to one
155
+ of other picture formats using, for example, netpbm utilities - `xwdtopnm <xwd_file> | pnmtopng > capture.png`.
134
156
 
157
+ To install the necessary libraries on ubuntu:
135
158
 
159
+ `import` - run `sudo apt-get install imagemagick`
160
+ `xwd` - run `sudo apt-get install X11-apps` and if you are going to use netpbm utilities for image conversion - `sudo apt-get install netpbm`
161
+
162
+ ## Troubleshooting
163
+
164
+ ### Display socket is taken but lock file is missing
165
+
166
+ This means that there is an X server that is taking up the chosen display number, but its lock file is missing. This is an exceptional situation. Please stop the server process manually (`pkill Xvfb`) and open an issue.
167
+
168
+ ### Video not recording
136
169
 
137
- [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/leonid-shevtsov/headless/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
170
+ If video is not recording, and there are no visible exceptions, try passing the following option to Headless to figure out the reason: `Headless.new(video: {log_file_path: STDERR})`. In particular, there are some issues with the version of avconv packaged with Ubuntu 12.04 - an outdated release, but still in use on Travis.
171
+
172
+
173
+ ##[Contributors](https://github.com/leonid-shevtsov/headless/graphs/contributors)
174
+
175
+ ---
138
176
 
177
+ &copy; 2011-2015 Leonid Shevtsov, released under the MIT license
@@ -3,7 +3,7 @@ spec = Gem::Specification.new do |s|
3
3
  s.email = 'leonid@shevtsov.me'
4
4
 
5
5
  s.name = 'headless'
6
- s.version = '1.0.2'
6
+ s.version = '2.0.0'
7
7
  s.summary = 'Ruby headless display interface'
8
8
 
9
9
  s.description = <<-EOF
@@ -15,5 +15,6 @@ spec = Gem::Specification.new do |s|
15
15
  s.files = `git ls-files`.split("\n")
16
16
 
17
17
  s.add_development_dependency 'rake'
18
- s.add_development_dependency "rspec", "~> 2.6"
18
+ s.add_development_dependency "rspec", "~> 3"
19
+ s.add_development_dependency "selenium-webdriver"
19
20
  end
@@ -44,8 +44,7 @@ class Headless
44
44
  DEFAULT_DISPLAY_NUMBER = 99
45
45
  MAX_DISPLAY_NUMBER = 10_000
46
46
  DEFAULT_DISPLAY_DIMENSIONS = '1280x1024x24'
47
- # How long should we wait for Xvfb to open a display, before assuming that it is frozen (in seconds)
48
- XVFB_LAUNCH_TIMEOUT = 10
47
+ DEFAULT_XVFB_LAUNCH_TIMEOUT = 10
49
48
 
50
49
  class Exception < RuntimeError
51
50
  end
@@ -55,19 +54,30 @@ class Headless
55
54
 
56
55
  # The display dimensions
57
56
  attr_reader :dimensions
57
+ attr_reader :xvfb_launch_timeout
58
58
 
59
- # Creates a new headless server, but does NOT switch to it immediately. Call #start for that
59
+ # Creates a new headless server, but does NOT switch to it immediately.
60
+ # Call #start for that
60
61
  #
61
62
  # List of available options:
62
63
  # * +display+ (default 99) - what display number to listen to;
63
- # * +reuse+ (default true) - if given display server already exists, should we use it or try another?
64
- # * +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.
65
- # * +dimensions+ (default 1280x1024x24) - display dimensions and depth. Not all combinations are possible, refer to +man Xvfb+.
66
- # * +destroy_at_exit+ (default true) - if a display is started but not stopped, should it be destroyed when the script finishes?
64
+ # * +reuse+ (default true) - if given display server already exists,
65
+ # should we use it or try another?
66
+ # * +autopick+ (default true is display number isn't explicitly set) - if
67
+ # Headless should automatically pick a display, or fail if the given one is
68
+ # not available.
69
+ # * +dimensions+ (default 1280x1024x24) - display dimensions and depth. Not
70
+ # all combinations are possible, refer to +man Xvfb+.
71
+ # * +destroy_at_exit+ (default true) - if a display is started but not
72
+ # stopped, should it be destroyed when the script finishes?
73
+ # * +xvfb_launch_timeout+ - how long should we wait for Xvfb to open a
74
+ # display, before assuming that it is frozen (in seconds, default is 10)
75
+ # * +video+ - options to be passed to the ffmpeg video recorder
67
76
  def initialize(options = {})
68
77
  CliUtil.ensure_application_exists!('Xvfb', 'Xvfb not found on your system')
69
78
 
70
79
  @display = options.fetch(:display, DEFAULT_DISPLAY_NUMBER).to_i
80
+ @xvfb_launch_timeout = options.fetch(:xvfb_launch_timeout, DEFAULT_XVFB_LAUNCH_TIMEOUT).to_i
71
81
  @autopick_display = options.fetch(:autopick, !options.key?(:display))
72
82
  @reuse_display = options.fetch(:reuse, true)
73
83
  @dimensions = options.fetch(:dimensions, DEFAULT_DISPLAY_DIMENSIONS)
@@ -93,7 +103,13 @@ class Headless
93
103
  # Switches back from the headless server and terminates the headless session
94
104
  def destroy
95
105
  stop
96
- CliUtil.kill_process(pid_filename)
106
+ CliUtil.kill_process(pid_filename, preserve_pid_file: true)
107
+ end
108
+
109
+ # Same as destroy, but waits for Xvfb process to terminate
110
+ def destroy_sync
111
+ stop
112
+ CliUtil.kill_process(pid_filename, preserve_pid_file: true, wait: true)
97
113
  end
98
114
 
99
115
  # Block syntax:
@@ -115,10 +131,17 @@ class Headless
115
131
  @video_recorder ||= VideoRecorder.new(display, dimensions, @video_capture_options)
116
132
  end
117
133
 
118
- def take_screenshot(file_path)
119
- CliUtil.ensure_application_exists!('import', "imagemagick not found on your system. Please install it using sudo apt-get install imagemagick")
120
-
121
- system "#{CliUtil.path_to('import')} -display localhost:#{display} -window root #{file_path}"
134
+ def take_screenshot(file_path, options={})
135
+ using = options.fetch(:using, :imagemagick)
136
+ if using == :imagemagick
137
+ CliUtil.ensure_application_exists!('import', "imagemagick is not found on your system. Please install it using sudo apt-get install imagemagick")
138
+ system "#{CliUtil.path_to('import')} -display localhost:#{display} -window root #{file_path}"
139
+ elsif using == :xwd
140
+ CliUtil.ensure_application_exists!('xwd', "xwd is not found on your system. Please install it using sudo apt-get install X11-apps")
141
+ system "#{CliUtil.path_to('xwd')} -display localhost:#{display} -silent -root -out #{file_path}"
142
+ else
143
+ raise Headless::Exception.new('Unknown :using option value')
144
+ end
122
145
  end
123
146
 
124
147
  private
@@ -142,18 +165,30 @@ private
142
165
  end
143
166
 
144
167
  def launch_xvfb
145
- #TODO error reporting
146
- result = system "#{CliUtil.path_to("Xvfb")} :#{display} -screen 0 #{dimensions} -ac >/dev/null 2>&1 &"
147
- raise Headless::Exception.new("Xvfb did not launch - something's wrong") unless result
148
- ensure_xvfb_is_running
168
+ out_pipe, in_pipe = IO.pipe
169
+ pid = Process.spawn(
170
+ CliUtil.path_to("Xvfb"), ":#{display}", "-screen", "0", dimensions, "-ac",
171
+ err: in_pipe)
172
+ in_pipe.close
173
+ raise Headless::Exception.new("Xvfb did not launch - something's wrong") unless pid
174
+ ensure_xvfb_is_running(out_pipe)
149
175
  return true
150
176
  end
151
177
 
152
- def ensure_xvfb_is_running
178
+ def ensure_xvfb_is_running(out_pipe)
153
179
  start_time = Time.now
180
+ errors = ""
154
181
  begin
182
+ begin
183
+ errors += out_pipe.read_nonblock(10000)
184
+ if errors.include? "Cannot establish any listening sockets"
185
+ raise Headless::Exception.new("Display socket is taken but lock file is missing - check the Headless troubleshooting guide")
186
+ end
187
+ rescue IO::WaitReadable
188
+ # will retry next cycle
189
+ end
155
190
  sleep 0.01 # to avoid cpu hogging
156
- raise Headless::Exception.new("Xvfb is frozen") if (Time.now-start_time)>=XVFB_LAUNCH_TIMEOUT
191
+ raise Headless::Exception.new("Xvfb launched but did not complete initialization") if (Time.now-start_time)>=@xvfb_launch_timeout
157
192
  end while !xvfb_running?
158
193
  end
159
194
 
@@ -180,4 +215,3 @@ private
180
215
  end
181
216
  end
182
217
  end
183
-
@@ -53,11 +53,13 @@ class Headless
53
53
  # Process.wait tried to wait on a dead process
54
54
  end
55
55
  end
56
-
57
- begin
58
- FileUtils.rm pid_filename
59
- rescue Errno::ENOENT
60
- # pid file already removed
56
+
57
+ unless options[:preserve_pid_file]
58
+ begin
59
+ FileUtils.rm pid_filename
60
+ rescue Errno::ENOENT
61
+ # pid file already removed
62
+ end
61
63
  end
62
64
  end
63
65
  end
@@ -5,8 +5,6 @@ class Headless
5
5
  attr_accessor :pid_file_path, :tmp_file_path, :log_file_path
6
6
 
7
7
  def initialize(display, dimensions, options = {})
8
- CliUtil.ensure_application_exists!('ffmpeg', 'Ffmpeg not found on your system. Install it with sudo apt-get install ffmpeg')
9
-
10
8
  @display = display
11
9
  @dimensions = dimensions[/.+(?=x)/]
12
10
 
@@ -15,6 +13,10 @@ class Headless
15
13
  @log_file_path = options.fetch(:log_file_path, "/dev/null")
16
14
  @codec = options.fetch(:codec, "qtrle")
17
15
  @frame_rate = options.fetch(:frame_rate, 30)
16
+ @provider = options.fetch(:provider, :libav) # or :ffmpeg
17
+ @extra = Array(options.fetch(:extra, []))
18
+
19
+ CliUtil.ensure_application_exists!(provider_binary, "#{provider_binary} not found on your system. Install it or change video recorder provider")
18
20
  end
19
21
 
20
22
  def capture_running?
@@ -22,7 +24,8 @@ class Headless
22
24
  end
23
25
 
24
26
  def start_capture
25
- CliUtil.fork_process("#{CliUtil.path_to('ffmpeg')} -y -r #{@frame_rate} -g 600 -s #{@dimensions} -f x11grab -i :#{@display} -vcodec #{@codec} #{@tmp_file_path}", @pid_file_path, @log_file_path)
27
+ CliUtil.fork_process(command_line_for_capture,
28
+ @pid_file_path, @log_file_path)
26
29
  at_exit do
27
30
  exit_status = $!.status if $!.is_a?(SystemExit)
28
31
  stop_and_discard
@@ -49,5 +52,32 @@ class Headless
49
52
  # that's ok if the file doesn't exist
50
53
  end
51
54
  end
55
+
56
+ private
57
+
58
+ def provider_binary
59
+ @provider==:libav ? 'avconv' : 'ffmpeg'
60
+ end
61
+
62
+ def command_line_for_capture
63
+ if @provider == :libav
64
+ group_of_pic_size_option = '-g 600'
65
+ dimensions = @dimensions
66
+ else
67
+ group_of_pic_size_option = nil
68
+ dimensions = @dimensions.match(/^(\d+x\d+)/)[0]
69
+ end
70
+
71
+ ([
72
+ CliUtil.path_to(provider_binary),
73
+ "-y",
74
+ "-r #{@frame_rate}",
75
+ "-s #{dimensions}",
76
+ "-f x11grab",
77
+ "-i :#{@display}",
78
+ group_of_pic_size_option,
79
+ "-vcodec #{@codec}"
80
+ ].compact + @extra + [@tmp_file_path]).join(' ')
81
+ end
52
82
  end
53
83
  end
@@ -6,170 +6,201 @@ describe Headless do
6
6
  stub_environment
7
7
  end
8
8
 
9
- context "instantiation" do
10
- context "when Xvfb is not installed" do
11
- before do
12
- Headless::CliUtil.stub(:application_exists?).and_return(false)
13
- end
9
+ describe 'launch options' do
10
+ before do
11
+ allow_any_instance_of(Headless).to receive(:ensure_xvfb_is_running).and_return(true)
12
+ end
14
13
 
15
- it "raises an error" do
16
- lambda { Headless.new }.should raise_error(Headless::Exception)
17
- end
14
+ it "starts Xvfb" do
15
+ expect(Process).to receive(:spawn).with(*(%w(/usr/bin/Xvfb :99 -screen 0 1280x1024x24 -ac)+[hash_including(:err)])).and_return(123)
16
+ headless = Headless.new
18
17
  end
19
18
 
20
- context "when Xvfb is not started yet" do
21
- it "starts Xvfb" do
22
- Headless.any_instance.should_receive(:system).with("/usr/bin/Xvfb :99 -screen 0 1280x1024x24 -ac >/dev/null 2>&1 &").and_return(true)
19
+ it "allows setting screen dimensions" do
20
+ expect(Process).to receive(:spawn).with(*(%w(/usr/bin/Xvfb :99 -screen 0 1024x768x16 -ac)+[hash_including(:err)])).and_return(123)
21
+ headless = Headless.new(:dimensions => "1024x768x16")
22
+ end
23
+ end
23
24
 
24
- headless = Headless.new
25
- end
25
+ context 'with stubbed launch_xvfb' do
26
+ before do
27
+ allow_any_instance_of(Headless).to receive(:launch_xvfb).and_return(true)
28
+ end
26
29
 
27
- it "allows setting screen dimensions" do
28
- Headless.any_instance.should_receive(:system).with("/usr/bin/Xvfb :99 -screen 0 1024x768x16 -ac >/dev/null 2>&1 &").and_return(true)
30
+ context "instantiation" do
31
+ context "when Xvfb is not installed" do
32
+ before do
33
+ allow(Headless::CliUtil).to receive(:application_exists?).and_return(false)
34
+ end
29
35
 
30
- headless = Headless.new(:dimensions => "1024x768x16")
36
+ it "raises an error" do
37
+ expect { Headless.new }.to raise_error(Headless::Exception)
38
+ end
31
39
  end
32
- end
33
40
 
34
- context "when Xvfb is already running" do
35
- before do
36
- Headless::CliUtil.stub(:read_pid).with('/tmp/.X99-lock').and_return(31337)
37
- Headless::CliUtil.stub(:read_pid).with('/tmp/.X100-lock').and_return(nil)
38
- end
41
+ context "when Xvfb is already running" do
42
+ before do
43
+ allow(Headless::CliUtil).to receive(:read_pid).with('/tmp/.X99-lock').and_return(31337)
44
+ allow(Headless::CliUtil).to receive(:read_pid).with('/tmp/.X100-lock').and_return(nil)
45
+ end
39
46
 
40
- context "and display reuse is allowed" do
41
- let(:options) { {:reuse => true} }
47
+ context "and display reuse is allowed" do
48
+ let(:options) { {:reuse => true} }
42
49
 
43
- it "should reuse the existing Xvfb" do
44
- Headless.new(options).display.should == 99
50
+ it "should reuse the existing Xvfb" do
51
+ expect(Headless.new(options).display).to eq 99
52
+ end
45
53
  end
46
- end
47
54
 
48
- context "and display reuse is not allowed" do
49
- let(:options) { {:reuse => false} }
55
+ context "and display reuse is not allowed" do
56
+ let(:options) { {:reuse => false} }
57
+
58
+ it "should pick the next available display number" do
59
+ expect(Headless.new(options).display).to eq 100
60
+ end
61
+
62
+ context "and display number is explicitly set" do
63
+ let(:options) { {:reuse => false, :display => 99} }
64
+
65
+ it "should fail with an exception" do
66
+ expect { Headless.new(options) }.to raise_error(Headless::Exception)
67
+ end
68
+
69
+ context "and autopicking is allowed" do
70
+ let(:options) { {:reuse => false, :display => 99, :autopick => true} }
71
+
72
+ it "should pick the next available display number" do
73
+ expect(Headless.new(options).display).to eq 100
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
50
79
 
51
- it "should pick the next available display number" do
52
- Headless.new(options).display.should == 100
80
+ context 'when Xvfb is started, but by another user' do
81
+ before do
82
+ allow(Headless::CliUtil).to receive(:read_pid).with('/tmp/.X99-lock') { raise Errno::EPERM }
83
+ allow(Headless::CliUtil).to receive(:read_pid).with('/tmp/.X100-lock').and_return(nil)
53
84
  end
54
85
 
55
- context "and display number is explicitly set" do
56
- let(:options) { {:reuse => false, :display => 99} }
86
+ context "and display autopicking is not allowed" do
87
+ let(:options) { {:autopick => false} }
57
88
 
58
- it "should fail with an exception" do
59
- lambda { Headless.new(options) }.should raise_error(Headless::Exception)
89
+ it "should fail with and exception" do
90
+ expect { Headless.new(options) }.to raise_error(Headless::Exception)
60
91
  end
92
+ end
61
93
 
62
- context "and autopicking is allowed" do
63
- let(:options) { {:reuse => false, :display => 99, :autopick => true} }
94
+ context "and display autopicking is allowed" do
95
+ let(:options) { {:autopick => true} }
64
96
 
65
- it "should pick the next available display number" do
66
- Headless.new(options).display.should == 100
67
- end
97
+ it "should pick the next display number" do
98
+ expect(Headless.new(options).display).to eq 100
68
99
  end
69
100
  end
70
101
  end
71
102
  end
72
103
 
73
- context 'when Xvfb is started, but by another user' do
74
- before do
75
- Headless::CliUtil.stub(:read_pid).with('/tmp/.X99-lock') { raise Errno::EPERM }
76
- Headless::CliUtil.stub(:read_pid).with('/tmp/.X100-lock').and_return(nil)
104
+ context "lifecycle" do
105
+ let(:headless) { Headless.new }
106
+ describe "#start" do
107
+ it "switches to the headless server" do
108
+ expect(ENV['DISPLAY']).to eq ":31337"
109
+ headless.start
110
+ expect(ENV['DISPLAY']).to eq ":99"
111
+ end
77
112
  end
78
113
 
79
- context "and display autopicking is not allowed" do
80
- let(:options) { {:autopick => false} }
81
-
82
- it "should fail with and exception" do
83
- lambda { Headless.new(options) }.should raise_error(Headless::Exception)
114
+ describe "#stop" do
115
+ it "switches back from the headless server" do
116
+ expect(ENV['DISPLAY']).to eq ":31337"
117
+ headless.start
118
+ expect(ENV['DISPLAY']).to eq ":99"
119
+ headless.stop
120
+ expect(ENV['DISPLAY']).to eq ":31337"
84
121
  end
85
122
  end
86
123
 
87
- context "and display autopicking is allowed" do
88
- let(:options) { {:autopick => true} }
124
+ describe "#destroy" do
125
+ before do
126
+ allow(Headless::CliUtil).to receive(:read_pid).and_return(4444)
127
+ end
128
+
129
+ it "switches back from the headless server and terminates the headless session" do
130
+ expect(Process).to receive(:kill).with('TERM', 4444)
89
131
 
90
- it "should pick the next display number" do
91
- Headless.new(options).display.should == 100
132
+ expect(ENV['DISPLAY']).to eq ":31337"
133
+ headless.start
134
+ expect(ENV['DISPLAY']).to eq ":99"
135
+ headless.destroy
136
+ expect(ENV['DISPLAY']).to eq ":31337"
92
137
  end
93
138
  end
94
139
  end
95
- end
96
140
 
97
- context "lifecycle" do
98
- let(:headless) { Headless.new }
99
- describe "#start" do
100
- it "switches to the headless server" do
101
- ENV['DISPLAY'].should == ":31337"
102
- headless.start
103
- ENV['DISPLAY'].should == ":99"
104
- end
105
- end
141
+ context "#video" do
142
+ let(:headless) { Headless.new }
106
143
 
107
- describe "#stop" do
108
- it "switches back from the headless server" do
109
- ENV['DISPLAY'].should == ":31337"
110
- headless.start
111
- ENV['DISPLAY'].should == ":99"
112
- headless.stop
113
- ENV['DISPLAY'].should == ":31337"
144
+ it "returns video recorder" do
145
+ expect(headless.video).to be_a_kind_of(Headless::VideoRecorder)
114
146
  end
115
- end
116
147
 
117
- describe "#destroy" do
118
- before do
119
- Headless::CliUtil.stub(:read_pid).and_return(4444)
148
+ it "returns the same instance" do
149
+ recorder = headless.video
150
+ expect(headless.video).to eq recorder
120
151
  end
152
+ end
121
153
 
122
- it "switches back from the headless server and terminates the headless session" do
123
- Process.should_receive(:kill).with('TERM', 4444)
154
+ context "#take_screenshot" do
155
+ let(:headless) { Headless.new }
124
156
 
125
- ENV['DISPLAY'].should == ":31337"
126
- headless.start
127
- ENV['DISPLAY'].should == ":99"
128
- headless.destroy
129
- ENV['DISPLAY'].should == ":31337"
157
+ it "raises an error if unknown value for option :using is used" do
158
+ expect { headless.take_screenshot('a.png', :using => :teleportation) }.to raise_error(Headless::Exception)
130
159
  end
131
- end
132
- end
133
160
 
134
- context "#video" do
135
- let(:headless) { Headless.new }
161
+ it "raises an error if imagemagick is not installed, with default options" do
162
+ allow(Headless::CliUtil).to receive(:application_exists?).with('import').and_return(false)
136
163
 
137
- it "returns video recorder" do
138
- headless.video.should be_a_kind_of(Headless::VideoRecorder)
139
- end
164
+ expect { headless.take_screenshot('a.png') }.to raise_error(Headless::Exception)
165
+ end
140
166
 
141
- it "returns the same instance" do
142
- recorder = headless.video
143
- headless.video.should be_eql(recorder)
144
- end
145
- end
167
+ it "raises an error if imagemagick is not installed, with using: :imagemagick" do
168
+ allow(Headless::CliUtil).to receive(:application_exists?).with('import').and_return(false)
146
169
 
147
- context "#take_screenshot" do
148
- let(:headless) { Headless.new }
170
+ expect { headless.take_screenshot('a.png', :using => :imagemagick) }.to raise_error(Headless::Exception)
171
+ end
149
172
 
150
- it "raises an error if imagemagick is not installed" do
151
- Headless::CliUtil.stub(:application_exists?).and_return(false)
173
+ it "raises an error if xwd is not installed, with using: :xwd" do
174
+ allow(Headless::CliUtil).to receive(:application_exists?).with('xwd').and_return(false)
152
175
 
153
- lambda { headless.take_screenshot }.should raise_error(Headless::Exception)
154
- end
176
+ expect { headless.take_screenshot('a.png', :using => :xwd) }.to raise_error(Headless::Exception)
177
+ end
155
178
 
156
- it "issues command to take screenshot" do
157
- headless = Headless.new
179
+ it "issues command to take screenshot, with default options" do
180
+ allow(Headless::CliUtil).to receive(:path_to).with('import').and_return('path/import')
181
+ expect(headless).to receive(:system).with("path/import -display localhost:99 -window root /tmp/image.png")
182
+ headless.take_screenshot("/tmp/image.png")
183
+ end
158
184
 
159
- Headless.any_instance.should_receive(:system)
185
+ it "issues command to take screenshot, with using: :imagemagick" do
186
+ allow(Headless::CliUtil).to receive(:path_to).with('import').and_return('path/import')
187
+ expect(headless).to receive(:system).with("path/import -display localhost:99 -window root /tmp/image.png")
188
+ headless.take_screenshot("/tmp/image.png", :using => :imagemagick)
189
+ end
160
190
 
161
- headless.take_screenshot("/tmp/image.png")
191
+ it "issues command to take screenshot, with using: :xwd" do
192
+ allow(Headless::CliUtil).to receive(:path_to).with('xwd').and_return('path/xwd')
193
+ expect(headless).to receive(:system).with("path/xwd -display localhost:99 -silent -root -out /tmp/image.png")
194
+ headless.take_screenshot("/tmp/image.png", :using => :xwd)
195
+ end
162
196
  end
163
197
  end
164
198
 
165
199
  private
166
200
 
167
201
  def stub_environment
168
- Headless::CliUtil.stub(:application_exists?).and_return(true)
169
- Headless::CliUtil.stub(:read_pid).and_return(nil)
170
- Headless::CliUtil.stub(:path_to).and_return("/usr/bin/Xvfb")
171
-
172
- # TODO this is wrong. But, as long as Xvfb is started inside the constructor (which is also wrong), I don't see another option to make tests pass
173
- Headless.any_instance.stub(:ensure_xvfb_is_running).and_return(true)
202
+ allow(Headless::CliUtil).to receive(:application_exists?).and_return(true)
203
+ allow(Headless::CliUtil).to receive(:read_pid).and_return(nil)
204
+ allow(Headless::CliUtil).to receive(:path_to).and_return("/usr/bin/Xvfb")
174
205
  end
175
206
  end
@@ -0,0 +1,44 @@
1
+ require 'headless'
2
+ require 'selenium-webdriver'
3
+
4
+ describe 'Integration test' do
5
+ let!(:headless) { Headless.new }
6
+ before { headless.start }
7
+
8
+ after { headless.destroy_sync }
9
+
10
+ it 'should use xvfb' do
11
+ work_with_browser
12
+ end
13
+
14
+ it 'should record screenshots' do
15
+ headless.take_screenshot("test.jpg")
16
+ expect(File.exist?("test.jpg")).to eq true
17
+ end
18
+
19
+ # Unfortunately the libav version that ships with Ubuntu 12.04 has
20
+ # buggy X11 capture.
21
+ it 'should record video with ffmpeg', pending: ENV['TRAVIS'] do
22
+ headless.video.start_capture
23
+ work_with_browser
24
+ headless.video.stop_and_save("test.mov")
25
+ expect(File.exist?("test.mov")).to eq true
26
+ end
27
+
28
+ it 'should raise an error when trying to create the same display' do
29
+ expect {
30
+ FileUtils.mv("/tmp/.X#{headless.display}-lock", "/tmp/headless-test-tmp")
31
+ Headless.new(display: headless.display, reuse: false)
32
+ }.to raise_error(Headless::Exception, /troubleshooting guide/)
33
+ FileUtils.mv("/tmp/headless-test-tmp", "/tmp/.X#{headless.display}-lock")
34
+ end
35
+
36
+ private
37
+
38
+ def work_with_browser
39
+ driver = Selenium::WebDriver.for :firefox
40
+ driver.navigate.to 'http://google.com'
41
+ expect(driver.title).to match(/Google/)
42
+ driver.close
43
+ end
44
+ end
@@ -7,30 +7,39 @@ describe Headless::VideoRecorder do
7
7
 
8
8
  describe "instantiation" do
9
9
  before do
10
- Headless::CliUtil.stub(:application_exists?).and_return(false)
10
+ allow(Headless::CliUtil).to receive(:application_exists?).and_return(false)
11
11
  end
12
12
 
13
13
  it "throws an error if ffmpeg is not installed" do
14
- lambda { Headless::VideoRecorder.new(99, "1024x768x32") }.should raise_error(Headless::Exception)
14
+ expect { Headless::VideoRecorder.new(99, "1024x768x32") }.to raise_error(Headless::Exception)
15
15
  end
16
16
  end
17
17
 
18
18
  describe "#capture" do
19
+ before do
20
+ allow(Headless::CliUtil).to receive(:path_to).and_return('ffmpeg')
21
+ end
22
+
19
23
  it "starts ffmpeg" do
20
- Headless::CliUtil.stub(:path_to).and_return('ffmpeg')
21
- Headless::CliUtil.should_receive(:fork_process).with(/ffmpeg -y -r 30 -g 600 -s 1024x768 -f x11grab -i :99 -vcodec qtrle/, "/tmp/.headless_ffmpeg_99.pid", '/dev/null')
24
+ expect(Headless::CliUtil).to receive(:fork_process).with(/^ffmpeg -y -r 30 -s 1024x768 -f x11grab -i :99 -g 600 -vcodec qtrle [^ ]+$/, "/tmp/.headless_ffmpeg_99.pid", '/dev/null')
22
25
 
23
26
  recorder = Headless::VideoRecorder.new(99, "1024x768x32")
24
27
  recorder.start_capture
25
28
  end
26
29
 
27
30
  it "starts ffmpeg with specified codec" do
28
- Headless::CliUtil.stub(:path_to).and_return('ffmpeg')
29
- Headless::CliUtil.should_receive(:fork_process).with(/ffmpeg -y -r 30 -g 600 -s 1024x768 -f x11grab -i :99 -vcodec libvpx/, "/tmp/.headless_ffmpeg_99.pid", '/dev/null')
31
+ expect(Headless::CliUtil).to receive(:fork_process).with(/^ffmpeg -y -r 30 -s 1024x768 -f x11grab -i :99 -g 600 -vcodec libvpx [^ ]+$/, "/tmp/.headless_ffmpeg_99.pid", '/dev/null')
30
32
 
31
33
  recorder = Headless::VideoRecorder.new(99, "1024x768x32", {:codec => 'libvpx'})
32
34
  recorder.start_capture
33
35
  end
36
+
37
+ it "starts ffmpeg from ffmpeg provider with correct parameters" do
38
+ expect(Headless::CliUtil).to receive(:fork_process).with(/^ffmpeg -y -r 30 -s 1024x768 -f x11grab -i :99 -vcodec qtrle [^ ]+$/, "/tmp/.headless_ffmpeg_99.pid", '/dev/null')
39
+
40
+ recorder = Headless::VideoRecorder.new(99, "1024x768x32", {:provider => :ffmpeg})
41
+ recorder.start_capture
42
+ end
34
43
  end
35
44
 
36
45
  context "stopping video recording" do
@@ -46,9 +55,9 @@ describe Headless::VideoRecorder do
46
55
 
47
56
  describe "using #stop_and_save" do
48
57
  it "stops video recording and saves file" do
49
- Headless::CliUtil.should_receive(:kill_process).with(pidfile, :wait => true)
50
- File.should_receive(:exists?).with(tmpfile).and_return(true)
51
- FileUtils.should_receive(:mv).with(tmpfile, filename)
58
+ expect(Headless::CliUtil).to receive(:kill_process).with(pidfile, :wait => true)
59
+ expect(File).to receive(:exists?).with(tmpfile).and_return(true)
60
+ expect(FileUtils).to receive(:mv).with(tmpfile, filename)
52
61
 
53
62
  subject.stop_and_save(filename)
54
63
  end
@@ -56,8 +65,8 @@ describe Headless::VideoRecorder do
56
65
 
57
66
  describe "using #stop_and_discard" do
58
67
  it "stops video recording and deletes temporary file" do
59
- Headless::CliUtil.should_receive(:kill_process).with(pidfile, :wait => true)
60
- FileUtils.should_receive(:rm).with(tmpfile)
68
+ expect(Headless::CliUtil).to receive(:kill_process).with(pidfile, :wait => true)
69
+ expect(FileUtils).to receive(:rm).with(tmpfile)
61
70
 
62
71
  subject.stop_and_discard
63
72
  end
@@ -67,7 +76,7 @@ describe Headless::VideoRecorder do
67
76
  private
68
77
 
69
78
  def stub_environment
70
- Headless::CliUtil.stub(:application_exists?).and_return(true)
71
- Headless::CliUtil.stub(:fork_process).and_return(true)
79
+ allow(Headless::CliUtil).to receive(:application_exists?).and_return(true)
80
+ allow(Headless::CliUtil).to receive(:fork_process).and_return(true)
72
81
  end
73
82
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: headless
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leonid Shevtsov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-03 00:00:00.000000000 Z
11
+ date: 2015-04-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -30,14 +30,28 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '2.6'
33
+ version: '3'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '2.6'
40
+ version: '3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: selenium-webdriver
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  description: |2
42
56
  Headless is a Ruby interface for Xvfb. It allows you to create a headless display straight from Ruby code, hiding some low-level action.
43
57
  email: leonid@shevtsov.me
@@ -57,6 +71,7 @@ files:
57
71
  - lib/headless/cli_util.rb
58
72
  - lib/headless/video/video_recorder.rb
59
73
  - spec/headless_spec.rb
74
+ - spec/integration_spec.rb
60
75
  - spec/video_recorder_spec.rb
61
76
  homepage: http://leonid.shevtsov.me/en/headless
62
77
  licenses: []
@@ -78,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
93
  requirements:
79
94
  - Xvfb
80
95
  rubyforge_project:
81
- rubygems_version: 2.2.2
96
+ rubygems_version: 2.4.5
82
97
  signing_key:
83
98
  specification_version: 4
84
99
  summary: Ruby headless display interface