xvfb 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.travis.yml +19 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +137 -0
- data/Rakefile +1 -0
- data/lib/xvfb.rb +210 -0
- data/lib/xvfb/cli_util.rb +70 -0
- data/lib/xvfb/video/video_recorder.rb +106 -0
- data/spec/integration_spec.rb +37 -0
- data/spec/video_recorder_spec.rb +115 -0
- data/spec/xvfb_spec.rb +172 -0
- data/xvfb.gemspec +21 -0
- metadata +100 -0
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
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
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: []
|