headless 1.0.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 [](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
|