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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ecdab81182658752d292ec13079deae78ed26241
4
- data.tar.gz: 2f61437e9fe159aa2160e45263f731e20a43ffb8
3
+ metadata.gz: 36a92b0fed73ea273bf7cc4201b545be19d539e2
4
+ data.tar.gz: 29feae5dcdb2be2fa1d5147e009910d4c7f7c044
5
5
  SHA512:
6
- metadata.gz: ca8f638bd43c1ddbda26171974b2c4bd11e0508d5c973c5ce35553658e8e647df8bb9027fa62e31c00fde382bf8f87ebd352d762904e3bddefc653169b8b5731
7
- data.tar.gz: af8d46f3c14dbc2d0f8bea5bdfc04e7b2e917f878ac208abcf91f30c3bbc5f66582540c488b8a89b97f76527f4da0e825e0a6d6e6af715160d78b3ad4d7283a2
6
+ metadata.gz: 0a2c40f5cad61642e952565e3ab9666f00a4fe40eaee09ec99eb2facfff76bc4a87976cd2ca42dea02cf5b185d7d4f4d2a812ec44eb9c5a43ed016f9cbef12f6
7
+ data.tar.gz: ce8ea2ace8823b3507bcef65da7194f57b0fbce038f2fe0eef4f7a7dfbb69e7de678e4172c4761736297eb4b7d05f1e3128c93c6ee95cd6d42e81a18050fe773
@@ -1,7 +1,9 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.8.7
4
- - 1.9.2
5
3
  - 1.9.3
