headless 2.3.1 → 3.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 +5 -5
- data/.github/workflows/test.yml +38 -0
- data/.gitignore +0 -1
- data/CHANGELOG +39 -32
- data/Gemfile +1 -1
- data/Gemfile.lock +96 -0
- data/README.md +39 -20
- data/headless.gemspec +13 -12
- data/lib/headless/cli_util.rb +15 -10
- data/lib/headless/video/video_recorder.rb +26 -37
- data/lib/headless.rb +58 -36
- data/spec/headless_spec.rb +67 -51
- data/spec/integration_spec.rb +8 -8
- data/spec/video_recorder_spec.rb +35 -45
- metadata +26 -16
- data/.travis.yml +0 -19
data/lib/headless.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "headless/cli_util"
|
2
|
+
require "headless/video/video_recorder"
|
3
3
|
|
4
4
|
# A class incapsulating the creation and usage of a headless X server
|
5
5
|
#
|
@@ -40,10 +40,9 @@ require 'headless/video/video_recorder'
|
|
40
40
|
# TODO test that reuse actually works with an existing xvfb session
|
41
41
|
#++
|
42
42
|
class Headless
|
43
|
-
|
44
43
|
DEFAULT_DISPLAY_NUMBER = 99
|
45
44
|
MAX_DISPLAY_NUMBER = 10_000
|
46
|
-
DEFAULT_DISPLAY_DIMENSIONS =
|
45
|
+
DEFAULT_DISPLAY_DIMENSIONS = "1280x1024x24"
|
47
46
|
DEFAULT_XVFB_LAUNCH_TIMEOUT = 10
|
48
47
|
|
49
48
|
class Exception < RuntimeError
|
@@ -73,9 +72,10 @@ class Headless
|
|
73
72
|
# (default true unless reuse is true and a server is already running)
|
74
73
|
# * +xvfb_launch_timeout+ - how long should we wait for Xvfb to open a
|
75
74
|
# display, before assuming that it is frozen (in seconds, default is 10)
|
76
|
-
# * +video+ - options to be passed to the ffmpeg video recorder.
|
75
|
+
# * +video+ - options to be passed to the ffmpeg video recorder.
|
76
|
+
# See Headless::VideoRecorder#initialize for documentation
|
77
77
|
def initialize(options = {})
|
78
|
-
CliUtil.ensure_application_exists!(
|
78
|
+
CliUtil.ensure_application_exists!("Xvfb", "Xvfb not found on your system")
|
79
79
|
|
80
80
|
@display = options.fetch(:display, DEFAULT_DISPLAY_NUMBER).to_i
|
81
81
|
@xvfb_launch_timeout = options.fetch(:xvfb_launch_timeout, DEFAULT_XVFB_LAUNCH_TIMEOUT).to_i
|
@@ -83,8 +83,14 @@ class Headless
|
|
83
83
|
@reuse_display = options.fetch(:reuse, true)
|
84
84
|
@dimensions = options.fetch(:dimensions, DEFAULT_DISPLAY_DIMENSIONS)
|
85
85
|
@video_capture_options = options.fetch(:video, {})
|
86
|
+
@extensions = options.fetch(:extensions, [])
|
87
|
+
@extensions = [@extensions] unless @extensions.is_a? Array
|
86
88
|
|
87
|
-
already_running =
|
89
|
+
already_running = begin
|
90
|
+
xvfb_running?
|
91
|
+
rescue
|
92
|
+
false
|
93
|
+
end
|
88
94
|
@destroy_at_exit = options.fetch(:destroy_at_exit, !(@reuse_display && already_running))
|
89
95
|
|
90
96
|
@pid = nil # the pid of the running Xvfb process
|
@@ -95,14 +101,14 @@ class Headless
|
|
95
101
|
|
96
102
|
# Switches to the headless server
|
97
103
|
def start
|
98
|
-
@old_display = ENV[
|
99
|
-
ENV[
|
104
|
+
@old_display = ENV["DISPLAY"]
|
105
|
+
ENV["DISPLAY"] = ":#{display}"
|
100
106
|
hook_at_exit
|
101
107
|
end
|
102
108
|
|
103
109
|
# Switches back from the headless server
|
104
110
|
def stop
|
105
|
-
ENV[
|
111
|
+
ENV["DISPLAY"] = @old_display
|
106
112
|
end
|
107
113
|
|
108
114
|
# Switches back from the headless server and terminates the headless session
|
@@ -137,37 +143,42 @@ class Headless
|
|
137
143
|
# # perform operations in headless mode
|
138
144
|
# end
|
139
145
|
# See #new for options
|
140
|
-
def self.run(options={}, &block)
|
146
|
+
def self.run(options = {}, &block)
|
141
147
|
headless = Headless.new(options)
|
142
148
|
headless.start
|
143
149
|
yield headless
|
144
150
|
ensure
|
145
|
-
headless
|
151
|
+
headless&.destroy
|
146
152
|
end
|
147
|
-
class <<self; alias_method :ly, :run; end
|
153
|
+
class << self; alias_method :ly, :run; end
|
148
154
|
|
149
155
|
def video
|
150
156
|
@video_recorder ||= VideoRecorder.new(display, dimensions, @video_capture_options)
|
151
157
|
end
|
152
158
|
|
153
|
-
def take_screenshot(file_path, options={})
|
159
|
+
def take_screenshot(file_path, options = {})
|
154
160
|
using = options.fetch(:using, :imagemagick)
|
155
161
|
case using
|
156
162
|
when :imagemagick
|
157
|
-
CliUtil.ensure_application_exists!(
|
158
|
-
|
163
|
+
CliUtil.ensure_application_exists!("import",
|
164
|
+
"imagemagick is not found on your system. " \
|
165
|
+
"Please install it using sudo apt-get install imagemagick")
|
166
|
+
system "#{CliUtil.path_to("import")} -display :#{display} -window root #{file_path}"
|
159
167
|
when :xwd
|
160
|
-
CliUtil.ensure_application_exists!(
|
161
|
-
|
168
|
+
CliUtil.ensure_application_exists!("xwd",
|
169
|
+
"xwd is not found on your system. " \
|
170
|
+
"Please install it using sudo apt-get install X11-apps")
|
171
|
+
system "#{CliUtil.path_to("xwd")} -display localhost:#{display} -silent -root -out #{file_path}"
|
162
172
|
when :graphicsmagick, :gm
|
163
|
-
CliUtil.ensure_application_exists!(
|
164
|
-
|
173
|
+
CliUtil.ensure_application_exists!("gm", "graphicsmagick is not found on your system. " \
|
174
|
+
"Please install it.")
|
175
|
+
system "#{CliUtil.path_to("gm")} import -display localhost:#{display} -window root #{file_path}"
|
165
176
|
else
|
166
|
-
raise Headless::Exception.new(
|
177
|
+
raise Headless::Exception.new("Unknown :using option value")
|
167
178
|
end
|
168
179
|
end
|
169
180
|
|
170
|
-
private
|
181
|
+
private
|
171
182
|
|
172
183
|
def attach_xvfb
|
173
184
|
possible_display_set = @autopick_display ? @display..MAX_DISPLAY_NUMBER : Array(@display)
|
@@ -187,26 +198,30 @@ private
|
|
187
198
|
def launch_xvfb
|
188
199
|
out_pipe, in_pipe = IO.pipe
|
189
200
|
@pid = Process.spawn(
|
190
|
-
CliUtil.path_to("Xvfb"), ":#{display}", "-screen", "0", dimensions, "-ac",
|
191
|
-
err: in_pipe
|
201
|
+
CliUtil.path_to("Xvfb"), ":#{display}", "-screen", "0", dimensions, "-ac", *extensions,
|
202
|
+
err: in_pipe
|
203
|
+
)
|
192
204
|
raise Headless::Exception.new("Xvfb did not launch - something's wrong") unless @pid
|
205
|
+
|
193
206
|
# According to docs, you should either wait or detach on spawned procs:
|
194
207
|
Process.detach @pid
|
195
|
-
|
196
|
-
|
197
|
-
|
208
|
+
ensure_xvfb_launched(out_pipe)
|
209
|
+
ensure
|
210
|
+
in_pipe.close
|
198
211
|
end
|
199
212
|
|
200
213
|
def ensure_xvfb_launched(out_pipe)
|
201
214
|
start_time = Time.now
|
202
215
|
errors = ""
|
203
|
-
|
216
|
+
loop do
|
204
217
|
begin
|
205
218
|
errors += out_pipe.read_nonblock(10000)
|
206
|
-
if errors.include? "
|
207
|
-
raise Headless::Exception
|
208
|
-
|
209
|
-
|
219
|
+
if errors.include? "directory /tmp/.X11-unix will not be created."
|
220
|
+
raise Headless::Exception, "/tmp/.X11-unix is missing - check the Headless troubleshooting guide"
|
221
|
+
elsif errors.include? "Cannot establish any listening sockets"
|
222
|
+
raise Headless::Exception,
|
223
|
+
"Display socket is taken but lock file is missing - check the Headless troubleshooting guide"
|
224
|
+
elsif errors.include? "Server is already active for display #{display}"
|
210
225
|
# This can happen if there is a race to grab the lock file.
|
211
226
|
# Not an exception, just return false to let pick_available_display choose another:
|
212
227
|
return false
|
@@ -215,13 +230,16 @@ private
|
|
215
230
|
# will retry next cycle
|
216
231
|
end
|
217
232
|
sleep 0.01 # to avoid cpu hogging
|
218
|
-
|
219
|
-
|
220
|
-
|
233
|
+
if (Time.now - start_time) >= @xvfb_launch_timeout
|
234
|
+
raise Headless::Exception.new("Xvfb launched but did not complete initialization")
|
235
|
+
end
|
236
|
+
# Continue looping until Xvfb has written its pidfile:
|
237
|
+
break if xvfb_running?
|
238
|
+
end
|
221
239
|
|
222
240
|
# If for any reason the pid file doesn't match ours, we lost the race to
|
223
241
|
# get the file lock:
|
224
|
-
|
242
|
+
@pid == read_xvfb_pid
|
225
243
|
end
|
226
244
|
|
227
245
|
def xvfb_mine?
|
@@ -252,4 +270,8 @@ private
|
|
252
270
|
end
|
253
271
|
end
|
254
272
|
end
|
273
|
+
|
274
|
+
def extensions
|
275
|
+
@extensions.map { |ext| "+" + ext.to_s }
|
276
|
+
end
|
255
277
|
end
|
data/spec/headless_spec.rb
CHANGED
@@ -1,28 +1,44 @@
|
|
1
|
-
require
|
1
|
+
require "headless"
|
2
2
|
|
3
3
|
describe Headless do
|
4
4
|
before do
|
5
|
-
ENV[
|
5
|
+
ENV["DISPLAY"] = ":31337"
|
6
6
|
stub_environment
|
7
7
|
end
|
8
8
|
|
9
|
-
describe
|
9
|
+
describe "launch options" do
|
10
10
|
before do
|
11
11
|
allow_any_instance_of(Headless).to receive(:ensure_xvfb_launched).and_return(true)
|
12
12
|
end
|
13
13
|
|
14
14
|
it "starts Xvfb" do
|
15
|
-
expect(Process).to receive(:spawn).with(*(%w
|
16
|
-
|
15
|
+
expect(Process).to receive(:spawn).with(*(%w[/usr/bin/Xvfb :99 -screen 0 1280x1024x24
|
16
|
+
-ac] + [hash_including(:err)])).and_return(123)
|
17
|
+
Headless.new
|
17
18
|
end
|
18
19
|
|
19
20
|
it "allows setting screen dimensions" do
|
20
|
-
expect(Process).to receive(:spawn).with(*(%w
|
21
|
-
|
21
|
+
expect(Process).to receive(:spawn).with(*(%w[/usr/bin/Xvfb :99 -screen 0 1024x768x16
|
22
|
+
-ac] + [hash_including(:err)])).and_return(123)
|
23
|
+
Headless.new(dimensions: "1024x768x16")
|
24
|
+
end
|
25
|
+
|
26
|
+
it "allows to enable extensions", focus: true do
|
27
|
+
expect(Process).to receive(:spawn).with(*(%w[/usr/bin/Xvfb :99 -screen 0 1280x1024x24 -ac
|
28
|
+
+iglx] + [hash_including(:err)])).and_return(123)
|
29
|
+
Headless.new(extensions: [:iglx])
|
30
|
+
|
31
|
+
expect(Process).to receive(:spawn).with(*(%w[/usr/bin/Xvfb :99 -screen 0 1280x1024x24 -ac
|
32
|
+
+iglx] + [hash_including(:err)])).and_return(123)
|
33
|
+
Headless.new(extensions: "iglx")
|
34
|
+
|
35
|
+
expect(Process).to receive(:spawn).with(*(%w[/usr/bin/Xvfb :99 -screen 0 1280x1024x24 -ac +iglx
|
36
|
+
+dummy] + [hash_including(:err)])).and_return(123)
|
37
|
+
Headless.new(extensions: ["iglx", :dummy])
|
22
38
|
end
|
23
39
|
end
|
24
40
|
|
25
|
-
context
|
41
|
+
context "with stubbed launch_xvfb" do
|
26
42
|
before do
|
27
43
|
allow_any_instance_of(Headless).to receive(:launch_xvfb).and_return(true)
|
28
44
|
end
|
@@ -40,15 +56,15 @@ describe Headless do
|
|
40
56
|
|
41
57
|
context "when Xvfb is already running and was started by this user" do
|
42
58
|
before do
|
43
|
-
allow(Headless::CliUtil).to receive(:read_pid).with(
|
59
|
+
allow(Headless::CliUtil).to receive(:read_pid).with("/tmp/.X99-lock").and_return(31337)
|
44
60
|
allow(Headless::CliUtil).to receive(:process_running?).with(31337).and_return(true)
|
45
61
|
allow(Headless::CliUtil).to receive(:process_mine?).with(31337).and_return(true)
|
46
62
|
|
47
|
-
allow(Headless::CliUtil).to receive(:read_pid).with(
|
63
|
+
allow(Headless::CliUtil).to receive(:read_pid).with("/tmp/.X100-lock").and_return(nil)
|
48
64
|
end
|
49
65
|
|
50
66
|
context "and display reuse is allowed" do
|
51
|
-
let(:options) { {:
|
67
|
+
let(:options) { {reuse: true} }
|
52
68
|
|
53
69
|
it "should reuse the existing Xvfb" do
|
54
70
|
expect(Headless.new(options).display).to eq 99
|
@@ -60,21 +76,21 @@ describe Headless do
|
|
60
76
|
end
|
61
77
|
|
62
78
|
context "and display reuse is not allowed" do
|
63
|
-
let(:options) { {:
|
79
|
+
let(:options) { {reuse: false} }
|
64
80
|
|
65
81
|
it "should pick the next available display number" do
|
66
82
|
expect(Headless.new(options).display).to eq 100
|
67
83
|
end
|
68
84
|
|
69
85
|
context "and display number is explicitly set" do
|
70
|
-
let(:options) { {:
|
86
|
+
let(:options) { {reuse: false, display: 99} }
|
71
87
|
|
72
88
|
it "should fail with an exception" do
|
73
89
|
expect { Headless.new(options) }.to raise_error(Headless::Exception)
|
74
90
|
end
|
75
91
|
|
76
92
|
context "and autopicking is allowed" do
|
77
|
-
let(:options) { {:
|
93
|
+
let(:options) { {reuse: false, display: 99, autopick: true} }
|
78
94
|
|
79
95
|
it "should pick the next available display number" do
|
80
96
|
expect(Headless.new(options).display).to eq 100
|
@@ -84,17 +100,17 @@ describe Headless do
|
|
84
100
|
end
|
85
101
|
end
|
86
102
|
|
87
|
-
context
|
103
|
+
context "when Xvfb is started, but by another user" do
|
88
104
|
before do
|
89
|
-
allow(Headless::CliUtil).to receive(:read_pid).with(
|
105
|
+
allow(Headless::CliUtil).to receive(:read_pid).with("/tmp/.X99-lock").and_return(31337)
|
90
106
|
allow(Headless::CliUtil).to receive(:process_running?).with(31337).and_return(true)
|
91
107
|
allow(Headless::CliUtil).to receive(:process_mine?).with(31337).and_return(false)
|
92
108
|
|
93
|
-
allow(Headless::CliUtil).to receive(:read_pid).with(
|
109
|
+
allow(Headless::CliUtil).to receive(:read_pid).with("/tmp/.X100-lock").and_return(nil)
|
94
110
|
end
|
95
111
|
|
96
112
|
context "and display autopicking is not allowed" do
|
97
|
-
let(:options) { {:
|
113
|
+
let(:options) { {autopick: false} }
|
98
114
|
|
99
115
|
it "should reuse the display" do
|
100
116
|
expect(Headless.new(options).display).to eq 99
|
@@ -102,7 +118,7 @@ describe Headless do
|
|
102
118
|
end
|
103
119
|
|
104
120
|
context "and display autopicking is allowed" do
|
105
|
-
let(:options) { {:
|
121
|
+
let(:options) { {autopick: true} }
|
106
122
|
|
107
123
|
it "should pick the next display number" do
|
108
124
|
expect(Headless.new(options).display).to eq 100
|
@@ -115,19 +131,19 @@ describe Headless do
|
|
115
131
|
let(:headless) { Headless.new }
|
116
132
|
describe "#start" do
|
117
133
|
it "switches to the headless server" do
|
118
|
-
expect(ENV[
|
134
|
+
expect(ENV["DISPLAY"]).to eq ":31337"
|
119
135
|
headless.start
|
120
|
-
expect(ENV[
|
136
|
+
expect(ENV["DISPLAY"]).to eq ":99"
|
121
137
|
end
|
122
138
|
end
|
123
139
|
|
124
140
|
describe "#stop" do
|
125
141
|
it "switches back from the headless server" do
|
126
|
-
expect(ENV[
|
142
|
+
expect(ENV["DISPLAY"]).to eq ":31337"
|
127
143
|
headless.start
|
128
|
-
expect(ENV[
|
144
|
+
expect(ENV["DISPLAY"]).to eq ":99"
|
129
145
|
headless.stop
|
130
|
-
expect(ENV[
|
146
|
+
expect(ENV["DISPLAY"]).to eq ":31337"
|
131
147
|
end
|
132
148
|
end
|
133
149
|
|
@@ -137,13 +153,13 @@ describe Headless do
|
|
137
153
|
end
|
138
154
|
|
139
155
|
it "switches back from the headless server and terminates the headless session" do
|
140
|
-
expect(Process).to receive(:kill).with(
|
156
|
+
expect(Process).to receive(:kill).with("TERM", 4444)
|
141
157
|
|
142
|
-
expect(ENV[
|
158
|
+
expect(ENV["DISPLAY"]).to eq ":31337"
|
143
159
|
headless.start
|
144
|
-
expect(ENV[
|
160
|
+
expect(ENV["DISPLAY"]).to eq ":99"
|
145
161
|
headless.destroy
|
146
|
-
expect(ENV[
|
162
|
+
expect(ENV["DISPLAY"]).to eq ":31337"
|
147
163
|
end
|
148
164
|
end
|
149
165
|
end
|
@@ -165,72 +181,72 @@ describe Headless do
|
|
165
181
|
let(:headless) { Headless.new }
|
166
182
|
|
167
183
|
it "raises an error if unknown value for option :using is used" do
|
168
|
-
expect { headless.take_screenshot(
|
184
|
+
expect { headless.take_screenshot("a.png", using: :teleportation) }.to raise_error(Headless::Exception)
|
169
185
|
end
|
170
186
|
|
171
187
|
it "raises an error if imagemagick is not installed, with default options" do
|
172
|
-
allow(Headless::CliUtil).to receive(:application_exists?).with(
|
188
|
+
allow(Headless::CliUtil).to receive(:application_exists?).with("import").and_return(false)
|
173
189
|
|
174
|
-
expect { headless.take_screenshot(
|
190
|
+
expect { headless.take_screenshot("a.png") }.to raise_error(Headless::Exception)
|
175
191
|
end
|
176
192
|
|
177
193
|
it "raises an error if imagemagick is not installed, with using: :imagemagick" do
|
178
|
-
allow(Headless::CliUtil).to receive(:application_exists?).with(
|
194
|
+
allow(Headless::CliUtil).to receive(:application_exists?).with("import").and_return(false)
|
179
195
|
|
180
|
-
expect { headless.take_screenshot(
|
196
|
+
expect { headless.take_screenshot("a.png", using: :imagemagick) }.to raise_error(Headless::Exception)
|
181
197
|
end
|
182
198
|
|
183
199
|
it "raises an error if xwd is not installed, with using: :xwd" do
|
184
|
-
allow(Headless::CliUtil).to receive(:application_exists?).with(
|
200
|
+
allow(Headless::CliUtil).to receive(:application_exists?).with("xwd").and_return(false)
|
185
201
|
|
186
|
-
expect { headless.take_screenshot(
|
202
|
+
expect { headless.take_screenshot("a.png", using: :xwd) }.to raise_error(Headless::Exception)
|
187
203
|
end
|
188
204
|
|
189
205
|
it "raises an error if gm is not installed with using: :graphicsmagick" do
|
190
|
-
allow(Headless::CliUtil).to receive(:application_exists?).with(
|
206
|
+
allow(Headless::CliUtil).to receive(:application_exists?).with("gm").and_return(false)
|
191
207
|
|
192
|
-
expect { headless.take_screenshot(
|
208
|
+
expect { headless.take_screenshot("a.png", using: :graphicsmagick) }.to raise_error(Headless::Exception)
|
193
209
|
end
|
194
210
|
|
195
211
|
it "raises an error if gm is not installed with using: :gm" do
|
196
|
-
allow(Headless::CliUtil).to receive(:application_exists?).with(
|
212
|
+
allow(Headless::CliUtil).to receive(:application_exists?).with("gm").and_return(false)
|
197
213
|
|
198
|
-
expect { headless.take_screenshot(
|
214
|
+
expect { headless.take_screenshot("a.png", using: :gm) }.to raise_error(Headless::Exception)
|
199
215
|
end
|
200
216
|
|
201
217
|
it "issues command to take screenshot, with default options" do
|
202
|
-
allow(Headless::CliUtil).to receive(:path_to).with(
|
203
|
-
expect(headless).to receive(:system).with("path/import -display
|
218
|
+
allow(Headless::CliUtil).to receive(:path_to).with("import").and_return("path/import")
|
219
|
+
expect(headless).to receive(:system).with("path/import -display :99 -window root /tmp/image.png")
|
204
220
|
headless.take_screenshot("/tmp/image.png")
|
205
221
|
end
|
206
222
|
|
207
223
|
it "issues command to take screenshot, with using: :imagemagick" do
|
208
|
-
allow(Headless::CliUtil).to receive(:path_to).with(
|
209
|
-
expect(headless).to receive(:system).with("path/import -display
|
210
|
-
headless.take_screenshot("/tmp/image.png", :
|
224
|
+
allow(Headless::CliUtil).to receive(:path_to).with("import").and_return("path/import")
|
225
|
+
expect(headless).to receive(:system).with("path/import -display :99 -window root /tmp/image.png")
|
226
|
+
headless.take_screenshot("/tmp/image.png", using: :imagemagick)
|
211
227
|
end
|
212
228
|
|
213
229
|
it "issues command to take screenshot, with using: :xwd" do
|
214
|
-
allow(Headless::CliUtil).to receive(:path_to).with(
|
230
|
+
allow(Headless::CliUtil).to receive(:path_to).with("xwd").and_return("path/xwd")
|
215
231
|
expect(headless).to receive(:system).with("path/xwd -display localhost:99 -silent -root -out /tmp/image.png")
|
216
|
-
headless.take_screenshot("/tmp/image.png", :
|
232
|
+
headless.take_screenshot("/tmp/image.png", using: :xwd)
|
217
233
|
end
|
218
234
|
|
219
235
|
it "issues command to take screenshot, with using: :graphicsmagick" do
|
220
|
-
allow(Headless::CliUtil).to receive(:path_to).with(
|
236
|
+
allow(Headless::CliUtil).to receive(:path_to).with("gm").and_return("path/gm")
|
221
237
|
expect(headless).to receive(:system).with("path/gm import -display localhost:99 -window root /tmp/image.png")
|
222
|
-
headless.take_screenshot("/tmp/image.png", :
|
238
|
+
headless.take_screenshot("/tmp/image.png", using: :graphicsmagick)
|
223
239
|
end
|
224
240
|
|
225
241
|
it "issues command to take screenshot, with using: :gm" do
|
226
|
-
allow(Headless::CliUtil).to receive(:path_to).with(
|
242
|
+
allow(Headless::CliUtil).to receive(:path_to).with("gm").and_return("path/gm")
|
227
243
|
expect(headless).to receive(:system).with("path/gm import -display localhost:99 -window root /tmp/image.png")
|
228
|
-
headless.take_screenshot("/tmp/image.png", :
|
244
|
+
headless.take_screenshot("/tmp/image.png", using: :gm)
|
229
245
|
end
|
230
246
|
end
|
231
247
|
end
|
232
248
|
|
233
|
-
private
|
249
|
+
private
|
234
250
|
|
235
251
|
def stub_environment
|
236
252
|
allow(Headless::CliUtil).to receive(:application_exists?).and_return(true)
|
data/spec/integration_spec.rb
CHANGED
@@ -1,29 +1,29 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "headless"
|
2
|
+
require "selenium-webdriver"
|
3
3
|
|
4
|
-
describe
|
4
|
+
describe "Integration test" do
|
5
5
|
let!(:headless) { Headless.new }
|
6
6
|
before { headless.start }
|
7
7
|
|
8
8
|
after { headless.destroy_sync }
|
9
9
|
|
10
|
-
it
|
10
|
+
it "should use xvfb" do
|
11
11
|
work_with_browser
|
12
12
|
end
|
13
13
|
|
14
|
-
it
|
14
|
+
it "should record screenshots" do
|
15
15
|
headless.take_screenshot("test.jpg")
|
16
16
|
expect(File.exist?("test.jpg")).to eq true
|
17
17
|
end
|
18
18
|
|
19
|
-
it
|
19
|
+
it "should record video with ffmpeg" do
|
20
20
|
headless.video.start_capture
|
21
21
|
work_with_browser
|
22
22
|
headless.video.stop_and_save("test.mov")
|
23
23
|
expect(File.exist?("test.mov")).to eq true
|
24
24
|
end
|
25
25
|
|
26
|
-
it
|
26
|
+
it "should raise an error when trying to create the same display" do
|
27
27
|
expect {
|
28
28
|
FileUtils.mv("/tmp/.X#{headless.display}-lock", "/tmp/headless-test-tmp")
|
29
29
|
Headless.new(display: headless.display, reuse: false)
|
@@ -35,7 +35,7 @@ describe 'Integration test' do
|
|
35
35
|
|
36
36
|
def work_with_browser
|
37
37
|
driver = Selenium::WebDriver.for :firefox
|
38
|
-
driver.navigate.to
|
38
|
+
driver.navigate.to "http://google.com"
|
39
39
|
expect(driver.title).to match(/Google/)
|
40
40
|
driver.close
|
41
41
|
end
|
data/spec/video_recorder_spec.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
require
|
1
|
+
require "headless"
|
2
2
|
|
3
|
-
require
|
3
|
+
require "tempfile"
|
4
4
|
|
5
5
|
describe Headless::VideoRecorder do
|
6
6
|
before do
|
@@ -8,89 +8,79 @@ describe Headless::VideoRecorder do
|
|
8
8
|
end
|
9
9
|
|
10
10
|
describe "instantiation" do
|
11
|
-
|
12
|
-
it "throws an error if provider_binary_path is not installed" do
|
11
|
+
it "throws an error if ffmpeg_path is not installed" do
|
13
12
|
allow(Headless::CliUtil).to receive(:application_exists?).and_return(false)
|
14
13
|
expect { Headless::VideoRecorder.new(99, "1024x768x32") }.to raise_error(Headless::Exception)
|
15
14
|
end
|
16
15
|
|
17
|
-
it "allows
|
18
|
-
Tempfile.open(
|
19
|
-
v = Headless::VideoRecorder.new(99, "1024x768x32",
|
20
|
-
expect(v.
|
16
|
+
it "allows ffmpeg_path to be specified" do
|
17
|
+
Tempfile.open("ffmpeg") do |f|
|
18
|
+
v = Headless::VideoRecorder.new(99, "1024x768x32", ffmpeg_path: f.path)
|
19
|
+
expect(v.ffmpeg_path).to eq(f.path)
|
21
20
|
end
|
22
21
|
end
|
23
22
|
|
24
|
-
it "
|
25
|
-
Tempfile.open(
|
26
|
-
v = Headless::VideoRecorder.new(99, "1024x768x32",
|
27
|
-
expect(v.
|
23
|
+
it "supports provider_binary_path for backward compatibility" do
|
24
|
+
Tempfile.open("ffmpeg") do |f|
|
25
|
+
v = Headless::VideoRecorder.new(99, "1024x768x32", provider_binary_path: f.path)
|
26
|
+
expect(v.ffmpeg_path).to eq(f.path)
|
28
27
|
end
|
29
28
|
end
|
30
|
-
|
31
|
-
context "provider_binary_path not specified" do
|
32
|
-
it "assumes the provider binary is 'ffmpeg' if the provider is :ffmpeg" do
|
33
|
-
v = Headless::VideoRecorder.new(99, "1024x768x32", provider: :ffmpeg)
|
34
|
-
expect(v.provider_binary_path).to eq("ffmpeg")
|
35
|
-
end
|
36
|
-
|
37
|
-
it "assumes the provider binary is 'avconv' if the provider is :libav" do
|
38
|
-
v = Headless::VideoRecorder.new(99, "1024x768x32", provider: :libav)
|
39
|
-
expect(v.provider_binary_path).to eq("avconv")
|
40
|
-
end
|
41
|
-
|
42
|
-
end
|
43
29
|
end
|
44
30
|
|
45
31
|
describe "#capture" do
|
46
32
|
before do
|
47
|
-
allow(Headless::CliUtil).to receive(:path_to).and_return(
|
33
|
+
allow(Headless::CliUtil).to receive(:path_to).and_return("ffmpeg")
|
48
34
|
end
|
49
35
|
|
50
36
|
it "starts ffmpeg" do
|
51
|
-
expect(Headless::CliUtil).to receive(:fork_process).with(
|
37
|
+
expect(Headless::CliUtil).to receive(:fork_process).with(
|
38
|
+
/^ffmpeg -y -r 30 -s 1024x768 -f x11grab -i :99 -vcodec qtrle [^ ]+$/,
|
39
|
+
"/tmp/.headless_ffmpeg_99.pid",
|
40
|
+
File::NULL
|
41
|
+
)
|
52
42
|
|
53
43
|
recorder = Headless::VideoRecorder.new(99, "1024x768x32")
|
54
44
|
recorder.start_capture
|
55
45
|
end
|
56
46
|
|
57
47
|
it "starts ffmpeg with specified codec" do
|
58
|
-
expect(Headless::CliUtil).to receive(:fork_process).with(
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
it "starts ffmpeg from ffmpeg provider with correct parameters" do
|
65
|
-
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')
|
48
|
+
expect(Headless::CliUtil).to receive(:fork_process).with(
|
49
|
+
/^ffmpeg -y -r 30 -s 1024x768 -f x11grab -i :99 -vcodec libvpx [^ ]+$/,
|
50
|
+
"/tmp/.headless_ffmpeg_99.pid",
|
51
|
+
File::NULL
|
52
|
+
)
|
66
53
|
|
67
|
-
recorder = Headless::VideoRecorder.new(99, "1024x768x32", {:
|
54
|
+
recorder = Headless::VideoRecorder.new(99, "1024x768x32", {codec: "libvpx"})
|
68
55
|
recorder.start_capture
|
69
56
|
end
|
70
57
|
|
71
58
|
it "starts ffmpeg with specified extra device options" do
|
72
|
-
expect(Headless::CliUtil).to receive(:fork_process).with(
|
59
|
+
expect(Headless::CliUtil).to receive(:fork_process).with(
|
60
|
+
/^ffmpeg -y -r 30 -s 1024x768 -f x11grab -i :99 -draw_mouse 0 -vcodec qtrle [^ ]+$/,
|
61
|
+
"/tmp/.headless_ffmpeg_99.pid", File::NULL
|
62
|
+
)
|
73
63
|
|
74
|
-
recorder = Headless::VideoRecorder.new(99, "1024x768x32", {:
|
64
|
+
recorder = Headless::VideoRecorder.new(99, "1024x768x32", {devices: ["-draw_mouse 0"]})
|
75
65
|
recorder.start_capture
|
76
66
|
end
|
77
67
|
end
|
78
68
|
|
79
69
|
context "stopping video recording" do
|
80
|
-
let(:tmpfile) {
|
81
|
-
let(:filename) {
|
82
|
-
let(:pidfile) {
|
70
|
+
let(:tmpfile) { "/tmp/ci.mov" }
|
71
|
+
let(:filename) { "/tmp/test.mov" }
|
72
|
+
let(:pidfile) { "/tmp/pid" }
|
83
73
|
|
84
74
|
subject do
|
85
|
-
recorder = Headless::VideoRecorder.new(99, "1024x768x32", :
|
75
|
+
recorder = Headless::VideoRecorder.new(99, "1024x768x32", pid_file_path: pidfile, tmp_file_path: tmpfile)
|
86
76
|
recorder.start_capture
|
87
77
|
recorder
|
88
78
|
end
|
89
79
|
|
90
80
|
describe "using #stop_and_save" do
|
91
81
|
it "stops video recording and saves file" do
|
92
|
-
expect(Headless::CliUtil).to receive(:kill_process).with(pidfile, :
|
93
|
-
expect(File).to receive(:
|
82
|
+
expect(Headless::CliUtil).to receive(:kill_process).with(pidfile, wait: true)
|
83
|
+
expect(File).to receive(:exist?).with(tmpfile).and_return(true)
|
94
84
|
expect(FileUtils).to receive(:mv).with(tmpfile, filename)
|
95
85
|
|
96
86
|
subject.stop_and_save(filename)
|
@@ -99,7 +89,7 @@ describe Headless::VideoRecorder do
|
|
99
89
|
|
100
90
|
describe "using #stop_and_discard" do
|
101
91
|
it "stops video recording and deletes temporary file" do
|
102
|
-
expect(Headless::CliUtil).to receive(:kill_process).with(pidfile, :
|
92
|
+
expect(Headless::CliUtil).to receive(:kill_process).with(pidfile, wait: true)
|
103
93
|
expect(FileUtils).to receive(:rm).with(tmpfile)
|
104
94
|
|
105
95
|
subject.stop_and_discard
|