xvfb 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 70a2f74969d87e28871f89a642241fa48c5e456c
4
+ data.tar.gz: 75cf5fc301aab83dc0c77c1a36f8f1d1e1535593
5
+ SHA512:
6
+ metadata.gz: 532ccfc8b2d560a473066af407549210a147b3f176bd8effdeb2f516db101b605d1338082cfa2e3f53b964547753dfdd41f02328bfb247e1a2ae1908b16003a9
7
+ data.tar.gz: 40999436585f058196e1442eb0b6d066ab0425bfe6a70d76bf4ec898b829f6e20151c837ea84e37470948d6ddbd78e027722bacfb166ac3ab624b69abcf49a7e
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .bundle
2
+ Gemfile.lock
3
+ pkg/*
data/.travis.yml ADDED
@@ -0,0 +1,19 @@
1
+ sudo: required
2
+ dist: trusty
3
+ language: ruby
4
+ cache: bundler
5
+ matrix:
6
+ include:
7
+ - rvm: 1.9.3
8
+ - rvm: 2.0.0-p648
9
+ - rvm: 2.1.10
10
+ - rvm: 2.2.5
11
+ - rvm: 2.3.1
12
+ # see https://github.com/travis-ci/travis-ci/issues/6471
13
+ - rvm: jruby-9.1.2.0
14
+ env: JRUBY_OPTS=""
15
+ before_install:
16
+ - "sudo apt-get update"
17
+ - "sudo apt-get install -y firefox xvfb libav-tools"
18
+
19
+ script: "bundle exec rspec"
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in ci_util.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2017 Piotr Krajewski
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
20
+
data/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # Xvfb
2
+
3
+ Xvfb is *the* Ruby interface for Xvfb. It allows you to create a xvfb 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
+
6
+ I created it so I can run Selenium tests in Cucumber without any shell scripting. Even more, you can go xvfb only when you run tests against Selenium.
7
+ Other possible uses include pdf generation with `wkhtmltopdf`, or screenshotting.
8
+
9
+ Documentation is available at [rubydoc.info](http://www.rubydoc.info/gems/xvfb)
10
+
11
+ [Changelog](https://github.com/mits87/Xvfb/blob/master/CHANGELOG)
12
+
13
+ ## Installation
14
+
15
+ On Debian/Ubuntu:
16
+
17
+ ```sh
18
+ sudo apt-get install xvfb
19
+ gem install xvfb
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```ruby
25
+ require 'rubygems'
26
+ require 'xvfb'
27
+ require 'selenium-webdriver'
28
+
29
+ xvfb = Xvfb.new
30
+ xvfb.start
31
+
32
+ driver = Selenium::WebDriver.for :firefox
33
+ driver.navigate.to 'http://google.com'
34
+ puts driver.title
35
+
36
+ xvfb.destroy
37
+ ```
38
+
39
+ ## Cucumber
40
+
41
+ Running cucumber xvfb is now as simple as adding a before and after hook in `features/support/env.rb`:
42
+
43
+ ```ruby
44
+ # change the condition to fit your setup
45
+ if Capybara.current_driver == :selenium
46
+ require 'xvfb'
47
+
48
+ xvfb = Xvfb.new
49
+ xvfb.start
50
+ end
51
+ ```
52
+
53
+ ## Running tests in parallel
54
+
55
+ If you have multiple threads running acceptance tests in parallel, you want to spawn Xvfb before forking, and then reuse that instance with `destroy_at_exit: false`.
56
+ You can even spawn a Xvfb instance in one ruby script, and then reuse the same instance in other scripts by specifying the same display number and `reuse: true`.
57
+
58
+ ```ruby
59
+ # spawn_xvfb.rb
60
+ Xvfb.new(display: 100, destroy_at_exit: false).start
61
+
62
+ # test_suite_that_could_be_ran_multiple_times.rb
63
+ Xvfb.new(display: 100, reuse: true, destroy_at_exit: false).start
64
+
65
+ # reap_xvfb.rb
66
+ xvfb = Xvfb.new(display: 100, reuse: true)
67
+ xvfb.destroy
68
+
69
+ # kill_xvfb_without_waiting.rb
70
+ xvfb = Xvfb.new
71
+ xvfb.destroy_without_sync
72
+ ```
73
+
74
+ There's also a different approach that creates a new virtual display for every parallel test process - see [this implementation](https://gist.github.com/rosskevin/5937888) by @rosskevin.
75
+
76
+ ## Cucumber with wkhtmltopdf
77
+
78
+ _Note: this is true for other programs which may use xvfb at the same time as cucumber is running_
79
+
80
+ When wkhtmltopdf is using Xvfb, and cucumber is invoking a block of code which uses a xvfb 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)`.
81
+
82
+ ```ruby
83
+ xvfb = Xvfb.new(:display => '100')
84
+ xvfb.start
85
+ ```
86
+
87
+ ## Capturing video
88
+
89
+ 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:
90
+
91
+ ```ruby
92
+ require 'xvfb'
93
+
94
+ xvfb = Xvfb.new
95
+ xvfb.start
96
+
97
+ Before do
98
+ xvfb.video.start_capture
99
+ end
100
+
101
+ After do |scenario|
102
+ if scenario.failed?
103
+ xvfb.video.stop_and_save("/tmp/#{BUILD_ID}/#{scenario.name.split.join("_")}.mov")
104
+ else
105
+ xvfb.video.stop_and_discard
106
+ end
107
+ end
108
+ ```
109
+
110
+ ### Video options
111
+
112
+ When initiating Xvfb you may pass a hash with video options.
113
+
114
+ ```ruby
115
+ xvfb = Xvfb.new(:video => { :frame_rate => 12, :codec => 'libx264' })
116
+ ```
117
+
118
+ Available options:
119
+
120
+ * :codec - codec to be used by ffmpeg
121
+ * :frame_rate - frame rate of video capture
122
+ * :provider - ffmpeg provider - either :libav (default) or :ffmpeg
123
+ * :provider_binary_path - Explicit path to avconv or ffmpeg. Only required when the binary cannot be discovered on the system $PATH.
124
+ * :pid_file_path - path to ffmpeg pid file, default: "/tmp/.headless_ffmpeg_#{@display}.pid"
125
+ * :tmp_file_path - path to tmp video file, default: "/tmp/.headless_ffmpeg_#{@display}.mov"
126
+ * :log_file_path - ffmpeg log file, default: "/dev/null"
127
+ * :extra - array of extra ffmpeg options, default: []
128
+
129
+ ## Troubleshooting
130
+
131
+ ### Display socket is taken but lock file is missing
132
+
133
+ 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.
134
+
135
+ ### Video not recording
136
+
137
+ If video is not recording, and there are no visible exceptions, try passing the following option to Xvfb to figure out the reason: `Xvfb.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.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/lib/xvfb.rb ADDED
@@ -0,0 +1,210 @@
1
+ require 'xvfb/cli_util'
2
+ require 'xvfb/video/video_recorder'
3
+
4
+ # A class incapsulating the creation and usage of a xvfb X server
5
+ #
6
+ # == Prerequisites
7
+ #
8
+ # * X Window System
9
+ # * Xvfb[http://en.wikipedia.org/wiki/Xvfb]
10
+ #
11
+ # == Usage
12
+ #
13
+ # require 'rubygems'
14
+ # require 'xvfb'
15
+ # require 'selenium-webdriver'
16
+ #
17
+ # xvfb = Xvfb.new
18
+ # xvfb.start
19
+ #
20
+ # driver = Selenium::WebDriver.for :firefox
21
+ # driver.navigate.to 'http://google.com'
22
+ # puts driver.title
23
+ #
24
+ # xvfb.destroy
25
+ #--
26
+ # TODO test that reuse actually works with an existing xvfb session
27
+ #++
28
+ class Xvfb
29
+
30
+ DEFAULT_DISPLAY_NUMBER = 99
31
+ MAX_DISPLAY_NUMBER = 10_000
32
+ DEFAULT_DISPLAY_DIMENSIONS = '1280x1024x24'
33
+ DEFAULT_XVFB_LAUNCH_TIMEOUT = 10
34
+
35
+ class Exception < RuntimeError
36
+ end
37
+
38
+ # The display number
39
+ attr_reader :display
40
+
41
+ # The display dimensions
42
+ attr_reader :dimensions
43
+ attr_reader :xvfb_launch_timeout
44
+
45
+ # Creates a new xvfb server, but does NOT switch to it immediately.
46
+ # Call #start for that
47
+ #
48
+ # List of available options:
49
+ # * +display+ (default 99) - what display number to listen to;
50
+ # * +reuse+ (default true) - if given display server already exists,
51
+ # should we use it or try another?
52
+ # * +autopick+ (default true if display number isn't explicitly set) - if
53
+ # Xvfb should automatically pick a display, or fail if the given one is
54
+ # not available.
55
+ # * +dimensions+ (default 1280x1024x24) - display dimensions and depth. Not
56
+ # all combinations are possible, refer to +man Xvfb+.
57
+ # * +destroy_at_exit+ - if a display is started but not stopped, should it
58
+ # be destroyed when the script finishes?
59
+ # (default true unless reuse is true and a server is already running)
60
+ # * +xvfb_launch_timeout+ - how long should we wait for Xvfb to open a
61
+ # display, before assuming that it is frozen (in seconds, default is 10)
62
+ # * +video+ - options to be passed to the ffmpeg video recorder. See Xvfb::VideoRecorder#initialize for
63
+ # * +browser+ - options to be passed to the browser chromium. See Xvfb::Browser#initialize for documentation
64
+ def initialize(options = {})
65
+ CliUtil.ensure_application_exists!('Xvfb', 'Xvfb not found on your system')
66
+
67
+ @display = options.fetch(:display, DEFAULT_DISPLAY_NUMBER).to_i
68
+ @xvfb_launch_timeout = options.fetch(:xvfb_launch_timeout, DEFAULT_XVFB_LAUNCH_TIMEOUT).to_i
69
+ @autopick_display = options.fetch(:autopick, !options.key?(:display))
70
+ @reuse_display = options.fetch(:reuse, true)
71
+ @dimensions = options.fetch(:dimensions, DEFAULT_DISPLAY_DIMENSIONS)
72
+ @video_capture_options = options.fetch(:video, {})
73
+
74
+ already_running = xvfb_running? rescue false
75
+ @destroy_at_exit = options.fetch(:destroy_at_exit, !(@reuse_display && already_running))
76
+
77
+ @pid = nil # the pid of the running Xvfb process
78
+
79
+ # FIXME Xvfb launch should not happen inside the constructor
80
+ attach_xvfb
81
+ end
82
+
83
+ # Switches to the xvfb server
84
+ def start
85
+ @old_display = ENV['DISPLAY']
86
+ ENV['DISPLAY'] = ":#{display}"
87
+ hook_at_exit
88
+ end
89
+
90
+ # Switches back from the xvfb server
91
+ def stop
92
+ ENV['DISPLAY'] = @old_display
93
+ end
94
+
95
+ # Switches back from the xvfb server and terminates the xvfb session
96
+ # while waiting for Xvfb process to terminate.
97
+ def destroy
98
+ stop
99
+ CliUtil.kill_process(pid_filename, preserve_pid_file: true, wait: true)
100
+ end
101
+
102
+ # Deprecated.
103
+ # Same as destroy.
104
+ # Kept for backward compatibility in June 2015.
105
+ def destroy_sync
106
+ destroy
107
+ end
108
+
109
+ # Same as the old destroy function -- doesn't wait for Xvfb to die.
110
+ # Can cause zombies: http://stackoverflow.com/a/31003621/1651458
111
+ def destroy_without_sync
112
+ stop
113
+ CliUtil.kill_process(pid_filename, preserve_pid_file: true)
114
+ end
115
+
116
+ # Whether the xvfb display will be destroyed when the script finishes.
117
+ def destroy_at_exit?
118
+ @destroy_at_exit
119
+ end
120
+
121
+ def video
122
+ @video_recorder ||= VideoRecorder.new(display, dimensions, @video_capture_options)
123
+ end
124
+
125
+ private
126
+
127
+ def attach_xvfb
128
+ possible_display_set = @autopick_display ? @display..MAX_DISPLAY_NUMBER : Array(@display)
129
+ pick_available_display(possible_display_set, @reuse_display)
130
+ end
131
+
132
+ def pick_available_display(display_set, can_reuse)
133
+ display_set.each do |display_number|
134
+ @display = display_number
135
+
136
+ return true if xvfb_running? && can_reuse && (xvfb_mine? || !@autopick_display)
137
+ return true if !xvfb_running? && launch_xvfb
138
+ end
139
+ raise Xvfb::Exception.new("Could not find an available display")
140
+ end
141
+
142
+ def launch_xvfb
143
+ out_pipe, in_pipe = IO.pipe
144
+ @pid = Process.spawn(
145
+ CliUtil.path_to("Xvfb"), ":#{display}", "-screen", "0", dimensions, "-ac",
146
+ err: in_pipe)
147
+ raise Xvfb::Exception.new("Xvfb did not launch - something's wrong") unless @pid
148
+ # According to docs, you should either wait or detach on spawned procs:
149
+ Process.detach @pid
150
+ return ensure_xvfb_launched(out_pipe)
151
+ ensure
152
+ in_pipe.close
153
+ end
154
+
155
+ def ensure_xvfb_launched(out_pipe)
156
+ start_time = Time.now
157
+ errors = ""
158
+ begin
159
+ begin
160
+ errors += out_pipe.read_nonblock(10000)
161
+ if errors.include? "Cannot establish any listening sockets"
162
+ raise Xvfb::Exception.new("Display socket is taken but lock file is missing - check the Xvfb troubleshooting guide")
163
+ end
164
+ if errors.include? "Server is already active for display #{display}"
165
+ # This can happen if there is a race to grab the lock file.
166
+ # Not an exception, just return false to let pick_available_display choose another:
167
+ return false
168
+ end
169
+ rescue IO::WaitReadable
170
+ # will retry next cycle
171
+ end
172
+ sleep 0.01 # to avoid cpu hogging
173
+ raise Xvfb::Exception.new("Xvfb launched but did not complete initialization") if (Time.now-start_time)>=@xvfb_launch_timeout
174
+ # Continue looping until Xvfb has written its pidfile:
175
+ end while !xvfb_running?
176
+
177
+ # If for any reason the pid file doesn't match ours, we lost the race to
178
+ # get the file lock:
179
+ return @pid == read_xvfb_pid
180
+ end
181
+
182
+ def xvfb_mine?
183
+ CliUtil.process_mine?(read_xvfb_pid)
184
+ end
185
+
186
+ # Check whether an Xvfb process is running on @display.
187
+ # NOTE: This might be a process started by someone else!
188
+ def xvfb_running?
189
+ (pid = read_xvfb_pid) && CliUtil.process_running?(pid)
190
+ end
191
+
192
+ def pid_filename
193
+ "/tmp/.X#{display}-lock"
194
+ end
195
+
196
+ def read_xvfb_pid
197
+ CliUtil.read_pid(pid_filename)
198
+ end
199
+
200
+ def hook_at_exit
201
+ unless @at_exit_hook_installed
202
+ @at_exit_hook_installed = true
203
+ at_exit do
204
+ exit_status = $!.status if $!.is_a?(SystemExit)
205
+ destroy if destroy_at_exit?
206
+ exit exit_status if exit_status
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,70 @@
1
+ class Xvfb
2
+ class CliUtil
3
+ def self.application_exists?(app)
4
+ !!path_to(app)
5
+ end
6
+
7
+ def self.ensure_application_exists!(app, error_message)
8
+ if !self.application_exists?(app)
9
+ raise Xvfb::Exception.new(error_message)
10
+ end
11
+ end
12
+
13
+ # Credit: http://stackoverflow.com/a/5471032/6678
14
+ def self.path_to(app)
15
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
16
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
17
+ exts.each { |ext|
18
+ exe = File.join(path, "#{app}#{ext}")
19
+ return exe if File.executable?(exe) && !File.directory?(exe)
20
+ }
21
+ end
22
+ return nil
23
+ end
24
+
25
+ def self.process_mine?(pid)
26
+ Process.kill(0, pid) && true
27
+ rescue Errno::EPERM, Errno::ESRCH
28
+ false
29
+ end
30
+
31
+ def self.process_running?(pid)
32
+ Process.getpgid(pid) && true
33
+ rescue Errno::ESRCH
34
+ false
35
+ end
36
+
37
+ def self.read_pid(pid_filename)
38
+ pid = (File.read(pid_filename) rescue "").strip
39
+ pid.empty? ? nil : pid.to_i
40
+ end
41
+
42
+ def self.fork_process(command, pid_filename, log_filename='/dev/null')
43
+ pid = Process.spawn(command, err: log_filename)
44
+ File.open pid_filename, 'w' do |f|
45
+ f.puts pid
46
+ end
47
+ end
48
+
49
+ def self.kill_process(pid_filename, options={})
50
+ if pid = read_pid(pid_filename)
51
+ begin
52
+ Process.kill 'TERM', pid
53
+ Process.wait pid if options[:wait]
54
+ rescue Errno::ESRCH
55
+ # no such process; assume it's already killed
56
+ rescue Errno::ECHILD
57
+ # Process.wait tried to wait on a dead process
58
+ end
59
+ end
60
+
61
+ unless options[:preserve_pid_file]
62
+ begin
63
+ FileUtils.rm pid_filename
64
+ rescue Errno::ENOENT
65
+ # pid file already removed
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,106 @@
1
+ require 'tempfile'
2
+
3
+ class Xvfb
4
+ class VideoRecorder
5
+ attr_accessor :pid_file_path, :tmp_file_path, :log_file_path, :provider_binary_path
6
+
7
+ # Construct a new Video Recorder instance. Typically done from inside Xvfb, but can be also created manually,
8
+ # and even used separately from Xvfb' Xvfb features.
9
+ # * display - display number to capture
10
+ # * dimensions - dimensions of the captured video
11
+ # * options - available options:
12
+ # * provider - either :ffmpeg or :libav; default is :ffmpeg - switch if your system is provisioned with FFMpeg
13
+ # * provider_binary_path - override path to ffmpeg / libav binary
14
+ # * pid_file_path - override path to PID file, default is placed in /tmp
15
+ # * tmp_file_path - override path to temp file, default is placed in /tmp
16
+ # * log_file_path - set log file path, default is /dev/null
17
+ # * codec - change ffmpeg codec, default is qtrle
18
+ # * frame_rate - change frame rate, default is 30
19
+ # * devices - array of device options - see https://www.ffmpeg.org/ffmpeg-devices.html
20
+ # * extra - array of extra options to append to the FFMpeg command line
21
+ def initialize(display, dimensions, options = {})
22
+ @display = display
23
+ @dimensions = dimensions[/.+(?=x)/]
24
+
25
+ @pid_file_path = options.fetch :pid_file_path, "/tmp/.headless_ffmpeg_#{@display}.pid"
26
+ @tmp_file_path = options.fetch :tmp_file_path, "/tmp/.headless_ffmpeg_#{@display}.mov"
27
+ @log_file_path = options.fetch :log_file_path, "/dev/null"
28
+ @codec = options.fetch :codec, "libx264"
29
+ @frame_rate = options.fetch :frame_rate, 12
30
+ @provider = options.fetch :provider, :ffmpeg # or :libav
31
+
32
+ # If no provider_binary_path was specified, then
33
+ # make a guess based upon the provider.
34
+ @provider_binary_path = options.fetch(:provider_binary_path, guess_the_provider_binary_path)
35
+
36
+ @extra = Array(options.fetch :extra, [])
37
+ @devices = Array(options.fetch :devices, [])
38
+
39
+ CliUtil.ensure_application_exists! provider_binary_path, "#{provider_binary_path} not found on your system. Install it or change video recorder provider"
40
+ end
41
+
42
+ def capture_running?
43
+ CliUtil.read_pid @pid_file_path
44
+ end
45
+
46
+ def start_capture
47
+ CliUtil.fork_process command_line_for_capture, @pid_file_path, @log_file_path
48
+ at_exit do
49
+ exit_status = $!.status if $!.is_a?(SystemExit)
50
+ stop_and_discard
51
+ exit exit_status if exit_status
52
+ end
53
+ end
54
+
55
+ def stop_and_save(path)
56
+ CliUtil.kill_process @pid_file_path, wait: true
57
+ if File.exists? @tmp_file_path
58
+ begin
59
+ FileUtils.mkdir_p File.dirname(path)
60
+ FileUtils.mv @tmp_file_path, path
61
+ rescue
62
+ nil
63
+ end
64
+ end
65
+ end
66
+
67
+ def stop_and_discard
68
+ CliUtil.kill_process @pid_file_path, wait: true
69
+ begin
70
+ FileUtils.rm @tmp_file_path
71
+ rescue Errno::ENOENT
72
+ # that's ok if the file doesn't exist
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def guess_the_provider_binary_path
79
+ @provider== :libav ? 'avconv' : 'ffmpeg'
80
+ end
81
+
82
+ def command_line_for_capture
83
+ if @provider == :libav
84
+ group_of_pic_size_option = '-g 600'
85
+ dimensions = @dimensions
86
+ else
87
+ group_of_pic_size_option = nil
88
+ dimensions = @dimensions.match(/^(\d+x\d+)/)[0]
89
+ end
90
+
91
+ [
92
+ CliUtil.path_to(provider_binary_path),
93
+ "-y",
94
+ "-r #{@frame_rate}",
95
+ "-s #{dimensions}",
96
+ "-f x11grab",
97
+ "-i :#{@display}",
98
+ @devices,
99
+ group_of_pic_size_option,
100
+ "-vcodec #{@codec}",
101
+ @extra,
102
+ @tmp_file_path
103
+ ].flatten.compact.join(' ')
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,37 @@
1
+ require 'xvfb'
2
+ require 'selenium-webdriver'
3
+
4
+ describe 'Integration test' do
5
+ let!(:xvfb) { xvfb.new }
6
+ before { xvfb.start }
7
+
8
+ after { xvfb.destroy_sync }
9
+
10
+ it 'should use xvfb' do
11
+ work_with_browser
12
+ end
13
+
14
+ it 'should record video with ffmpeg' do
15
+ xvfb.video.start_capture
16
+ work_with_browser
17
+ xvfb.video.stop_and_save("test.mov")
18
+ expect(File.exist?("test.mov")).to eq true
19
+ end
20
+
21
+ it 'should raise an error when trying to create the same display' do
22
+ expect {
23
+ FileUtils.mv("/tmp/.X#{xvfb.display}-lock", "/tmp/xvfb-test-tmp")
24
+ xvfb.new(display: xvfb.display, reuse: false)
25
+ }.to raise_error(Xvfb::Exception, /troubleshooting guide/)
26
+ FileUtils.mv("/tmp/xvfb-test-tmp", "/tmp/.X#{xvfb.display}-lock")
27
+ end
28
+
29
+ private
30
+
31
+ def work_with_browser
32
+ driver = Selenium::WebDriver.for :firefox
33
+ driver.navigate.to 'http://google.com'
34
+ expect(driver.title).to match(/Google/)
35
+ driver.close
36
+ end
37
+ end
@@ -0,0 +1,115 @@
1
+ require 'xvfb'
2
+ require 'tempfile'
3
+
4
+ describe Xvfb::VideoRecorder do
5
+ before do
6
+ stub_environment
7
+ end
8
+
9
+ describe "instantiation" do
10
+
11
+ it "throws an error if provider_binary_path is not installed" do
12
+ allow(Xvfb::CliUtil).to receive(:application_exists?).and_return(false)
13
+ expect { Xvfb::VideoRecorder.new(99, "1024x768x32") }.to raise_error(Xvfb::Exception)
14
+ end
15
+
16
+ it "allows provider_binary_path to be specified" do
17
+ Tempfile.open('some_provider') do |f|
18
+ v = Xvfb::VideoRecorder.new(99, "1024x768x32", provider: :libav, provider_binary_path: f.path)
19
+ expect(v.provider_binary_path).to eq(f.path)
20
+ end
21
+ end
22
+
23
+ it "allows provider_binary_path to be specified" do
24
+ Tempfile.open('some_provider') do |f|
25
+ v = Xvfb::VideoRecorder.new(99, "1024x768x32", provider: :libav, provider_binary_path: f.path)
26
+ expect(v.provider_binary_path).to eq(f.path)
27
+ end
28
+ end
29
+
30
+ context "provider_binary_path not specified" do
31
+ it "assumes the provider binary is 'ffmpeg' if the provider is :ffmpeg" do
32
+ v = Xvfb::VideoRecorder.new(99, "1024x768x32", provider: :ffmpeg)
33
+ expect(v.provider_binary_path).to eq("ffmpeg")
34
+ end
35
+
36
+ it "assumes the provider binary is 'avconv' if the provider is :libav" do
37
+ v = Xvfb::VideoRecorder.new(99, "1024x768x32", provider: :libav)
38
+ expect(v.provider_binary_path).to eq("avconv")
39
+ end
40
+
41
+ end
42
+ end
43
+
44
+ describe "#capture" do
45
+ before do
46
+ allow(Xvfb::CliUtil).to receive(:path_to).and_return('ffmpeg')
47
+ end
48
+
49
+ it "starts ffmpeg" do
50
+ expect(Xvfb::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')
51
+
52
+ recorder = Xvfb::VideoRecorder.new(99, "1024x768x32")
53
+ recorder.start_capture
54
+ end
55
+
56
+ it "starts ffmpeg with specified codec" do
57
+ expect(Xvfb::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')
58
+
59
+ recorder = Xvfb::VideoRecorder.new(99, "1024x768x32", {:codec => 'libvpx'})
60
+ recorder.start_capture
61
+ end
62
+
63
+ it "starts ffmpeg from libav provider with correct parameters" do
64
+ expect(Xvfb::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')
65
+
66
+ recorder = Xvfb::VideoRecorder.new(99, "1024x768x32", {:provider => :libav})
67
+ recorder.start_capture
68
+ end
69
+
70
+ it "starts ffmpeg with specified extra device options" do
71
+ expect(Xvfb::CliUtil).to receive(:fork_process).with(/^ffmpeg -y -r 30 -s 1024x768 -f x11grab -draw_mouse 0 -i :99 -g 600 -vcodec qtrle [^ ]+$/, "/tmp/.headless_ffmpeg_99.pid", '/dev/null')
72
+
73
+ recorder = Xvfb::VideoRecorder.new(99, "1024x768x32", {:devices => ["-draw_mouse 0"]})
74
+ recorder.start_capture
75
+ end
76
+ end
77
+
78
+ context "stopping video recording" do
79
+ let(:tmpfile) { '/tmp/ci.mov' }
80
+ let(:filename) { '/tmp/test.mov' }
81
+ let(:pidfile) { '/tmp/pid' }
82
+
83
+ subject do
84
+ recorder = Xvfb::VideoRecorder.new(99, "1024x768x32", :pid_file_path => pidfile, :tmp_file_path => tmpfile)
85
+ recorder.start_capture
86
+ recorder
87
+ end
88
+
89
+ describe "using #stop_and_save" do
90
+ it "stops video recording and saves file" do
91
+ expect(Xvfb::CliUtil).to receive(:kill_process).with(pidfile, :wait => true)
92
+ expect(File).to receive(:exists?).with(tmpfile).and_return(true)
93
+ expect(FileUtils).to receive(:mv).with(tmpfile, filename)
94
+
95
+ subject.stop_and_save(filename)
96
+ end
97
+ end
98
+
99
+ describe "using #stop_and_discard" do
100
+ it "stops video recording and deletes temporary file" do
101
+ expect(Xvfb::CliUtil).to receive(:kill_process).with(pidfile, :wait => true)
102
+ expect(FileUtils).to receive(:rm).with(tmpfile)
103
+
104
+ subject.stop_and_discard
105
+ end
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ def stub_environment
112
+ allow(Xvfb::CliUtil).to receive(:application_exists?).and_return(true)
113
+ allow(Xvfb::CliUtil).to receive(:fork_process).and_return(true)
114
+ end
115
+ end
data/spec/xvfb_spec.rb ADDED
@@ -0,0 +1,172 @@
1
+ require 'xvfb'
2
+
3
+ describe Xvfb do
4
+ before do
5
+ ENV['DISPLAY'] = ":31337"
6
+ stub_environment
7
+ end
8
+
9
+ describe 'launch options' do
10
+ before do
11
+ allow_any_instance_of(Xvfb).to receive(:ensure_xvfb_launched).and_return(true)
12
+ end
13
+
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
+ xvfb = Xvfb.new
17
+ end
18
+
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
+ xvfb = Xvfb.new(:dimensions => "1024x768x16")
22
+ end
23
+ end
24
+
25
+ context 'with stubbed launch_xvfb' do
26
+ before do
27
+ allow_any_instance_of(Xvfb).to receive(:launch_xvfb).and_return(true)
28
+ end
29
+
30
+ context "instantiation" do
31
+ context "when Xvfb is not installed" do
32
+ before do
33
+ allow(Xvfb::CliUtil).to receive(:application_exists?).and_return(false)
34
+ end
35
+
36
+ it "raises an error" do
37
+ expect { Xvfb.new }.to raise_error(Xvfb::Exception)
38
+ end
39
+ end
40
+
41
+ context "when Xvfb is already running and was started by this user" do
42
+ before do
43
+ allow(Xvfb::CliUtil).to receive(:read_pid).with('/tmp/.X99-lock').and_return(31337)
44
+ allow(Xvfb::CliUtil).to receive(:process_running?).with(31337).and_return(true)
45
+ allow(Xvfb::CliUtil).to receive(:process_mine?).with(31337).and_return(true)
46
+
47
+ allow(Xvfb::CliUtil).to receive(:read_pid).with('/tmp/.X100-lock').and_return(nil)
48
+ end
49
+
50
+ context "and display reuse is allowed" do
51
+ let(:options) { {:reuse => true} }
52
+
53
+ it "should reuse the existing Xvfb" do
54
+ expect(Xvfb.new(options).display).to eq 99
55
+ end
56
+
57
+ it "should not be destroyed at exit by default" do
58
+ expect(Xvfb.new(options).destroy_at_exit?).to eq false
59
+ end
60
+ end
61
+
62
+ context "and display reuse is not allowed" do
63
+ let(:options) { {:reuse => false} }
64
+
65
+ it "should pick the next available display number" do
66
+ expect(Xvfb.new(options).display).to eq 100
67
+ end
68
+
69
+ context "and display number is explicitly set" do
70
+ let(:options) { {:reuse => false, :display => 99} }
71
+
72
+ it "should fail with an exception" do
73
+ expect { Xvfb.new(options) }.to raise_error(Xvfb::Exception)
74
+ end
75
+
76
+ context "and autopicking is allowed" do
77
+ let(:options) { {:reuse => false, :display => 99, :autopick => true} }
78
+
79
+ it "should pick the next available display number" do
80
+ expect(Xvfb.new(options).display).to eq 100
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ context 'when Xvfb is started, but by another user' do
88
+ before do
89
+ allow(Xvfb::CliUtil).to receive(:read_pid).with('/tmp/.X99-lock').and_return(31337)
90
+ allow(Xvfb::CliUtil).to receive(:process_running?).with(31337).and_return(true)
91
+ allow(Xvfb::CliUtil).to receive(:process_mine?).with(31337).and_return(false)
92
+
93
+ allow(Xvfb::CliUtil).to receive(:read_pid).with('/tmp/.X100-lock').and_return(nil)
94
+ end
95
+
96
+ context "and display autopicking is not allowed" do
97
+ let(:options) { {:autopick => false} }
98
+
99
+ it "should reuse the display" do
100
+ expect(Xvfb.new(options).display).to eq 99
101
+ end
102
+ end
103
+
104
+ context "and display autopicking is allowed" do
105
+ let(:options) { {:autopick => true} }
106
+
107
+ it "should pick the next display number" do
108
+ expect(Xvfb.new(options).display).to eq 100
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ context "lifecycle" do
115
+ let(:xvfb) { Xvfb.new }
116
+ describe "#start" do
117
+ it "switches to the xvfb server" do
118
+ expect(ENV['DISPLAY']).to eq ":31337"
119
+ xvfb.start
120
+ expect(ENV['DISPLAY']).to eq ":99"
121
+ end
122
+ end
123
+
124
+ describe "#stop" do
125
+ it "switches back from the xvfb server" do
126
+ expect(ENV['DISPLAY']).to eq ":31337"
127
+ xvfb.start
128
+ expect(ENV['DISPLAY']).to eq ":99"
129
+ xvfb.stop
130
+ expect(ENV['DISPLAY']).to eq ":31337"
131
+ end
132
+ end
133
+
134
+ describe "#destroy" do
135
+ before do
136
+ allow(Xvfb::CliUtil).to receive(:read_pid).and_return(4444)
137
+ end
138
+
139
+ it "switches back from the xvfb server and terminates the xvfb session" do
140
+ expect(Process).to receive(:kill).with('TERM', 4444)
141
+
142
+ expect(ENV['DISPLAY']).to eq ":31337"
143
+ xvfb.start
144
+ expect(ENV['DISPLAY']).to eq ":99"
145
+ xvfb.destroy
146
+ expect(ENV['DISPLAY']).to eq ":31337"
147
+ end
148
+ end
149
+ end
150
+
151
+ context "#video" do
152
+ let(:xvfb) { Xvfb.new }
153
+
154
+ it "returns video recorder" do
155
+ expect(xvfb.video).to be_a_kind_of(Xvfb::VideoRecorder)
156
+ end
157
+
158
+ it "returns the same instance" do
159
+ recorder = xvfb.video
160
+ expect(xvfb.video).to eq recorder
161
+ end
162
+ end
163
+ end
164
+
165
+ private
166
+
167
+ def stub_environment
168
+ allow(Xvfb::CliUtil).to receive(:application_exists?).and_return(true)
169
+ allow(Xvfb::CliUtil).to receive(:read_pid).and_return(nil)
170
+ allow(Xvfb::CliUtil).to receive(:path_to).and_return("/usr/bin/Xvfb")
171
+ end
172
+ end
data/xvfb.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ Gem::Specification.new do |s|
2
+ s.author = 'Piotr Krajewski'
3
+ s.email = 'mits87@gmail.com'
4
+
5
+ s.name = 'xvfb'
6
+ s.version = '1.0.4'
7
+ s.summary = 'Ruby interface for Xvfb'
8
+ s.license = 'MIT'
9
+
10
+ s.description = <<-EOF
11
+ Ruby interface for Xvfb. It allows you to create a headless display straight from Ruby code, hiding some low-level action.
12
+ EOF
13
+ s.requirements = 'Xvfb'
14
+ s.homepage = 'https://github.com/mits87/Xvfb'
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+
18
+ s.add_development_dependency 'rake'
19
+ s.add_development_dependency 'rspec', '~> 3'
20
+ s.add_development_dependency 'selenium-webdriver'
21
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xvfb
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Piotr Krajewski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-08-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
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'
55
+ description: " Ruby interface for Xvfb. It allows you to create a headless display
56
+ straight from Ruby code, hiding some low-level action.\n"
57
+ email: mits87@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".travis.yml"
64
+ - Gemfile
65
+ - LICENSE
66
+ - README.md
67
+ - Rakefile
68
+ - lib/xvfb.rb
69
+ - lib/xvfb/cli_util.rb
70
+ - lib/xvfb/video/video_recorder.rb
71
+ - spec/integration_spec.rb
72
+ - spec/video_recorder_spec.rb
73
+ - spec/xvfb_spec.rb
74
+ - xvfb.gemspec
75
+ homepage: https://github.com/mits87/Xvfb
76
+ licenses:
77
+ - MIT
78
+ metadata: {}
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements:
94
+ - Xvfb
95
+ rubyforge_project:
96
+ rubygems_version: 2.6.12
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: Ruby interface for Xvfb
100
+ test_files: []