6
- - ree
4
+ - 2.0
5
+ - 2.2
6
+ before_install:
7
+ - "sudo apt-get update"
8
+ - "sudo apt-get install -y firefox xvfb libav-tools"
7
9
  script: "rspec"
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 some low-level action.
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 [rdoc.info](http://rdoc.info/projects/leonid-shevtsov/headless)
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
- Images are captured using `import` utility which is part of `imagemagick` library. You can install it on Ubuntu via `sudo apt-get install imagemagick`. You can call `headless.take_screenshot` at any time. You have to supply full path to target file. File format is determined by supplied file extension.
143
+ Call `headless.take_screenshot` to take a screenshot. It needs two arguments:
126
144
 
127
- ## Contributors
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
- * [Igor Afonov](http://iafonov.github.com) - video and screenshot capturing functionality.
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
- © 2011 Leonid Shevtsov, released under the MIT license
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
- [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/leonid-shevtsov/headless/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
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
+ &copy; 2011-2015 Leonid Shevtsov, released under the MIT license
@@ -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 = '1.0.2'
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", "~> 2.6"
18
+ s.add_development_dependency "rspec", "~> 3"
19
+ s.add_development_dependency "selenium-webdriver"
19
20
  end
@@ -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
- # How long should we wait for Xvfb to open a display, before assuming that it is frozen (in seconds)
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. Call #start for that
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, should we use it or try another?
64
- # * +autopick+ (default true is display number isn't explicitly set) - if Headless should automatically pick a display, or fail if the given one is not available.
65
- # * +dimensions+ (default 1280x1024x24) - display dimensions and depth. Not all combinations are possible, refer to +man Xvfb+.
66
- # * +destroy_at_exit+ (default true) - if a display is started but not stopped, should it be destroyed when the script finishes?
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
- CliUtil.ensure_application_exists!('import', "imagemagick not found on your system. Please install it using sudo apt-get install imagemagick")
120
-
121
- system "#{CliUtil.path_to('import')} -display localhost:#{display} -window root #{file_path}"
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
- #TODO error reporting
146
- result = system "#{CliUtil.path_to("Xvfb")} :#{display} -screen 0 #{dimensions} -ac >/dev/null 2>&1 &"
147
- raise Headless::Exception.new("Xvfb did not launch - something's wrong") unless result
148
- ensure_xvfb_is_running
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 is frozen") if (Time.now-start_time)>=XVFB_LAUNCH_TIMEOUT
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
-
@@ -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
- begin
58
- FileUtils.rm pid_filename
59
- rescue Errno::ENOENT
60
- # pid file already removed
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("#{CliUtil.path_to('ffmpeg')} -y -r #{@frame_rate} -g 600 -s #{@dimensions} -f x11grab -i :#{@display} -vcodec #{@codec} #{@tmp_file_path}", @pid_file_path, @log_file_path)
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
@@ -6,170 +6,201 @@ describe Headless do
6
6
  stub_environment
7
7
  end
8
8
 
9
- context "instantiation" do
10
- context "when Xvfb is not installed" do
11
- before do
12
- Headless::CliUtil.stub(:application_exists?).and_return(false)
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
- it "raises an error" do
16
- lambda { Headless.new }.should raise_error(Headless::Exception)
17
- end
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
- context "when Xvfb is not started yet" do
21
- it "starts Xvfb" do
22
- Headless.any_instance.should_receive(:system).with("/usr/bin/Xvfb :99 -screen 0 1280x1024x24 -ac >/dev/null 2>&1 &").and_return(true)
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
- headless = Headless.new
25
- end
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
- it "allows setting screen dimensions" do
28
- Headless.any_instance.should_receive(:system).with("/usr/bin/Xvfb :99 -screen 0 1024x768x16 -ac >/dev/null 2>&1 &").and_return(true)
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
- headless = Headless.new(:dimensions => "1024x768x16")
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
- context "when Xvfb is already running" do
35
- before do
36
- Headless::CliUtil.stub(:read_pid).with('/tmp/.X99-lock').and_return(31337)
37
- Headless::CliUtil.stub(:read_pid).with('/tmp/.X100-lock').and_return(nil)
38
- end
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
- context "and display reuse is allowed" do
41
- let(:options) { {:reuse => true} }
47
+ context "and display reuse is allowed" do
48
+ let(:options) { {:reuse => true} }
42
49
 
43
- it "should reuse the existing Xvfb" do
44
- Headless.new(options).display.should == 99
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
- context "and display reuse is not allowed" do
49
- let(:options) { {:reuse => false} }
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
- it "should pick the next available display number" do
52
- Headless.new(options).display.should == 100
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 number is explicitly set" do
56
- let(:options) { {:reuse => false, :display => 99} }
86
+ context "and display autopicking is not allowed" do
87
+ let(:options) { {:autopick => false} }
57
88
 
58
- it "should fail with an exception" do
59
- lambda { Headless.new(options) }.should raise_error(Headless::Exception)
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
- context "and autopicking is allowed" do
63
- let(:options) { {:reuse => false, :display => 99, :autopick => true} }
94
+ context "and display autopicking is allowed" do
95
+ let(:options) { {:autopick => true} }
64
96
 
65
- it "should pick the next available display number" do
66
- Headless.new(options).display.should == 100
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 'when Xvfb is started, but by another user' do
74
- before do
75
- Headless::CliUtil.stub(:read_pid).with('/tmp/.X99-lock') { raise Errno::EPERM }
76
- Headless::CliUtil.stub(:read_pid).with('/tmp/.X100-lock').and_return(nil)
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
- context "and display autopicking is not allowed" do
80
- let(:options) { {:autopick => false} }
81
-
82
- it "should fail with and exception" do
83
- lambda { Headless.new(options) }.should raise_error(Headless::Exception)
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
- context "and display autopicking is allowed" do
88
- let(:options) { {:autopick => true} }
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
- it "should pick the next display number" do
91
- Headless.new(options).display.should == 100
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
- context "lifecycle" do
98
- let(:headless) { Headless.new }
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
- describe "#stop" do
108
- it "switches back from the headless server" do
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
- describe "#destroy" do
118
- before do
119
- Headless::CliUtil.stub(:read_pid).and_return(4444)
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
- it "switches back from the headless server and terminates the headless session" do
123
- Process.should_receive(:kill).with('TERM', 4444)
154
+ context "#take_screenshot" do
155
+ let(:headless) { Headless.new }
124
156
 
125
- ENV['DISPLAY'].should == ":31337"
126
- headless.start
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
- context "#video" do
135
- let(:headless) { Headless.new }
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
- it "returns video recorder" do
138
- headless.video.should be_a_kind_of(Headless::VideoRecorder)
139
- end
164
+ expect { headless.take_screenshot('a.png') }.to raise_error(Headless::Exception)
165
+ end
140
166
 
141
- it "returns the same instance" do
142
- recorder = headless.video
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
- context "#take_screenshot" do
148
- let(:headless) { Headless.new }
170
+ expect { headless.take_screenshot('a.png', :using => :imagemagick) }.to raise_error(Headless::Exception)
171
+ end
149
172
 
150
- it "raises an error if imagemagick is not installed" do
151
- Headless::CliUtil.stub(:application_exists?).and_return(false)
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
- lambda { headless.take_screenshot }.should raise_error(Headless::Exception)
154
- end
176
+ expect { headless.take_screenshot('a.png', :using => :xwd) }.to raise_error(Headless::Exception)
177
+ end
155
178
 
156
- it "issues command to take screenshot" do
157
- headless = Headless.new
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
- Headless.any_instance.should_receive(:system)
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
- headless.take_screenshot("/tmp/image.png")
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.stub(:application_exists?).and_return(true)
169
- Headless::CliUtil.stub(:read_pid).and_return(nil)
170
- Headless::CliUtil.stub(:path_to).and_return("/usr/bin/Xvfb")
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
@@ -7,30 +7,39 @@ describe Headless::VideoRecorder do
7
7
 
8
8
  describe "instantiation" do
9
9
  before do
10
- Headless::CliUtil.stub(:application_exists?).and_return(false)
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
- lambda { Headless::VideoRecorder.new(99, "1024x768x32") }.should raise_error(Headless::Exception)
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.stub(:path_to).and_return('ffmpeg')
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.stub(:path_to).and_return('ffmpeg')
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.should_receive(:kill_process).with(pidfile, :wait => true)
50
- File.should_receive(:exists?).with(tmpfile).and_return(true)
51
- FileUtils.should_receive(:mv).with(tmpfile, filename)
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.should_receive(:kill_process).with(pidfile, :wait => true)
60
- FileUtils.should_receive(:rm).with(tmpfile)
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.stub(:application_exists?).and_return(true)
71
- Headless::CliUtil.stub(:fork_process).and_return(true)
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: 1.0.2
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: 2014-06-03 00:00:00.000000000 Z
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: '2.6'
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: '2.6'
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.2.2
96
+ rubygems_version: 2.4.5
82
97
  signing_key:
83
98
  specification_version: 4
84
99
  summary: Ruby headless display interface