headless-muse 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f3ef46e8f92d17bf336aae8bab33f7e138dd4619
4
+ data.tar.gz: 36029054c983b680a1996121ae0256a52c5f034a
5
+ SHA512:
6
+ metadata.gz: a4e524a612efbeab185f98efa58ba2c7490a391bc02bdee9a327b8760d62837ed76f575c4756c20aa3442978b17d0305bf0a261c8eb7e7391652c5e3b1006917
7
+ data.tar.gz: 0df9462f894b68182517fe89552b22fc5ce09f1143d44233efa46e9fd5dece0d559e049bb4311e752fb69757608871db0e0a922f1b4006cc087566d4b7b12254
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .bundle
2
+ Gemfile.lock
3
+ pkg/*
data/.travis.yml ADDED
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - 2.1.2
7
+ env:
8
+ - FFMPEG_VERSION=default
9
+ - FFMPEG_VERSION=2.3
10
+ before_install: ./.travis/setup.sh
11
+ script: "rspec"
12
+
data/.travis/setup.sh ADDED
@@ -0,0 +1,15 @@
1
+ #!/bin/bash
2
+ case "$FFMPEG_VERSION" in
3
+ 2.3)
4
+ sudo add-apt-repository ppa:archivematica/externals -y
5
+ sudo apt-get update -q
6
+ sudo apt-get install ffmpeg
7
+ ;;
8
+
9
+ 1.2)
10
+ stop
11
+ ;;
12
+ *)
13
+ sudo apt-get update -q
14
+ sudo apt-get install ffmpeg
15
+ esac
data/CHANGELOG ADDED
@@ -0,0 +1,33 @@
1
+ ## 1.0.2 (2014-06-03)
2
+
3
+ * pass options correctly to ffmpeg (from @abotalov)
4
+ * only destroy headless if it was created (from @evandrodp)
5
+
6
+ ## 1.0.1 (2013-02-20)
7
+
8
+ * when starting, wait for Xvfb to launch (fixed issue #33)
9
+
10
+ ## 1.0.0 (2013-01-28)
11
+
12
+ * bugfix release
13
+ * version number compliant to the [semantic versioning system](http://semver.org)
14
+
15
+ ## 0.3.1 (2012-03-29)
16
+
17
+ * added autopicking of display number, if the requested one is already taken
18
+ * fixed plenty of bugs thanks to @recursive, @gshakhn, @masatomo and @mabotelh
19
+
20
+ ## 0.2.2 (2011-09-01)
21
+
22
+ * improve detection of ffmpeg process (from https://github.com/alanshields/headless)
23
+
24
+ ## 0.2.1 (2011-08-26)
25
+
26
+ * added ability to capture screenshots (from https://github.com/iafonov/headless)
27
+ * added ability to capture video (from https://github.com/iafonov/headless)
28
+ * fixed issue with stray pidfile
29
+
30
+ ## 0.1.0 (2010-08-15)
31
+
32
+ * introduced options
33
+ * make it possible to change virtual screen dimensions and pixel depth
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in ci_util.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Leonid Shevtsov
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
20
+
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # Headless
2
+ [![Build Status](https://travis-ci.org/pgeraghty/headless.svg?branch=master)](https://travis-ci.org/pgeraghty/headless)
3
+ [![Coverage Status](https://img.shields.io/coveralls/pgeraghty/headless.svg)](https://coveralls.io/r/pgeraghty/headless?branch=master)
4
+
5
+ # This fork
6
+
7
+ This fork is the source of the [headless-muse gem]().
8
+
9
+ ## Notes by original author (Leonid Shevtsov)
10
+
11
+ Headless is *the* Ruby interface for Xvfb. It allows you to create a headless display straight from Ruby code, hiding some low-level action.
12
+ It can also capture images and video from the virtual framebuffer.
13
+
14
+ 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.
15
+ Other possible uses include pdf generation with `wkhtmltopdf`, or screenshotting.
16
+
17
+ Documentation is available at [rdoc.info](http://rdoc.info/projects/leonid-shevtsov/headless)
18
+
19
+ [Changelog](https://github.com/leonid-shevtsov/headless/blob/master/CHANGELOG)
20
+
21
+ **Note: Headless will NOT hide most applications on OS X. [Here is a detailed explanation](https://github.com/leonid-shevtsov/headless/issues/31#issuecomment-8933108)**
22
+
23
+ ## Installation
24
+
25
+ On Debian/Ubuntu:
26
+
27
+ ```sh
28
+ sudo apt-get install xvfb
29
+ gem install headless
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ Block mode:
35
+
36
+ ```ruby
37
+ require 'rubygems'
38
+ require 'headless'
39
+ require 'selenium-webdriver'
40
+
41
+ Headless.ly do
42
+ driver = Selenium::WebDriver.for :firefox
43
+ driver.navigate.to 'http://google.com'
44
+ puts driver.title
45
+ end
46
+ ```
47
+
48
+ Object mode:
49
+
50
+ ```ruby
51
+ require 'rubygems'
52
+ require 'headless'
53
+ require 'selenium-webdriver'
54
+
55
+ headless = Headless.new
56
+ headless.start
57
+
58
+ driver = Selenium::WebDriver.for :firefox
59
+ driver.navigate.to 'http://google.com'
60
+ puts driver.title
61
+
62
+ headless.destroy
63
+ ```
64
+
65
+ ## Cucumber
66
+
67
+ Running cucumber headless is now as simple as adding a before and after hook in `features/support/env.rb`:
68
+
69
+ ```ruby
70
+ # change the condition to fit your setup
71
+ if Capybara.current_driver == :selenium
72
+ require 'headless'
73
+
74
+ headless = Headless.new
75
+ headless.start
76
+ end
77
+ ```
78
+
79
+ ## Running tests in parallel
80
+
81
+ If you have multiple threads running acceptance tests in parallel, you want to spawn Headless before forking, and then reuse that instance with `destroy_at_exit: false`.
82
+ You can even spawn a Headless instance in one ruby script, and then reuse the same instance in other scripts by specifying the same display number and `reuse: true`.
83
+
84
+ ```ruby
85
+ # spawn_headless.rb
86
+ Headless.new(display: 100, destroy_at_exit: false).start
87
+
88
+ # test_suite_that_could_be_ran_multiple_times.rb
89
+ Headless.new(display: 100, reuse: true, destroy_at_exit: false).start
90
+
91
+ # reap_headless.rb
92
+ headless = Headless.new(display: 100, reuse: true)
93
+ headless.destroy
94
+ ```
95
+
96
+
97
+ ## Cucumber with wkhtmltopdf
98
+
99
+ _Note: this is true for other programs which may use headless at the same time as cucumber is running_
100
+
101
+ When wkhtmltopdf is using Headless, and cucumber is invoking a block of code which uses a headless session, make sure to override the default display of cucumber to retain browser focus. Assuming wkhtmltopdf is using the default display of 99, make sure to set the display to a value != 99 in `features/support/env.rb` file. This may be the cause of `Connection refused - connect(2) (Errno::ECONNREFUSED)`.
102
+
103
+ ```ruby
104
+ headless = Headless.new(:display => '100')
105
+ headless.start
106
+ ```
107
+
108
+ ## Capturing video
109
+
110
+ Video is captured using `ffmpeg`. You can install it on Debian/Ubuntu via `sudo apt-get install ffmpeg` or on OS X via `brew install ffmpeg`. You can capture video continuously or capture scenarios separately. Here is typical use case:
111
+
112
+ ```ruby
113
+ require 'headless'
114
+
115
+ headless = Headless.new
116
+ headless.start
117
+
118
+ Before do
119
+ headless.video.start_capture
120
+ end
121
+
122
+ After do |scenario|
123
+ if scenario.failed?
124
+ headless.video.stop_and_save("/tmp/#{BUILD_ID}/#{scenario.name.split.join("_")}.mov")
125
+ else
126
+ headless.video.stop_and_discard
127
+ end
128
+ end
129
+ ```
130
+
131
+ ## Taking screenshots
132
+
133
+ 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.
134
+
135
+ ## Contributors
136
+
137
+ * [Igor Afonov](http://iafonov.github.com) - video and screenshot capturing functionality.
138
+
139
+ ---
140
+
141
+ © 2011 Leonid Shevtsov, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+ Bundler::GemHelper.install_tasks
7
+
8
+ require 'rspec/core/rake_task'
9
+ RSpec::Core::RakeTask.new
10
+ task :default => :spec
data/headless.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ Gem::Specification.new do |s|
2
+ s.authors = ['Leonid Shevtsov', 'Igor Afonov', 'Paul Geraghty']
3
+ s.email = 'muse@appsthatcould.be'
4
+
5
+ s.name = 'headless-muse'
6
+ s.version = '1.1.0'
7
+ s.summary = 'Ruby headless display interface'
8
+
9
+ s.description = <<-EOF
10
+ Headless is a Ruby interface for Xvfb. It allows you to create a headless display straight from Ruby code, hiding some low-level action.
11
+ It can also capture video and audio via ffmpeg and take screenshots.
12
+ EOF
13
+ s.requirements = 'Xvfb'
14
+ s.homepage = 'https://github.com/pgeraghty/headless'
15
+ s.license = 'MIT'
16
+ s.files = `git ls-files`.split("\n")
17
+
18
+ s.add_development_dependency 'rake'
19
+ s.add_development_dependency 'rspec', '~> 2.6'
20
+ s.add_development_dependency('coveralls', '> 0') unless RUBY_VERSION == '1.8.7'
21
+ end
data/lib/headless.rb ADDED
@@ -0,0 +1,183 @@
1
+ require 'headless/cli_util'
2
+ require 'headless/video/video_recorder'
3
+
4
+ # A class incapsulating the creation and usage of a headless X server
5
+ #
6
+ # == Prerequisites
7
+ #
8
+ # * X Window System
9
+ # * Xvfb[http://en.wikipedia.org/wiki/Xvfb]
10
+ #
11
+ # == Usage
12
+ #
13
+ # Block mode:
14
+ #
15
+ # require 'rubygems'
16
+ # require 'headless'
17
+ # require 'selenium-webdriver'
18
+ #
19
+ # Headless.ly do
20
+ # driver = Selenium::WebDriver.for :firefox
21
+ # driver.navigate.to 'http://google.com'
22
+ # puts driver.title
23
+ # end
24
+ #
25
+ # Object mode:
26
+ #
27
+ # require 'rubygems'
28
+ # require 'headless'
29
+ # require 'selenium-webdriver'
30
+ #
31
+ # headless = Headless.new
32
+ # headless.start
33
+ #
34
+ # driver = Selenium::WebDriver.for :firefox
35
+ # driver.navigate.to 'http://google.com'
36
+ # puts driver.title
37
+ #
38
+ # headless.destroy
39
+ #--
40
+ # TODO test that reuse actually works with an existing xvfb session
41
+ #++
42
+ class Headless
43
+
44
+ DEFAULT_DISPLAY_NUMBER = 99
45
+ MAX_DISPLAY_NUMBER = 10_000
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
49
+
50
+ class Exception < RuntimeError
51
+ end
52
+
53
+ # The display number
54
+ attr_reader :display
55
+
56
+ # The display dimensions
57
+ attr_reader :dimensions
58
+
59
+ # Creates a new headless server, but does NOT switch to it immediately. Call #start for that
60
+ #
61
+ # List of available options:
62
+ # * +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?
67
+ def initialize(options = {})
68
+ CliUtil.ensure_application_exists!('Xvfb', 'Xvfb not found on your system')
69
+
70
+ @display = options.fetch(:display, DEFAULT_DISPLAY_NUMBER).to_i
71
+ @autopick_display = options.fetch(:autopick, !options.key?(:display))
72
+ @reuse_display = options.fetch(:reuse, true)
73
+ @dimensions = options.fetch(:dimensions, DEFAULT_DISPLAY_DIMENSIONS)
74
+ @video_capture_options = options.fetch(:video, {})
75
+ @destroy_at_exit = options.fetch(:destroy_at_exit, true)
76
+
77
+ # FIXME Xvfb launch should not happen inside the constructor
78
+ attach_xvfb
79
+ end
80
+
81
+ # Switches to the headless server
82
+ def start
83
+ @old_display = ENV['DISPLAY']
84
+ ENV['DISPLAY'] = ":#{display}"
85
+ hook_at_exit
86
+ end
87
+
88
+ # Switches back from the headless server
89
+ def stop
90
+ ENV['DISPLAY'] = @old_display
91
+ end
92
+
93
+ # Switches back from the headless server and terminates the headless session
94
+ def destroy
95
+ stop
96
+ CliUtil.kill_process(pid_filename)
97
+ end
98
+
99
+ # Block syntax:
100
+ #
101
+ # Headless.run do
102
+ # # perform operations in headless mode
103
+ # end
104
+ # See #new for options
105
+ def self.run(options={}, &block)
106
+ headless = Headless.new(options)
107
+ headless.start
108
+ yield headless
109
+ ensure
110
+ headless && headless.destroy
111
+ end
112
+ class <<self; alias_method :ly, :run; end
113
+
114
+ def video
115
+ @video_recorder ||= VideoRecorder.new(display, dimensions, @video_capture_options)
116
+ end
117
+
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}"
122
+ end
123
+
124
+ private
125
+
126
+ def attach_xvfb
127
+ possible_display_set = @autopick_display ? @display..MAX_DISPLAY_NUMBER : Array(@display)
128
+ pick_available_display(possible_display_set, @reuse_display)
129
+ end
130
+
131
+ def pick_available_display(display_set, can_reuse)
132
+ display_set.each do |display_number|
133
+ @display = display_number
134
+ begin
135
+ return true if xvfb_running? && can_reuse
136
+ return true if !xvfb_running? && launch_xvfb
137
+ rescue Errno::EPERM # display not accessible
138
+ next
139
+ end
140
+ end
141
+ raise Headless::Exception.new("Could not find an available display")
142
+ end
143
+
144
+ 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
149
+ return true
150
+ end
151
+
152
+ def ensure_xvfb_is_running
153
+ start_time = Time.now
154
+ begin
155
+ sleep 0.01 # to avoid cpu hogging
156
+ raise Headless::Exception.new("Xvfb is frozen") if (Time.now-start_time)>=XVFB_LAUNCH_TIMEOUT
157
+ end while !xvfb_running?
158
+ end
159
+
160
+ def xvfb_running?
161
+ !!read_xvfb_pid
162
+ end
163
+
164
+ def pid_filename
165
+ "/tmp/.X#{display}-lock"
166
+ end
167
+
168
+ def read_xvfb_pid
169
+ CliUtil.read_pid(pid_filename)
170
+ end
171
+
172
+ def hook_at_exit
173
+ unless @at_exit_hook_installed
174
+ @at_exit_hook_installed = true
175
+ at_exit do
176
+ exit_status = $!.status if $!.is_a?(SystemExit)
177
+ destroy if @destroy_at_exit
178
+ exit exit_status if exit_status
179
+ end
180
+ end
181
+ end
182
+ end
183
+
@@ -0,0 +1,64 @@
1
+ class Headless
2
+ class CliUtil
3
+ def self.application_exists?(app)
4
+ `which #{app}`.strip != ""
5
+ end
6
+
7
+ def self.ensure_application_exists!(app, error_message)
8
+ if !self.application_exists?(app)
9
+ raise Headless::Exception.new(error_message)
10
+ end
11
+ end
12
+
13
+ def self.path_to(app)
14
+ `which #{app}`.strip
15
+ end
16
+
17
+ def self.read_pid(pid_filename)
18
+ pid = (File.read(pid_filename) rescue "").strip.to_i
19
+ pid = nil if pid.zero?
20
+
21
+ if pid
22
+ begin
23
+ Process.kill(0, pid)
24
+ pid
25
+ rescue Errno::ESRCH
26
+ nil
27
+ end
28
+ else
29
+ nil
30
+ end
31
+ end
32
+
33
+ def self.fork_process(command, pid_filename, log_filename='/dev/null')
34
+ pid = fork do
35
+ STDERR.reopen(log_filename)
36
+ exec command
37
+ exit! 127 # safeguard in case exec fails
38
+ end
39
+
40
+ File.open pid_filename, 'w' do |f|
41
+ f.puts pid
42
+ end
43
+ end
44
+
45
+ def self.kill_process(pid_filename, options={})
46
+ if pid = self.read_pid(pid_filename)
47
+ begin
48
+ Process.kill 'TERM', pid
49
+ Process.wait pid if options[:wait]
50
+ rescue Errno::ESRCH
51
+ # no such process; assume it's already killed
52
+ rescue Errno::ECHILD
53
+ # Process.wait tried to wait on a dead process
54
+ end
55
+ end
56
+
57
+ begin
58
+ FileUtils.rm pid_filename
59
+ rescue Errno::ENOENT
60
+ # pid file already removed
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,82 @@
1
+ require 'tempfile'
2
+
3
+ class Headless
4
+ class VideoRecorder
5
+ attr_accessor :pid_file_path, :tmp_file_path, :log_file_path, :bin_file_path, :bin_version, :capture_with
6
+
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
+ @display = display
11
+ @dimensions = dimensions[/.+(?=x)/]
12
+
13
+ @bin_file_path = options.fetch(:bin_file_path, CliUtil.path_to('ffmpeg'))
14
+ # divine version - tested on:
15
+ # ffmpeg version 2.3.1
16
+ # ffmpeg version 0.10.9-7:0.10.9-1~quantal1
17
+ # ffmpeg 0.8.10-6:0.8.10-0ubuntu0.12.10.1
18
+ @bin_version = options.fetch(:bin_version, guess_ffmpeg_version!)
19
+ @pid_file_path = options.fetch(:pid_file_path, "/tmp/.headless_ffmpeg_#{@display}.pid")
20
+ @tmp_file_path = options.fetch(:tmp_file_path, "/tmp/.headless_ffmpeg_#{@display}.mov")
21
+ @log_file_path = options.fetch(:log_file_path, '/dev/null')
22
+ @codec = options.fetch(:codec, 'qtrle')
23
+ @frame_rate = options.fetch(:frame_rate, 30).to_i
24
+ @nomouse = options.fetch(:nomouse, false)
25
+ @audio = options.fetch(:audio, false)
26
+ end
27
+
28
+ def guess_ffmpeg_version!
29
+ (Gem::Version.new(`#{@bin_file_path} -version`[/(?:ffmpeg )(?:version )?((?:(\d+)\.)?(?:(\d+)\.)?(\*|\d+))/, 1])) unless @bin_file_path.empty? rescue Gem::Version.new('0')
30
+ end
31
+
32
+ def capture_running?
33
+ !!CliUtil.read_pid(@pid_file_path)
34
+ end
35
+
36
+ def capture_with
37
+ # TODO adjust switches based on @bin_version e.g. if @bin_version < Gem::Version.new('1')
38
+ [
39
+ @bin_file_path,
40
+ 'y', # ignore already-existing file
41
+ ('f alsa -ac 2 -i hw:0,1' if @audio),
42
+ "r #{@frame_rate}",
43
+ "s #{@dimensions}",
44
+ 'f x11grab',
45
+ ('draw_mouse 0' if @nomouse),
46
+ "i :#{@display}",
47
+ "vcodec #{@codec}",
48
+ ("g #{@frame_rate.to_i*20}" if @bin_version && @bin_version < Gem::Version.new('1'))
49
+ ].compact*' -'
50
+ end
51
+
52
+ def start_capture
53
+ CliUtil.fork_process("#{capture_with} #{@tmp_file_path}", @pid_file_path, @log_file_path)
54
+ at_exit do
55
+ exit_status = $!.status if $!.is_a?(SystemExit)
56
+ stop_and_discard
57
+ exit exit_status if exit_status
58
+ end
59
+ end
60
+
61
+ def stop_and_save(path)
62
+ CliUtil.kill_process(@pid_file_path, :wait => true)
63
+ if File.exists? @tmp_file_path
64
+ begin
65
+ FileUtils.mv(@tmp_file_path, path)
66
+ true
67
+ rescue Errno::EINVAL
68
+ false
69
+ end
70
+ end
71
+ end
72
+
73
+ def stop_and_discard
74
+ CliUtil.kill_process(@pid_file_path, :wait => true)
75
+ begin
76
+ FileUtils.rm(@tmp_file_path)
77
+ rescue Errno::ENOENT
78
+ # that's ok if the file doesn't exist
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ describe Headless::CliUtil do
4
+ before do
5
+ subject.class.stub(:path_to).and_return('ffmpeg')
6
+ end
7
+
8
+ describe 'application_exists?' do
9
+ before { subject.class.stub(:`).and_return('/usr/bin/ffmpeg') }
10
+
11
+ it 'calls which to find the process' do
12
+ subject.class.should_receive(:`).with('which ffmpeg').and_return('/usr/bin/ffmpeg')
13
+ subject.class.application_exists?('ffmpeg').should eq(true)
14
+ end
15
+ end
16
+
17
+ describe 'fork_process' do
18
+ before { subject.class.stub(:application_exists?).and_return(true) }
19
+
20
+ it 'forks' do
21
+ recorder = Headless::VideoRecorder.new(99, '1024x768x32')
22
+
23
+ subject.class.should_receive(:fork).and_yield do |block|
24
+ block.stub(:exec).and_return(123)
25
+ block.stub(:exit!)
26
+ STDERR.should_receive(:reopen).with('/dev/null')
27
+ block.should_receive(:exec).with("#{recorder.capture_with} #{recorder.tmp_file_path}")
28
+ end
29
+ recorder.start_capture
30
+ end
31
+
32
+ it 'creates PID file' do
33
+ recorder = Headless::VideoRecorder.new(99, '1024x768x32')
34
+ subject.class.stub(:fork).and_return(123)
35
+
36
+ file = double('file')
37
+ File.should_receive(:open).with('/tmp/.headless_ffmpeg_99.pid', 'w').and_yield(file)
38
+ file.should_receive(:puts).with(123)
39
+
40
+ recorder.start_capture
41
+ end
42
+ end
43
+
44
+ describe 'read_pid' do
45
+ before do
46
+ Process.stub(:kill)
47
+ File.stub(:read).and_return('999999999999')
48
+ end
49
+
50
+ it 'reads PID file' do
51
+ File.should_receive(:read).with('/tmp/.headless_ffmpeg_99.pid').and_return('')
52
+ subject.class.read_pid('/tmp/.headless_ffmpeg_99.pid').should eq(nil)
53
+ end
54
+
55
+ it 'sends signal to process' do
56
+ Process.should_receive(:kill).with(0, 999999999999)
57
+ subject.class.read_pid('/tmp/.headless_ffmpeg_99.pid').should eq(999999999999)
58
+ end
59
+
60
+ it 'returns nil after a rescued error when process does not exist' do
61
+ Process.stub(:kill).and_raise(Errno::ESRCH)
62
+ subject.class.read_pid('/tmp/.headless_ffmpeg_99.pid').should eq(nil)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,219 @@
1
+ require 'spec_helper'
2
+
3
+ describe Headless do
4
+ before do
5
+ ENV['DISPLAY'] = ":31337"
6
+ stub_environment
7
+ end
8
+
9
+ after do
10
+ `killall Xvfb`
11
+ # RSpec::Mocks.proxy_for(Headless::CliUtil).reset
12
+ # RSpec::Mocks.proxy_for(Headless).reset
13
+ #
14
+ # ObjectSpace.each_object(Headless) { |h| RSpec::Mocks.proxy_for(h).reset; h.destroy }
15
+ end
16
+
17
+ context "instantiation" do
18
+ context "when Xvfb is not installed" do
19
+ before do
20
+ Headless::CliUtil.stub(:application_exists?).and_return(false)
21
+ end
22
+
23
+ it "raises an error" do
24
+ lambda { Headless.new }.should raise_error(Headless::Exception)
25
+ end
26
+ end
27
+
28
+ context "when Xvfb is not started yet" do
29
+ it "starts Xvfb" do
30
+ Headless.any_instance.should_receive(:system).with("/usr/bin/Xvfb :99 -screen 0 1280x1024x24 -ac >/dev/null 2>&1 &").and_return(true)
31
+
32
+ headless = Headless.new
33
+ end
34
+
35
+ it "allows setting screen dimensions" do
36
+ Headless.any_instance.should_receive(:system).with("/usr/bin/Xvfb :99 -screen 0 1024x768x16 -ac >/dev/null 2>&1 &").and_return(true)
37
+
38
+ headless = Headless.new(:dimensions => "1024x768x16")
39
+ end
40
+ end
41
+
42
+ context "when Xvfb is already running" do
43
+ before do
44
+ Headless::CliUtil.stub(:read_pid).with('/tmp/.X99-lock').and_return(31337)
45
+ Headless::CliUtil.stub(:read_pid).with('/tmp/.X100-lock').and_return(nil)
46
+ end
47
+
48
+ context "and display reuse is allowed" do
49
+ let(:options) { {:reuse => true} }
50
+
51
+ it "should reuse the existing Xvfb" do
52
+ Headless.new(options).display.should == 99
53
+ end
54
+ end
55
+
56
+ context "and display reuse is not allowed" do
57
+ let(:options) { {:reuse => false} }
58
+
59
+ it "should pick the next available display number" do
60
+ Headless.new(options).display.should == 100
61
+ end
62
+
63
+ context "and display number is explicitly set" do
64
+ let(:options) { {:reuse => false, :display => 99} }
65
+
66
+ it "should fail with an exception" do
67
+ lambda { Headless.new(options) }.should raise_error(Headless::Exception)
68
+ end
69
+
70
+ context "and autopicking is allowed" do
71
+ let(:options) { {:reuse => false, :display => 99, :autopick => true} }
72
+
73
+ it "should pick the next available display number" do
74
+ Headless.new(options).display.should == 100
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ context 'when Xvfb is started, but by another user' do
82
+ before do
83
+ Headless::CliUtil.stub(:read_pid).with('/tmp/.X99-lock') { raise Errno::EPERM }
84
+ Headless::CliUtil.stub(:read_pid).with('/tmp/.X100-lock').and_return(nil)
85
+ end
86
+
87
+ context "and display autopicking is not allowed" do
88
+ let(:options) { {:autopick => false} }
89
+
90
+ it "should fail with and exception" do
91
+ lambda { Headless.new(options) }.should raise_error(Headless::Exception)
92
+ end
93
+ end
94
+
95
+ context "and display autopicking is allowed" do
96
+ let(:options) { {:autopick => true} }
97
+
98
+ it "should pick the next display number" do
99
+ Headless.new(options).display.should == 100
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ context "lifecycle" do
106
+ let(:headless) { Headless.new }
107
+ describe "#start" do
108
+ it "switches to the headless server" do
109
+ ENV['DISPLAY'].should == ":31337"
110
+ headless.start
111
+ ENV['DISPLAY'].should == ":99"
112
+ end
113
+ end
114
+
115
+ describe "#stop" do
116
+ it "switches back from the headless server" do
117
+ ENV['DISPLAY'].should == ":31337"
118
+ headless.start
119
+ ENV['DISPLAY'].should == ":99"
120
+ headless.stop
121
+ ENV['DISPLAY'].should == ":31337"
122
+ end
123
+ end
124
+
125
+ describe "#destroy" do
126
+ before do
127
+ Headless::CliUtil.stub(:read_pid).and_return(4444)
128
+ end
129
+
130
+ it "switches back from the headless server and terminates the headless session" do
131
+ Process.should_receive(:kill).with('TERM', 4444)
132
+
133
+ ENV['DISPLAY'].should == ":31337"
134
+ headless.start
135
+ ENV['DISPLAY'].should == ":99"
136
+ headless.destroy
137
+ ENV['DISPLAY'].should == ":31337"
138
+ end
139
+ end
140
+ end
141
+
142
+ context "#video" do
143
+ let(:headless) { Headless.new }
144
+
145
+ it "returns video recorder" do
146
+ headless.video.should be_a_kind_of(Headless::VideoRecorder)
147
+ end
148
+
149
+ it "returns the same instance" do
150
+ recorder = headless.video
151
+ headless.video.should be_eql(recorder)
152
+ end
153
+ end
154
+
155
+ context "#take_screenshot" do
156
+ let(:headless) { Headless.new }
157
+
158
+ it "raises an error if imagemagick is not installed" do
159
+ Headless::CliUtil.stub(:application_exists?).and_return(false)
160
+
161
+ expect { headless.take_screenshot }.to raise_error
162
+ end
163
+
164
+ it "issues command to take screenshot" do
165
+ headless = Headless.new
166
+
167
+ Headless.any_instance.should_receive(:system)
168
+
169
+ headless.take_screenshot("/tmp/image.png")
170
+ end
171
+ end
172
+
173
+ context '#ensure_xvfb_is_running' do
174
+ let(:headless) { Headless.new }
175
+ before { headless.stub(:ensure_xvfb_is_running).and_call_original }
176
+
177
+
178
+ it 'store the start times and compares' do
179
+ headless.stub(:xvfb_running?).and_return(true)
180
+ Time.should_receive(:now).twice.and_call_original
181
+ headless.ensure_xvfb_is_running
182
+ end
183
+
184
+ it 'pauses briefly' do
185
+ headless.stub(:xvfb_running?).and_return(true)
186
+ headless.should_receive(:sleep).with(0.01)
187
+ headless.ensure_xvfb_is_running
188
+ end
189
+
190
+ it 'times out' do
191
+ headless.stub(:xvfb_running?).and_return(false)
192
+ times = [Time.now + 20, Time.now]
193
+ Time.stub(:now) do
194
+ times.pop
195
+ end
196
+ expect { headless.ensure_xvfb_is_running }.to raise_error(Headless::Exception)
197
+ end
198
+ end
199
+
200
+ context 'run' do
201
+ it 'instantiates, starts, yields and is destroyed' do
202
+ Headless.any_instance.should_receive(:start)
203
+ Headless.any_instance.should_receive(:destroy)
204
+ expect { |b| Headless.run({}, &b) }.to yield_with_args(Headless)
205
+ end
206
+ end
207
+
208
+ private
209
+ def stub_environment
210
+ Headless::CliUtil.stub(:application_exists?).and_return(true)
211
+ Headless::CliUtil.stub(:read_pid).and_return(nil)
212
+ Headless::CliUtil.stub(:path_to)
213
+ Headless::CliUtil.stub(:path_to).with('Xvfb').and_return('/usr/bin/Xvfb')
214
+ Headless::CliUtil.stub(:path_to).with('ffmpeg').and_return('/usr/bin/ffmpeg')
215
+
216
+ # 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
217
+ Headless.any_instance.stub(:ensure_xvfb_is_running).and_return(true)
218
+ end
219
+ end
@@ -0,0 +1,7 @@
1
+ # Coveralls requires Ruby 1.9.2
2
+ unless RUBY_VERSION == '1.8.7'
3
+ require 'coveralls'
4
+ Coveralls.wear!
5
+ end
6
+
7
+ require 'headless'
@@ -0,0 +1,93 @@
1
+ require 'spec_helper'
2
+
3
+ describe Headless::VideoRecorder do
4
+ before do
5
+ stub_environment
6
+ end
7
+
8
+ describe "instantiation" do
9
+ before do
10
+ Headless::CliUtil.stub(:application_exists?).and_return(false)
11
+ end
12
+
13
+ it "throws an error if ffmpeg is not installed" do
14
+ lambda { Headless::VideoRecorder.new(99, "1024x768x32") }.should raise_error(Headless::Exception)
15
+ end
16
+ end
17
+
18
+ describe "#capture" do
19
+ it "starts ffmpeg" do
20
+ Headless::CliUtil.stub(:path_to).and_return('ffmpeg')
21
+ recorder = Headless::VideoRecorder.new(99, "1024x768x32")
22
+
23
+ Headless::CliUtil.should_receive(:fork_process).with(/#{recorder.capture_with}/, '/tmp/.headless_ffmpeg_99.pid', '/dev/null')
24
+ recorder.start_capture
25
+ end
26
+
27
+ it "starts ffmpeg with specified codec" do
28
+ Headless::CliUtil.stub(:path_to).and_return('ffmpeg')
29
+ recorder = Headless::VideoRecorder.new(99, "1024x768x32", {:codec => 'libvpx'})
30
+ Headless::CliUtil.should_receive(:fork_process).with(/#{recorder.capture_with}/, '/tmp/.headless_ffmpeg_99.pid', '/dev/null')
31
+ recorder.start_capture
32
+ end
33
+ end
34
+
35
+ context "stopping video recording" do
36
+ let(:tmpfile) { '/tmp/ci.mov' }
37
+ let(:filename) { '/tmp/test.mov' }
38
+ let(:pidfile) { '/tmp/pid' }
39
+
40
+ subject do
41
+ recorder = Headless::VideoRecorder.new(99, "1024x768x32", :pid_file_path => pidfile, :tmp_file_path => tmpfile)
42
+ recorder.start_capture
43
+ recorder
44
+ end
45
+
46
+ describe "using #stop_and_save" do
47
+ it "stops video recording and saves file" do
48
+ Headless::CliUtil.should_receive(:kill_process).with(pidfile, :wait => true)
49
+ File.should_receive(:exists?).with(tmpfile).and_return(true)
50
+ FileUtils.should_receive(:mv).with(tmpfile, filename)
51
+
52
+ expect(subject.stop_and_save(filename)).to eq(true)
53
+ end
54
+
55
+ it 'returns false after a rescued error when attempting to move file' do
56
+ FileUtils.stub(:mv).and_raise(Errno::EINVAL)
57
+ File.should_receive(:exists?).and_return(true)
58
+
59
+ expect(subject.stop_and_save(tmpfile)).to eq(false)
60
+ end
61
+
62
+ it 'returns nil when target file does not exist' do
63
+ File.stub(:exists?).and_return(false)
64
+
65
+ expect(subject.stop_and_save(tmpfile)).to eq(nil)
66
+ end
67
+ end
68
+
69
+ describe "using #stop_and_discard" do
70
+ it "stops video recording and deletes temporary file" do
71
+ Headless::CliUtil.should_receive(:kill_process).with(pidfile, :wait => true)
72
+ FileUtils.should_receive(:rm).with(tmpfile)
73
+
74
+ subject.stop_and_discard
75
+ end
76
+ end
77
+
78
+ describe '#capture_running?' do
79
+ it 'returns false unless the PID file exists' do
80
+ Headless::CliUtil.should_receive(:read_pid).with(pidfile)
81
+
82
+ expect(subject.capture_running?).to eq(false)
83
+ end
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def stub_environment
90
+ Headless::CliUtil.stub(:application_exists?).and_return(true)
91
+ Headless::CliUtil.stub(:fork_process).and_return(true)
92
+ end
93
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: headless-muse
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Leonid Shevtsov
8
+ - Igor Afonov
9
+ - Paul Geraghty
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2014-08-17 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rake
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '0'
29
+ - !ruby/object:Gem::Dependency
30
+ name: rspec
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - "~>"
34
+ - !ruby/object:Gem::Version
35
+ version: '2.6'
36
+ type: :development
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: '2.6'
43
+ - !ruby/object:Gem::Dependency
44
+ name: coveralls
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">"
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ type: :development
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">"
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ description: |2
58
+ Headless is a Ruby interface for Xvfb. It allows you to create a headless display straight from Ruby code, hiding some low-level action.
59
+ It can also capture video and audio via ffmpeg and take screenshots.
60
+ email: muse@appsthatcould.be
61
+ executables: []
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - ".gitignore"
66
+ - ".travis.yml"
67
+ - ".travis/setup.sh"
68
+ - CHANGELOG
69
+ - Gemfile
70
+ - LICENSE
71
+ - README.md
72
+ - Rakefile
73
+ - headless.gemspec
74
+ - lib/headless.rb
75
+ - lib/headless/cli_util.rb
76
+ - lib/headless/video/video_recorder.rb
77
+ - spec/cli_util_spec.rb
78
+ - spec/headless_spec.rb
79
+ - spec/spec_helper.rb
80
+ - spec/video_recorder_spec.rb
81
+ homepage: https://github.com/pgeraghty/headless
82
+ licenses:
83
+ - MIT
84
+ metadata: {}
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements:
100
+ - Xvfb
101
+ rubyforge_project:
102
+ rubygems_version: 2.2.2
103
+ signing_key:
104
+ specification_version: 4
105
+ summary: Ruby headless display interface
106
+ test_files: []