diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..353d670 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,38 @@ +name: Test + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + ruby: + - "3.4" + - "3.3" + - "3.2" + - "jruby-10.0.0.1" + - "jruby-9.4.12.1" + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y ffmpeg imagemagick + - name: Install geckodriver + env: + GECKODRIVER_VERSION: v0.36.0 + run: | + wget https://github.com/mozilla/geckodriver/releases/download/${GECKODRIVER_VERSION}/geckodriver-${GECKODRIVER_VERSION}-linux64.tar.gz + mkdir -p geckodriver + tar -xzf geckodriver-${GECKODRIVER_VERSION}-linux64.tar.gz -C geckodriver + echo "$GITHUB_WORKSPACE/geckodriver" >> $GITHUB_PATH + - name: Run tests + run: bundle exec rspec diff --git a/.gitignore b/.gitignore index d904a66..b4335de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ .bundle -Gemfile.lock pkg/* diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b19d32d..0000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -sudo: required -dist: trusty -language: ruby -cache: bundler -matrix: - include: - - rvm: 2.2.8 - - rvm: 2.4.2 - # see https://github.com/travis-ci/travis-ci/issues/6471 - - rvm: jruby-9.1.14.0 - env: JRUBY_OPTS="" -before_install: - - "sudo apt-get update" - - "sudo apt-get install -y libav-tools" - # Install geckodriver to drive Firefox - - wget https://github.com/mozilla/geckodriver/releases/download/v0.19.1/geckodriver-v0.19.1-linux64.tar.gz - - mkdir geckodriver - - tar -xzf geckodriver-v0.19.1-linux64.tar.gz -C geckodriver - - export PATH=$PATH:$PWD/geckodriver -script: "bundle exec rspec" diff --git a/Gemfile b/Gemfile index 17558cd..bf41e21 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,4 @@ source "http://rubygems.org" -# Specify your gem's dependencies in ci_util.gemspec +# Specify dependencies in headless.gemspec gemspec diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..c2ddc00 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,96 @@ +PATH + remote: . + specs: + headless (2.3.1) + +GEM + remote: http://rubygems.org/ + specs: + ast (2.4.3) + base64 (0.2.0) + diff-lcs (1.6.1) + json (2.11.3) + json (2.11.3-java) + language_server-protocol (3.17.0.4) + lint_roller (1.1.0) + logger (1.7.0) + parallel (1.27.0) + parser (3.3.8.0) + ast (~> 2.4.1) + racc + prism (1.4.0) + racc (1.8.1) + racc (1.8.1-java) + rainbow (3.1.1) + rake (13.2.1) + regexp_parser (2.10.0) + rexml (3.4.1) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.3) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.4) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.4) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.3) + rubocop (1.75.5) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.44.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.44.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-performance (1.25.0) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + ruby-progressbar (1.13.0) + rubyzip (2.4.1) + selenium-webdriver (4.32.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) + standard (1.49.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.75.2) + standard-custom (~> 1.0.0) + standard-performance (~> 1.8) + standard-custom (1.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.50) + standard-performance (1.8.0) + lint_roller (~> 1.1) + rubocop-performance (~> 1.25.0) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) + websocket (1.2.11) + +PLATFORMS + java + ruby + +DEPENDENCIES + headless! + rake + rspec (>= 3.7) + selenium-webdriver (>= 4.32) + standard + +BUNDLED WITH + 2.6.8 diff --git a/README.md b/README.md index bbc1338..64158e2 100644 --- a/README.md +++ b/README.md @@ -139,10 +139,9 @@ Available options: * :codec - codec to be used by ffmpeg * :frame_rate - frame rate of video capture -* :provider - ffmpeg provider - either :libav (default) or :ffmpeg -* :provider_binary_path - Explicit path to avconv or ffmpeg. Only required when the binary cannot be discovered on the system $PATH. -* :pid*file_path - path to ffmpeg pid file, default: "/tmp/.headless_ffmpeg*#{@display}.pid" -* :tmp*file_path - path to tmp video file, default: "/tmp/.headless_ffmpeg*#{@display}.mov" +* :ffmpeg_path - Explicit path to ffmpeg. Only required when the binary cannot be discovered on the system $PATH. +* :pid_file_path - path to ffmpeg pid file, default: "/tmp/.headless_ffmpeg_#{@display}.pid" +* :tmp_file_path - path to tmp video file, default: "/tmp/.headless_ffmpeg_#{@display}.mov" * :log_file_path - ffmpeg log file, default: "/dev/null" * :extra - array of extra ffmpeg options, default: [] diff --git a/headless.gemspec b/headless.gemspec index 72c5ca9..11aad04 100644 --- a/headless.gemspec +++ b/headless.gemspec @@ -1,21 +1,22 @@ Gem::Specification.new do |s| - s.author = 'Leonid Shevtsov' - s.email = 'leonid@shevtsov.me' + s.author = "Leonid Shevtsov" + s.email = "leonid@shevtsov.me" - s.name = 'headless' - s.version = '2.3.1' - s.summary = 'Ruby headless display interface' - s.license = 'MIT' + s.name = "headless" + s.version = "2.3.1" + s.summary = "Ruby headless display interface" + s.license = "MIT" s.description = <<-EOF Headless is a Ruby interface for Xvfb. It allows you to create a headless display straight from Ruby code, hiding some low-level action. EOF - s.requirements = 'Xvfb' - s.homepage = 'https://github.com/leonid-shevtsov/headless' + s.requirements = "Xvfb" + s.homepage = "https://github.com/leonid-shevtsov/headless" - s.files = `git ls-files`.split("\n") + s.files = `git ls-files`.split("\n") - s.add_development_dependency 'rake' - s.add_development_dependency 'rspec', '>= 3.7' - s.add_development_dependency 'selenium-webdriver', '>=3.7' + s.add_development_dependency "rake" + s.add_development_dependency "rspec", ">= 3.7" + s.add_development_dependency "selenium-webdriver", ">=4.32" + s.add_development_dependency "standard" end diff --git a/lib/headless.rb b/lib/headless.rb index 1172012..728d45d 100644 --- a/lib/headless.rb +++ b/lib/headless.rb @@ -1,5 +1,5 @@ -require 'headless/cli_util' -require 'headless/video/video_recorder' +require "headless/cli_util" +require "headless/video/video_recorder" # A class incapsulating the creation and usage of a headless X server # @@ -40,10 +40,9 @@ # TODO test that reuse actually works with an existing xvfb session #++ class Headless - DEFAULT_DISPLAY_NUMBER = 99 MAX_DISPLAY_NUMBER = 10_000 - DEFAULT_DISPLAY_DIMENSIONS = '1280x1024x24' + DEFAULT_DISPLAY_DIMENSIONS = "1280x1024x24" DEFAULT_XVFB_LAUNCH_TIMEOUT = 10 class Exception < RuntimeError @@ -73,9 +72,10 @@ class Exception < RuntimeError # (default true unless reuse is true and a server is already running) # * +xvfb_launch_timeout+ - how long should we wait for Xvfb to open a # display, before assuming that it is frozen (in seconds, default is 10) - # * +video+ - options to be passed to the ffmpeg video recorder. See Headless::VideoRecorder#initialize for documentation + # * +video+ - options to be passed to the ffmpeg video recorder. + # See Headless::VideoRecorder#initialize for documentation def initialize(options = {}) - CliUtil.ensure_application_exists!('Xvfb', 'Xvfb not found on your system') + CliUtil.ensure_application_exists!("Xvfb", "Xvfb not found on your system") @display = options.fetch(:display, DEFAULT_DISPLAY_NUMBER).to_i @xvfb_launch_timeout = options.fetch(:xvfb_launch_timeout, DEFAULT_XVFB_LAUNCH_TIMEOUT).to_i @@ -86,7 +86,11 @@ def initialize(options = {}) @extensions = options.fetch(:extensions, []) @extensions = [@extensions] unless @extensions.is_a? Array - already_running = xvfb_running? rescue false + already_running = begin + xvfb_running? + rescue + false + end @destroy_at_exit = options.fetch(:destroy_at_exit, !(@reuse_display && already_running)) @pid = nil # the pid of the running Xvfb process @@ -97,14 +101,14 @@ def initialize(options = {}) # Switches to the headless server def start - @old_display = ENV['DISPLAY'] - ENV['DISPLAY'] = ":#{display}" + @old_display = ENV["DISPLAY"] + ENV["DISPLAY"] = ":#{display}" hook_at_exit end # Switches back from the headless server def stop - ENV['DISPLAY'] = @old_display + ENV["DISPLAY"] = @old_display end # Switches back from the headless server and terminates the headless session @@ -139,37 +143,42 @@ def destroy_at_exit? # # perform operations in headless mode # end # See #new for options - def self.run(options={}, &block) + def self.run(options = {}, &block) headless = Headless.new(options) headless.start yield headless ensure - headless && headless.destroy + headless&.destroy end - class <=@xvfb_launch_timeout - # Continue looping until Xvfb has written its pidfile: - end while !xvfb_running? + if (Time.now - start_time) >= @xvfb_launch_timeout + raise Headless::Exception.new("Xvfb launched but did not complete initialization") + end + # Continue looping until Xvfb has written its pidfile: + break if xvfb_running? + end # If for any reason the pid file doesn't match ours, we lost the race to # get the file lock: - return @pid == read_xvfb_pid + @pid == read_xvfb_pid end def xvfb_mine? @@ -257,6 +272,6 @@ def hook_at_exit end def extensions - @extensions.map { |ext| '+' + ext.to_s } + @extensions.map { |ext| "+" + ext.to_s } end end diff --git a/lib/headless/cli_util.rb b/lib/headless/cli_util.rb index 9dc37ab..2c33437 100644 --- a/lib/headless/cli_util.rb +++ b/lib/headless/cli_util.rb @@ -5,21 +5,21 @@ def self.application_exists?(app) end def self.ensure_application_exists!(app, error_message) - if !self.application_exists?(app) + if !application_exists?(app) raise Headless::Exception.new(error_message) end end # Credit: http://stackoverflow.com/a/5471032/6678 def self.path_to(app) - exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] - ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| + exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""] + ENV["PATH"].split(File::PATH_SEPARATOR).each do |path| exts.each { |ext| exe = File.join(path, "#{app}#{ext}") return exe if File.executable?(exe) && !File.directory?(exe) } end - return nil + nil end def self.process_mine?(pid) @@ -35,21 +35,26 @@ def self.process_running?(pid) end def self.read_pid(pid_filename) - pid = (File.read(pid_filename) rescue "").strip + pid = begin + File.read(pid_filename) + rescue + "" + end.strip pid.empty? ? nil : pid.to_i end - def self.fork_process(command, pid_filename, log_filename='/dev/null') + def self.fork_process(command, pid_filename, log_filename = File::NULL) pid = Process.spawn(command, err: log_filename) - File.open pid_filename, 'w' do |f| + File.open pid_filename, "w" do |f| f.puts pid end end - def self.kill_process(pid_filename, options={}) - if pid = read_pid(pid_filename) + def self.kill_process(pid_filename, options = {}) + pid = read_pid(pid_filename) + if pid begin - Process.kill 'TERM', pid + Process.kill "TERM", pid Process.wait pid if options[:wait] rescue Errno::ESRCH # no such process; assume it's already killed diff --git a/lib/headless/video/video_recorder.rb b/lib/headless/video/video_recorder.rb index 2e6be76..ff6e469 100644 --- a/lib/headless/video/video_recorder.rb +++ b/lib/headless/video/video_recorder.rb @@ -1,16 +1,15 @@ -require 'tempfile' +require "tempfile" class Headless class VideoRecorder - attr_accessor :pid_file_path, :tmp_file_path, :log_file_path, :provider_binary_path + attr_accessor :pid_file_path, :tmp_file_path, :log_file_path, :ffmpeg_path # Construct a new Video Recorder instance. Typically done from inside Headless, but can be also created manually, # and even used separately from Headless' Xvfb features. # * display - display number to capture # * dimensions - dimensions of the captured video # * options - available options: - # * provider - either :ffmpeg or :libav; default is :libav - switch if your system is provisioned with FFMpeg - # * provider_binary_path - override path to ffmpeg / libav binary + # * ffmpeg_path - override path to ffmpeg binary # * pid_file_path - override path to PID file, default is placed in /tmp # * tmp_file_path - override path to temp file, default is placed in /tmp # * log_file_path - set log file path, default is /dev/null @@ -24,19 +23,19 @@ def initialize(display, dimensions, options = {}) @pid_file_path = options.fetch(:pid_file_path, "/tmp/.headless_ffmpeg_#{@display}.pid") @tmp_file_path = options.fetch(:tmp_file_path, "/tmp/.headless_ffmpeg_#{@display}.mov") - @log_file_path = options.fetch(:log_file_path, "/dev/null") + @log_file_path = options.fetch(:log_file_path, File::NULL) @codec = options.fetch(:codec, "qtrle") @frame_rate = options.fetch(:frame_rate, 30) - @provider = options.fetch(:provider, :libav) # or :ffmpeg - # If no provider_binary_path was specified, then - # make a guess based upon the provider. - @provider_binary_path = options.fetch(:provider_binary_path, guess_the_provider_binary_path) + # If no ffmpeg_path was specified, use the default + @ffmpeg_path = options.fetch(:ffmpeg_path, options.fetch(:provider_binary_path, "ffmpeg")) @extra = Array(options.fetch(:extra, [])) @devices = Array(options.fetch(:devices, [])) - CliUtil.ensure_application_exists!(provider_binary_path, "#{provider_binary_path} not found on your system. Install it or change video recorder provider") + CliUtil.ensure_application_exists!(ffmpeg_path, + "#{ffmpeg_path} not found on your system. " \ + "Install it or change video recorder provider") end def capture_running? @@ -45,7 +44,7 @@ def capture_running? def start_capture CliUtil.fork_process(command_line_for_capture, - @pid_file_path, @log_file_path) + @pid_file_path, @log_file_path) at_exit do exit_status = $!.status if $!.is_a?(SystemExit) stop_and_discard @@ -54,8 +53,8 @@ def start_capture end def stop_and_save(path) - CliUtil.kill_process(@pid_file_path, :wait => true) - if File.exists? @tmp_file_path + CliUtil.kill_process(@pid_file_path, wait: true) + if File.exist? @tmp_file_path begin FileUtils.mkdir_p(File.dirname(path)) FileUtils.mv(@tmp_file_path, path) @@ -66,7 +65,7 @@ def stop_and_save(path) end def stop_and_discard - CliUtil.kill_process(@pid_file_path, :wait => true) + CliUtil.kill_process(@pid_file_path, wait: true) begin FileUtils.rm(@tmp_file_path) rescue Errno::ENOENT @@ -76,32 +75,21 @@ def stop_and_discard private - def guess_the_provider_binary_path - @provider== :libav ? 'avconv' : 'ffmpeg' - end - def command_line_for_capture - if @provider == :libav - group_of_pic_size_option = '-g 600' - dimensions = @dimensions - else - group_of_pic_size_option = nil - dimensions = @dimensions.match(/^(\d+x\d+)/)[0] - end + dimensions = @dimensions.match(/^(\d+x\d+)/)[0] [ - CliUtil.path_to(provider_binary_path), - "-y", - "-r #{@frame_rate}", - "-s #{dimensions}", - "-f x11grab", - "-i :#{@display}", - @devices, - group_of_pic_size_option, - "-vcodec #{@codec}", - @extra, - @tmp_file_path - ].flatten.compact.join(' ') + CliUtil.path_to(ffmpeg_path), + "-y", + "-r #{@frame_rate}", + "-s #{dimensions}", + "-f x11grab", + "-i :#{@display}", + @devices, + "-vcodec #{@codec}", + @extra, + @tmp_file_path + ].flatten.compact.join(" ") end end end diff --git a/spec/headless_spec.rb b/spec/headless_spec.rb index 43061a5..f476a67 100644 --- a/spec/headless_spec.rb +++ b/spec/headless_spec.rb @@ -1,39 +1,44 @@ -require 'headless' +require "headless" describe Headless do before do - ENV['DISPLAY'] = ":31337" + ENV["DISPLAY"] = ":31337" stub_environment end - describe 'launch options' do + describe "launch options" do before do allow_any_instance_of(Headless).to receive(:ensure_xvfb_launched).and_return(true) end it "starts Xvfb" do - expect(Process).to receive(:spawn).with(*(%w(/usr/bin/Xvfb :99 -screen 0 1280x1024x24 -ac)+[hash_including(:err)])).and_return(123) - headless = Headless.new + expect(Process).to receive(:spawn).with(*(%w[/usr/bin/Xvfb :99 -screen 0 1280x1024x24 + -ac] + [hash_including(:err)])).and_return(123) + Headless.new end it "allows setting screen dimensions" do - expect(Process).to receive(:spawn).with(*(%w(/usr/bin/Xvfb :99 -screen 0 1024x768x16 -ac)+[hash_including(:err)])).and_return(123) - headless = Headless.new(:dimensions => "1024x768x16") + expect(Process).to receive(:spawn).with(*(%w[/usr/bin/Xvfb :99 -screen 0 1024x768x16 + -ac] + [hash_including(:err)])).and_return(123) + Headless.new(dimensions: "1024x768x16") end it "allows to enable extensions", focus: true do - expect(Process).to receive(:spawn).with(*(%w(/usr/bin/Xvfb :99 -screen 0 1280x1024x24 -ac +iglx)+[hash_including(:err)])).and_return(123) - headless = Headless.new(:extensions => [:iglx]) + expect(Process).to receive(:spawn).with(*(%w[/usr/bin/Xvfb :99 -screen 0 1280x1024x24 -ac + +iglx] + [hash_including(:err)])).and_return(123) + Headless.new(extensions: [:iglx]) - expect(Process).to receive(:spawn).with(*(%w(/usr/bin/Xvfb :99 -screen 0 1280x1024x24 -ac +iglx)+[hash_including(:err)])).and_return(123) - headless = Headless.new(:extensions => 'iglx') + expect(Process).to receive(:spawn).with(*(%w[/usr/bin/Xvfb :99 -screen 0 1280x1024x24 -ac + +iglx] + [hash_including(:err)])).and_return(123) + Headless.new(extensions: "iglx") - expect(Process).to receive(:spawn).with(*(%w(/usr/bin/Xvfb :99 -screen 0 1280x1024x24 -ac +iglx +dummy)+[hash_including(:err)])).and_return(123) - headless = Headless.new(:extensions => ['iglx', :dummy]) + expect(Process).to receive(:spawn).with(*(%w[/usr/bin/Xvfb :99 -screen 0 1280x1024x24 -ac +iglx + +dummy] + [hash_including(:err)])).and_return(123) + Headless.new(extensions: ["iglx", :dummy]) end end - context 'with stubbed launch_xvfb' do + context "with stubbed launch_xvfb" do before do allow_any_instance_of(Headless).to receive(:launch_xvfb).and_return(true) end @@ -51,15 +56,15 @@ context "when Xvfb is already running and was started by this user" do before do - allow(Headless::CliUtil).to receive(:read_pid).with('/tmp/.X99-lock').and_return(31337) + allow(Headless::CliUtil).to receive(:read_pid).with("/tmp/.X99-lock").and_return(31337) allow(Headless::CliUtil).to receive(:process_running?).with(31337).and_return(true) allow(Headless::CliUtil).to receive(:process_mine?).with(31337).and_return(true) - allow(Headless::CliUtil).to receive(:read_pid).with('/tmp/.X100-lock').and_return(nil) + allow(Headless::CliUtil).to receive(:read_pid).with("/tmp/.X100-lock").and_return(nil) end context "and display reuse is allowed" do - let(:options) { {:reuse => true} } + let(:options) { {reuse: true} } it "should reuse the existing Xvfb" do expect(Headless.new(options).display).to eq 99 @@ -71,21 +76,21 @@ end context "and display reuse is not allowed" do - let(:options) { {:reuse => false} } + let(:options) { {reuse: false} } it "should pick the next available display number" do expect(Headless.new(options).display).to eq 100 end context "and display number is explicitly set" do - let(:options) { {:reuse => false, :display => 99} } + let(:options) { {reuse: false, display: 99} } it "should fail with an exception" do expect { Headless.new(options) }.to raise_error(Headless::Exception) end context "and autopicking is allowed" do - let(:options) { {:reuse => false, :display => 99, :autopick => true} } + let(:options) { {reuse: false, display: 99, autopick: true} } it "should pick the next available display number" do expect(Headless.new(options).display).to eq 100 @@ -95,17 +100,17 @@ end end - context 'when Xvfb is started, but by another user' do + context "when Xvfb is started, but by another user" do before do - allow(Headless::CliUtil).to receive(:read_pid).with('/tmp/.X99-lock').and_return(31337) + allow(Headless::CliUtil).to receive(:read_pid).with("/tmp/.X99-lock").and_return(31337) allow(Headless::CliUtil).to receive(:process_running?).with(31337).and_return(true) allow(Headless::CliUtil).to receive(:process_mine?).with(31337).and_return(false) - allow(Headless::CliUtil).to receive(:read_pid).with('/tmp/.X100-lock').and_return(nil) + allow(Headless::CliUtil).to receive(:read_pid).with("/tmp/.X100-lock").and_return(nil) end context "and display autopicking is not allowed" do - let(:options) { {:autopick => false} } + let(:options) { {autopick: false} } it "should reuse the display" do expect(Headless.new(options).display).to eq 99 @@ -113,7 +118,7 @@ end context "and display autopicking is allowed" do - let(:options) { {:autopick => true} } + let(:options) { {autopick: true} } it "should pick the next display number" do expect(Headless.new(options).display).to eq 100 @@ -126,19 +131,19 @@ let(:headless) { Headless.new } describe "#start" do it "switches to the headless server" do - expect(ENV['DISPLAY']).to eq ":31337" + expect(ENV["DISPLAY"]).to eq ":31337" headless.start - expect(ENV['DISPLAY']).to eq ":99" + expect(ENV["DISPLAY"]).to eq ":99" end end describe "#stop" do it "switches back from the headless server" do - expect(ENV['DISPLAY']).to eq ":31337" + expect(ENV["DISPLAY"]).to eq ":31337" headless.start - expect(ENV['DISPLAY']).to eq ":99" + expect(ENV["DISPLAY"]).to eq ":99" headless.stop - expect(ENV['DISPLAY']).to eq ":31337" + expect(ENV["DISPLAY"]).to eq ":31337" end end @@ -148,13 +153,13 @@ end it "switches back from the headless server and terminates the headless session" do - expect(Process).to receive(:kill).with('TERM', 4444) + expect(Process).to receive(:kill).with("TERM", 4444) - expect(ENV['DISPLAY']).to eq ":31337" + expect(ENV["DISPLAY"]).to eq ":31337" headless.start - expect(ENV['DISPLAY']).to eq ":99" + expect(ENV["DISPLAY"]).to eq ":99" headless.destroy - expect(ENV['DISPLAY']).to eq ":31337" + expect(ENV["DISPLAY"]).to eq ":31337" end end end @@ -176,72 +181,72 @@ let(:headless) { Headless.new } it "raises an error if unknown value for option :using is used" do - expect { headless.take_screenshot('a.png', :using => :teleportation) }.to raise_error(Headless::Exception) + expect { headless.take_screenshot("a.png", using: :teleportation) }.to raise_error(Headless::Exception) end it "raises an error if imagemagick is not installed, with default options" do - allow(Headless::CliUtil).to receive(:application_exists?).with('import').and_return(false) + allow(Headless::CliUtil).to receive(:application_exists?).with("import").and_return(false) - expect { headless.take_screenshot('a.png') }.to raise_error(Headless::Exception) + expect { headless.take_screenshot("a.png") }.to raise_error(Headless::Exception) end it "raises an error if imagemagick is not installed, with using: :imagemagick" do - allow(Headless::CliUtil).to receive(:application_exists?).with('import').and_return(false) + allow(Headless::CliUtil).to receive(:application_exists?).with("import").and_return(false) - expect { headless.take_screenshot('a.png', :using => :imagemagick) }.to raise_error(Headless::Exception) + expect { headless.take_screenshot("a.png", using: :imagemagick) }.to raise_error(Headless::Exception) end it "raises an error if xwd is not installed, with using: :xwd" do - allow(Headless::CliUtil).to receive(:application_exists?).with('xwd').and_return(false) + allow(Headless::CliUtil).to receive(:application_exists?).with("xwd").and_return(false) - expect { headless.take_screenshot('a.png', :using => :xwd) }.to raise_error(Headless::Exception) + expect { headless.take_screenshot("a.png", using: :xwd) }.to raise_error(Headless::Exception) end it "raises an error if gm is not installed with using: :graphicsmagick" do - allow(Headless::CliUtil).to receive(:application_exists?).with('gm').and_return(false) + allow(Headless::CliUtil).to receive(:application_exists?).with("gm").and_return(false) - expect { headless.take_screenshot('a.png', :using => :graphicsmagick) }.to raise_error(Headless::Exception) + expect { headless.take_screenshot("a.png", using: :graphicsmagick) }.to raise_error(Headless::Exception) end it "raises an error if gm is not installed with using: :gm" do - allow(Headless::CliUtil).to receive(:application_exists?).with('gm').and_return(false) + allow(Headless::CliUtil).to receive(:application_exists?).with("gm").and_return(false) - expect { headless.take_screenshot('a.png', :using => :gm) }.to raise_error(Headless::Exception) + expect { headless.take_screenshot("a.png", using: :gm) }.to raise_error(Headless::Exception) end it "issues command to take screenshot, with default options" do - allow(Headless::CliUtil).to receive(:path_to).with('import').and_return('path/import') + allow(Headless::CliUtil).to receive(:path_to).with("import").and_return("path/import") expect(headless).to receive(:system).with("path/import -display :99 -window root /tmp/image.png") headless.take_screenshot("/tmp/image.png") end it "issues command to take screenshot, with using: :imagemagick" do - allow(Headless::CliUtil).to receive(:path_to).with('import').and_return('path/import') + allow(Headless::CliUtil).to receive(:path_to).with("import").and_return("path/import") expect(headless).to receive(:system).with("path/import -display :99 -window root /tmp/image.png") - headless.take_screenshot("/tmp/image.png", :using => :imagemagick) + headless.take_screenshot("/tmp/image.png", using: :imagemagick) end it "issues command to take screenshot, with using: :xwd" do - allow(Headless::CliUtil).to receive(:path_to).with('xwd').and_return('path/xwd') + allow(Headless::CliUtil).to receive(:path_to).with("xwd").and_return("path/xwd") expect(headless).to receive(:system).with("path/xwd -display localhost:99 -silent -root -out /tmp/image.png") - headless.take_screenshot("/tmp/image.png", :using => :xwd) + headless.take_screenshot("/tmp/image.png", using: :xwd) end it "issues command to take screenshot, with using: :graphicsmagick" do - allow(Headless::CliUtil).to receive(:path_to).with('gm').and_return('path/gm') + allow(Headless::CliUtil).to receive(:path_to).with("gm").and_return("path/gm") expect(headless).to receive(:system).with("path/gm import -display localhost:99 -window root /tmp/image.png") - headless.take_screenshot("/tmp/image.png", :using => :graphicsmagick) + headless.take_screenshot("/tmp/image.png", using: :graphicsmagick) end it "issues command to take screenshot, with using: :gm" do - allow(Headless::CliUtil).to receive(:path_to).with('gm').and_return('path/gm') + allow(Headless::CliUtil).to receive(:path_to).with("gm").and_return("path/gm") expect(headless).to receive(:system).with("path/gm import -display localhost:99 -window root /tmp/image.png") - headless.take_screenshot("/tmp/image.png", :using => :gm) + headless.take_screenshot("/tmp/image.png", using: :gm) end end end -private + private def stub_environment allow(Headless::CliUtil).to receive(:application_exists?).and_return(true) diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb index 4cd2521..bad5f29 100644 --- a/spec/integration_spec.rb +++ b/spec/integration_spec.rb @@ -1,29 +1,29 @@ -require 'headless' -require 'selenium-webdriver' +require "headless" +require "selenium-webdriver" -describe 'Integration test' do +describe "Integration test" do let!(:headless) { Headless.new } before { headless.start } after { headless.destroy_sync } - it 'should use xvfb' do + it "should use xvfb" do work_with_browser end - it 'should record screenshots' do + it "should record screenshots" do headless.take_screenshot("test.jpg") expect(File.exist?("test.jpg")).to eq true end - it 'should record video with ffmpeg' do + it "should record video with ffmpeg" do headless.video.start_capture work_with_browser headless.video.stop_and_save("test.mov") expect(File.exist?("test.mov")).to eq true end - it 'should raise an error when trying to create the same display' do + it "should raise an error when trying to create the same display" do expect { FileUtils.mv("/tmp/.X#{headless.display}-lock", "/tmp/headless-test-tmp") Headless.new(display: headless.display, reuse: false) @@ -35,7 +35,7 @@ def work_with_browser driver = Selenium::WebDriver.for :firefox - driver.navigate.to 'http://google.com' + driver.navigate.to "http://google.com" expect(driver.title).to match(/Google/) driver.close end diff --git a/spec/video_recorder_spec.rb b/spec/video_recorder_spec.rb index cb2eca7..d698406 100644 --- a/spec/video_recorder_spec.rb +++ b/spec/video_recorder_spec.rb @@ -1,6 +1,6 @@ -require 'headless' +require "headless" -require 'tempfile' +require "tempfile" describe Headless::VideoRecorder do before do @@ -8,89 +8,79 @@ end describe "instantiation" do - - it "throws an error if provider_binary_path is not installed" do + it "throws an error if ffmpeg_path is not installed" do allow(Headless::CliUtil).to receive(:application_exists?).and_return(false) expect { Headless::VideoRecorder.new(99, "1024x768x32") }.to raise_error(Headless::Exception) end - it "allows provider_binary_path to be specified" do - Tempfile.open('some_provider') do |f| - v = Headless::VideoRecorder.new(99, "1024x768x32", provider: :ffmpeg, provider_binary_path: f.path) - expect(v.provider_binary_path).to eq(f.path) + it "allows ffmpeg_path to be specified" do + Tempfile.open("ffmpeg") do |f| + v = Headless::VideoRecorder.new(99, "1024x768x32", ffmpeg_path: f.path) + expect(v.ffmpeg_path).to eq(f.path) end end - it "allows provider_binary_path to be specified" do - Tempfile.open('some_provider') do |f| - v = Headless::VideoRecorder.new(99, "1024x768x32", provider: :ffmpeg, provider_binary_path: f.path) - expect(v.provider_binary_path).to eq(f.path) + it "supports provider_binary_path for backward compatibility" do + Tempfile.open("ffmpeg") do |f| + v = Headless::VideoRecorder.new(99, "1024x768x32", provider_binary_path: f.path) + expect(v.ffmpeg_path).to eq(f.path) end end - - context "provider_binary_path not specified" do - it "assumes the provider binary is 'ffmpeg' if the provider is :ffmpeg" do - v = Headless::VideoRecorder.new(99, "1024x768x32", provider: :ffmpeg) - expect(v.provider_binary_path).to eq("ffmpeg") - end - - it "assumes the provider binary is 'avconv' if the provider is :libav" do - v = Headless::VideoRecorder.new(99, "1024x768x32", provider: :libav) - expect(v.provider_binary_path).to eq("avconv") - end - - end end describe "#capture" do before do - allow(Headless::CliUtil).to receive(:path_to).and_return('ffmpeg') + allow(Headless::CliUtil).to receive(:path_to).and_return("ffmpeg") end it "starts ffmpeg" do - 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') + 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", + File::NULL + ) recorder = Headless::VideoRecorder.new(99, "1024x768x32") recorder.start_capture end it "starts ffmpeg with specified codec" do - 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') - - recorder = Headless::VideoRecorder.new(99, "1024x768x32", {:codec => 'libvpx'}) - recorder.start_capture - end - - it "starts ffmpeg from ffmpeg provider with correct parameters" do - 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') + expect(Headless::CliUtil).to receive(:fork_process).with( + /^ffmpeg -y -r 30 -s 1024x768 -f x11grab -i :99 -vcodec libvpx [^ ]+$/, + "/tmp/.headless_ffmpeg_99.pid", + File::NULL + ) - recorder = Headless::VideoRecorder.new(99, "1024x768x32", {:provider => :ffmpeg}) + recorder = Headless::VideoRecorder.new(99, "1024x768x32", {codec: "libvpx"}) recorder.start_capture end it "starts ffmpeg with specified extra device options" do - expect(Headless::CliUtil).to receive(:fork_process).with(/^ffmpeg -y -r 30 -s 1024x768 -f x11grab -i :99 -draw_mouse 0 -g 600 -vcodec qtrle [^ ]+$/, "/tmp/.headless_ffmpeg_99.pid", '/dev/null') + expect(Headless::CliUtil).to receive(:fork_process).with( + /^ffmpeg -y -r 30 -s 1024x768 -f x11grab -i :99 -draw_mouse 0 -vcodec qtrle [^ ]+$/, + "/tmp/.headless_ffmpeg_99.pid", File::NULL + ) - recorder = Headless::VideoRecorder.new(99, "1024x768x32", {:devices => ["-draw_mouse 0"]}) + recorder = Headless::VideoRecorder.new(99, "1024x768x32", {devices: ["-draw_mouse 0"]}) recorder.start_capture end end context "stopping video recording" do - let(:tmpfile) { '/tmp/ci.mov' } - let(:filename) { '/tmp/test.mov' } - let(:pidfile) { '/tmp/pid' } + let(:tmpfile) { "/tmp/ci.mov" } + let(:filename) { "/tmp/test.mov" } + let(:pidfile) { "/tmp/pid" } subject do - recorder = Headless::VideoRecorder.new(99, "1024x768x32", :pid_file_path => pidfile, :tmp_file_path => tmpfile) + recorder = Headless::VideoRecorder.new(99, "1024x768x32", pid_file_path: pidfile, tmp_file_path: tmpfile) recorder.start_capture recorder end describe "using #stop_and_save" do it "stops video recording and saves file" do - expect(Headless::CliUtil).to receive(:kill_process).with(pidfile, :wait => true) - expect(File).to receive(:exists?).with(tmpfile).and_return(true) + expect(Headless::CliUtil).to receive(:kill_process).with(pidfile, wait: true) + expect(File).to receive(:exist?).with(tmpfile).and_return(true) expect(FileUtils).to receive(:mv).with(tmpfile, filename) subject.stop_and_save(filename) @@ -99,7 +89,7 @@ describe "using #stop_and_discard" do it "stops video recording and deletes temporary file" do - expect(Headless::CliUtil).to receive(:kill_process).with(pidfile, :wait => true) + expect(Headless::CliUtil).to receive(:kill_process).with(pidfile, wait: true) expect(FileUtils).to receive(:rm).with(tmpfile) subject.stop_and_discard