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 +4 -4
- data/.travis.yml +5 -3
- data/CHANGELOG +11 -0
- data/README.md +48 -9
- data/headless.gemspec +3 -2
- data/lib/headless.rb +53 -19
- data/lib/headless/cli_util.rb +7 -5
- data/lib/headless/video/video_recorder.rb +33 -3
- data/spec/headless_spec.rb +140 -109
- data/spec/integration_spec.rb +44 -0
- data/spec/video_recorder_spec.rb +22 -13
- metadata +20 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 36a92b0fed73ea273bf7cc4201b545be19d539e2
|
4
|
+
data.tar.gz: 29feae5dcdb2be2fa1d5147e009910d4c7f7c044
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0a2c40f5cad61642e952565e3ab9666f00a4fe40eaee09ec99eb2facfff76bc4a87976cd2ca42dea02cf5b185d7d4f4d2a812ec44eb9c5a43ed016f9cbef12f6
|
7
|
+
data.tar.gz: ce8ea2ace8823b3507bcef65da7194f57b0fbce038f2fe0eef4f7a7dfbb69e7de678e4172c4761736297eb4b7d05f1e3128c93c6ee95cd6d42e81a18050fe773
|
data/.travis.yml
CHANGED
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
|
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 [
|
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
|
-
|
143
|
+
Call `headless.take_screenshot` to take a screenshot. It needs two arguments:
|
126
144
|
|
127
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
+
© 2011-2015 Leonid Shevtsov, released under the MIT license
|
data/headless.gemspec
CHANGED
@@ -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 = '
|
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", "~>
|
18
|
+
s.add_development_dependency "rspec", "~> 3"
|
19
|
+
s.add_development_dependency "selenium-webdriver"
|
19
20
|
end
|
data/lib/headless.rb
CHANGED
@@ -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
|
-
|
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.
|
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,
|
64
|
-
#
|
65
|
-
# * +
|
66
|
-
#
|
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
|
-
|
120
|
-
|
121
|
-
|
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
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
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
|
-
|
data/lib/headless/cli_util.rb
CHANGED
@@ -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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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(
|
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
|
data/spec/headless_spec.rb
CHANGED
@@ -6,170 +6,201 @@ describe Headless do
|
|
6
6
|
stub_environment
|
7
7
|
end
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
25
|
-
|
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
|
-
|
28
|
-
|
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
|
-
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
41
|
-
|
47
|
+
context "and display reuse is allowed" do
|
48
|
+
let(:options) { {:reuse => true} }
|
42
49
|
|
43
|
-
|
44
|
-
|
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
|
-
|
49
|
-
|
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
|
-
|
52
|
-
|
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
|
56
|
-
let(:options) { {:
|
86
|
+
context "and display autopicking is not allowed" do
|
87
|
+
let(:options) { {:autopick => false} }
|
57
88
|
|
58
|
-
it "should fail with
|
59
|
-
|
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
|
-
|
63
|
-
|
94
|
+
context "and display autopicking is allowed" do
|
95
|
+
let(:options) { {:autopick => true} }
|
64
96
|
|
65
|
-
|
66
|
-
|
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
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
88
|
-
|
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
|
-
|
91
|
-
|
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
|
-
|
98
|
-
|
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
|
-
|
108
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
123
|
-
|
154
|
+
context "#take_screenshot" do
|
155
|
+
let(:headless) { Headless.new }
|
124
156
|
|
125
|
-
|
126
|
-
headless.
|
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
|
-
|
135
|
-
|
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
|
-
|
138
|
-
|
139
|
-
end
|
164
|
+
expect { headless.take_screenshot('a.png') }.to raise_error(Headless::Exception)
|
165
|
+
end
|
140
166
|
|
141
|
-
|
142
|
-
|
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
|
-
|
148
|
-
|
170
|
+
expect { headless.take_screenshot('a.png', :using => :imagemagick) }.to raise_error(Headless::Exception)
|
171
|
+
end
|
149
172
|
|
150
|
-
|
151
|
-
|
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
|
-
|
154
|
-
|
176
|
+
expect { headless.take_screenshot('a.png', :using => :xwd) }.to raise_error(Headless::Exception)
|
177
|
+
end
|
155
178
|
|
156
|
-
|
157
|
-
|
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
|
-
|
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
|
-
|
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.
|
169
|
-
Headless::CliUtil.
|
170
|
-
Headless::CliUtil.
|
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
|
data/spec/video_recorder_spec.rb
CHANGED
@@ -7,30 +7,39 @@ describe Headless::VideoRecorder do
|
|
7
7
|
|
8
8
|
describe "instantiation" do
|
9
9
|
before do
|
10
|
-
Headless::CliUtil.
|
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
|
-
|
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.
|
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.
|
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.
|
50
|
-
File.
|
51
|
-
FileUtils.
|
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.
|
60
|
-
FileUtils.
|
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.
|
71
|
-
Headless::CliUtil.
|
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:
|
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:
|
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: '
|
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: '
|
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.
|
96
|
+
rubygems_version: 2.4.5
|
82
97
|
signing_key:
|
83
98
|
specification_version: 4
|
84
99
|
summary: Ruby headless display interface
|