kopflos 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +28 -0
- data/LICENSE.txt +676 -0
- data/README.rdoc +52 -0
- data/Rakefile +23 -0
- data/kopflos.gemspec +53 -0
- data/lib/kopflos.rb +63 -0
- data/lib/kopflos/cucumber.rb +16 -0
- data/lib/kopflos/rspec.rb +10 -0
- data/lib/kopflos/xvfb.rb +207 -0
- data/spec/kopflos/xvfb_spec.rb +81 -0
- data/spec/kopflos_spec.rb +112 -0
- data/spec/spec_helper.rb +12 -0
- metadata +87 -0
data/README.rdoc
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
= Kopflos
|
2
|
+
|
3
|
+
Yo dog, I heard you like testing in real browsers, so I put some virtual
|
4
|
+
framebuffer onto your system so you can test while you have no actual display.
|
5
|
+
|
6
|
+
== Installation
|
7
|
+
|
8
|
+
* Add 'kopflos' to your gemfile.
|
9
|
+
* Install xvfb.
|
10
|
+
* run your cucumber / steak / etc stories as before
|
11
|
+
* observe: no browser window popping up
|
12
|
+
|
13
|
+
Do not know how to support Mac yet. But which ruby developer uses a macintosh anyway?
|
14
|
+
|
15
|
+
=== Cucumber
|
16
|
+
|
17
|
+
In your env.rb:
|
18
|
+
|
19
|
+
require 'kopflos/cucumber'
|
20
|
+
|
21
|
+
=== RSpec
|
22
|
+
|
23
|
+
In your spec_helper.rb or rails_helper.rb
|
24
|
+
|
25
|
+
require 'kopflos/rspec'
|
26
|
+
|
27
|
+
== Future / TODO
|
28
|
+
|
29
|
+
* Screenshot support -- already build in, may get more sophisticated later (TM).
|
30
|
+
* disable kopflos temporarily -- to look at your browser from time to time
|
31
|
+
|
32
|
+
== Contributing to kopflos
|
33
|
+
|
34
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
35
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
36
|
+
* Fork the project
|
37
|
+
* Start a feature/bugfix branch
|
38
|
+
* Commit and push until you are happy with your contribution
|
39
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
40
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
41
|
+
|
42
|
+
== Gotchas
|
43
|
+
|
44
|
+
=== Lockfiles
|
45
|
+
|
46
|
+
Xvfb may leave lockfiles behind (/tmp/.X999-lock). If they run out, Xvfb cannot be started and Selenium may fail with EOFError.
|
47
|
+
|
48
|
+
== Copyright
|
49
|
+
|
50
|
+
Copyright (c) 2011 Niklas Hofer. See LICENSE.txt for
|
51
|
+
further details.
|
52
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
require 'rspec/core'
|
13
|
+
require 'rspec/core/rake_task'
|
14
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
15
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
16
|
+
end
|
17
|
+
|
18
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
19
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
20
|
+
spec.rcov = true
|
21
|
+
end
|
22
|
+
|
23
|
+
task :default => :spec
|
data/kopflos.gemspec
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
# stub: kopflos ruby lib
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "kopflos"
|
6
|
+
s.version = "0.0.2"
|
7
|
+
|
8
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
9
|
+
s.require_paths = ["lib"]
|
10
|
+
s.authors = ["Niklas Hofer"]
|
11
|
+
s.date = "2016-08-16"
|
12
|
+
s.description = "Starts a virtual framebuffer XServer (Xvfb) and redirects all X clients there"
|
13
|
+
s.email = "niklas+dev@lanpartei.de"
|
14
|
+
s.extra_rdoc_files = [
|
15
|
+
"LICENSE.txt",
|
16
|
+
"README.rdoc"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
"Gemfile",
|
20
|
+
"Gemfile.lock",
|
21
|
+
"LICENSE.txt",
|
22
|
+
"README.rdoc",
|
23
|
+
"Rakefile",
|
24
|
+
"kopflos.gemspec",
|
25
|
+
"lib/kopflos.rb",
|
26
|
+
"lib/kopflos/cucumber.rb",
|
27
|
+
"lib/kopflos/rspec.rb",
|
28
|
+
"lib/kopflos/xvfb.rb",
|
29
|
+
"spec/kopflos/xvfb_spec.rb",
|
30
|
+
"spec/kopflos_spec.rb",
|
31
|
+
"spec/spec_helper.rb"
|
32
|
+
]
|
33
|
+
s.homepage = "http://github.com/niklas/kopflos"
|
34
|
+
s.licenses = ["MIT"]
|
35
|
+
s.rubygems_version = "2.5.1"
|
36
|
+
s.summary = "hides all your spawning X applications"
|
37
|
+
|
38
|
+
if s.respond_to? :specification_version then
|
39
|
+
s.specification_version = 4
|
40
|
+
|
41
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
42
|
+
s.add_development_dependency(%q<rspec>, [">= 2"])
|
43
|
+
s.add_development_dependency(%q<bundler>, [">= 0"])
|
44
|
+
else
|
45
|
+
s.add_dependency(%q<rspec>, [">= 2"])
|
46
|
+
s.add_dependency(%q<bundler>, [">= 0"])
|
47
|
+
end
|
48
|
+
else
|
49
|
+
s.add_dependency(%q<rspec>, [">= 2"])
|
50
|
+
s.add_dependency(%q<bundler>, [">= 0"])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
data/lib/kopflos.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'kopflos/xvfb'
|
2
|
+
|
3
|
+
module Kopflos
|
4
|
+
class AlreadyRunning < Exception; end
|
5
|
+
|
6
|
+
EnvVar = 'KOPFLOS'
|
7
|
+
|
8
|
+
def self.start(opts={})
|
9
|
+
if disabled_by_env_variable?
|
10
|
+
log "disabled through environment variable #{EnvVar} (set to anything else than 'false' or '0')"
|
11
|
+
return
|
12
|
+
end
|
13
|
+
if disabled_by_switch_file?
|
14
|
+
log "disabled through switch file #{switch_file_path} (unlink to hide the frakkin browser windows again)"
|
15
|
+
return
|
16
|
+
end
|
17
|
+
if running?
|
18
|
+
unless opts[:reuse]
|
19
|
+
raise AlreadyRunning, "there is already a server running: #{@server}"
|
20
|
+
end
|
21
|
+
else
|
22
|
+
@server = Xvfb.new(opts)
|
23
|
+
@server.start
|
24
|
+
@server
|
25
|
+
end
|
26
|
+
rescue Xvfb::NotSupported => e
|
27
|
+
log "your platform is not supported (yet): #{RUBY_PLATFORM}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.stop
|
31
|
+
if running?
|
32
|
+
@server.stop
|
33
|
+
@server = nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.running?
|
38
|
+
@server && @server.running?
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.reset!
|
42
|
+
stop
|
43
|
+
@server = nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.disabled_by_env_variable?
|
47
|
+
%w(false disabled 0 disable no).include?(ENV[EnvVar].to_s)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.disabled_by_switch_file?
|
51
|
+
File.exists?( switch_file_path )
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.switch_file_path
|
55
|
+
'kopflos_disabled'
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.log(message)
|
59
|
+
if ENV['LOG']
|
60
|
+
STDERR.puts "#{self}: #{message}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'kopflos'
|
2
|
+
|
3
|
+
$kopflos_exit_code = 0
|
4
|
+
|
5
|
+
Before '@javascript' do |scenario|
|
6
|
+
Kopflos.start :reuse => true
|
7
|
+
end
|
8
|
+
|
9
|
+
After do |scenario|
|
10
|
+
$kopflos_exit_code = 1 if scenario.failed?
|
11
|
+
end
|
12
|
+
|
13
|
+
at_exit do
|
14
|
+
Kopflos.stop
|
15
|
+
exit($kopflos_exit_code)
|
16
|
+
end
|
data/lib/kopflos/xvfb.rb
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'open4'
|
3
|
+
|
4
|
+
# heavily inspired by selenium-client
|
5
|
+
# https://github.com/jodell/selenium-client/blob/da0f3dfbc05a6377c0cada09a3d650daf1261415/lib/xvfb/xvfb.rb
|
6
|
+
module Kopflos
|
7
|
+
class Xvfb
|
8
|
+
class Error < ::Exception; end
|
9
|
+
|
10
|
+
class NotInstalled < Error
|
11
|
+
def message
|
12
|
+
msg = <<-MSG
|
13
|
+
Could not find Xvfb in PATH.
|
14
|
+
Try installing xvfb via `sudo apt-get install xvfb`.
|
15
|
+
PATH: #{ENV['PATH']}
|
16
|
+
MSG
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class NotSupported < Error; end
|
21
|
+
|
22
|
+
attr_accessor :font_path, :resolution, :screen, :wait
|
23
|
+
|
24
|
+
def initialize(options = {})
|
25
|
+
@font_path = options[:font_path] || self.class.determine_font_path
|
26
|
+
@resolution = options[:resolution] || '1024x768x24'
|
27
|
+
@screen = options[:screen] || '1'
|
28
|
+
@wait = options[:wait] || 5
|
29
|
+
@manager = options[:manager] || options[:wm]
|
30
|
+
raise NotSupported unless self.class.supported?
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.start(options={})
|
34
|
+
xvfb = new(options)
|
35
|
+
xvfb.start
|
36
|
+
xvfb
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.supported?
|
40
|
+
RUBY_PLATFORM =~ /linux/
|
41
|
+
end
|
42
|
+
|
43
|
+
def start
|
44
|
+
authorize
|
45
|
+
@server = start_server_in_subprocess
|
46
|
+
log "started => #{@server.pid}"
|
47
|
+
sleep @wait # FIXME is there a generic way to find out if Xvfb has started?
|
48
|
+
@window_manager = start_window_manager
|
49
|
+
log "finished"
|
50
|
+
end
|
51
|
+
|
52
|
+
def running?
|
53
|
+
!!@server && !!@server.status
|
54
|
+
end
|
55
|
+
|
56
|
+
def stop
|
57
|
+
kill_window_manager
|
58
|
+
kill_server
|
59
|
+
Process.wait
|
60
|
+
rescue Errno::ENOENT => e
|
61
|
+
rescue Errno::ECHILD => e
|
62
|
+
rescue Errno::EPIPE => e
|
63
|
+
ensure
|
64
|
+
if @servernum
|
65
|
+
File.unlink( lockfile ) if File.exist?(lockfile)
|
66
|
+
end
|
67
|
+
load_env @origin_env
|
68
|
+
@server = nil
|
69
|
+
end
|
70
|
+
|
71
|
+
def screenshot(filename='screenshot.png')
|
72
|
+
system 'import', '-window', 'root', filename
|
73
|
+
end
|
74
|
+
|
75
|
+
def command
|
76
|
+
[
|
77
|
+
executable,
|
78
|
+
":#{servernum}",
|
79
|
+
'-br', # black background
|
80
|
+
"-fp", @font_path,
|
81
|
+
'-screen', @screen,
|
82
|
+
@resolution,
|
83
|
+
]
|
84
|
+
end
|
85
|
+
|
86
|
+
def authorize
|
87
|
+
@origin_env = current_env
|
88
|
+
ENV['XAUTHORITY'] = authfile.path
|
89
|
+
ENV['DISPLAY'] = ":#{servernum}.#{@screen}"
|
90
|
+
IO.popen('xauth source -', 'w') do |xauth|
|
91
|
+
xauth.puts %Q~add :#{servernum} . #{mcookie}~
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_s
|
96
|
+
"<#{self.class} :#{servernum}.#{@screen}>"
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
protected
|
101
|
+
|
102
|
+
def executable
|
103
|
+
return @executable if defined?(@executable)
|
104
|
+
@executable = `which Xvfb`.chomp
|
105
|
+
if @executable.empty?
|
106
|
+
raise NotInstalled
|
107
|
+
end
|
108
|
+
@executable
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.determine_font_path
|
112
|
+
if RUBY_PLATFORM =~ /linux/
|
113
|
+
case `cat /etc/issue`
|
114
|
+
when /Debian|Ubuntu/
|
115
|
+
return '/usr/share/fonts/X11/misc'
|
116
|
+
else
|
117
|
+
return ENV['XVFB_FONT_PATH'] if ENV['XVFB_FONT_PATH']
|
118
|
+
end
|
119
|
+
end
|
120
|
+
raise NotSupported, "#{RUBY_PLATFORM} not supported by default, Export $XVFB_FONT_PATH with a path to your X11 fonts/misc directory"
|
121
|
+
end
|
122
|
+
|
123
|
+
def authfile
|
124
|
+
@authfile ||= Tempfile.new('kopflos_Xauthority')
|
125
|
+
end
|
126
|
+
|
127
|
+
def mcookie
|
128
|
+
@mcookie ||= `mcookie`.chomp
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
# Find a free server number by looking at .X*-lock files in /tmp.
|
133
|
+
def find_free_servernum
|
134
|
+
servernum = 99
|
135
|
+
while File.exists?( lockfile( servernum ) )
|
136
|
+
servernum += 1
|
137
|
+
end
|
138
|
+
servernum
|
139
|
+
end
|
140
|
+
|
141
|
+
def servernum
|
142
|
+
@servernum ||= find_free_servernum
|
143
|
+
end
|
144
|
+
|
145
|
+
def start_window_manager
|
146
|
+
return unless @manager
|
147
|
+
Open4::background @manager
|
148
|
+
rescue Errno::ECHILD => e
|
149
|
+
log "could not start window manager: #{e}"
|
150
|
+
end
|
151
|
+
|
152
|
+
def log(message)
|
153
|
+
message = "#{self} #{message}"
|
154
|
+
if defined?(Rails)
|
155
|
+
Rails.logger.info(message)
|
156
|
+
else
|
157
|
+
STDERR.puts message
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def lockfile(num=servernum)
|
162
|
+
"/tmp/.X#{num}-lock"
|
163
|
+
end
|
164
|
+
|
165
|
+
def start_server_in_subprocess
|
166
|
+
empty = ''
|
167
|
+
log "starting: #{command.join(' ')}"
|
168
|
+
Open4::background(command, 0 => empty, 1 => empty, 2 => empty, 'ignore_exit_failure' => true)
|
169
|
+
rescue Errno::ECHILD => e
|
170
|
+
log "could not start server: #{e}"
|
171
|
+
end
|
172
|
+
|
173
|
+
def kill_server
|
174
|
+
if @server
|
175
|
+
# first, kill the X server
|
176
|
+
Process.kill("USR1", @server.pid)
|
177
|
+
# then wait for the thread to finish
|
178
|
+
@server.join
|
179
|
+
else
|
180
|
+
log "no server found to kill"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def kill_window_manager
|
185
|
+
if @window_manager && @window_manager.respond_to?(:pid)
|
186
|
+
# first, kill the external process
|
187
|
+
Process.kill("USR1", @window_manager.pid)
|
188
|
+
# then wait for the thread to finish
|
189
|
+
@window_manager.join
|
190
|
+
else
|
191
|
+
log "no server found to kill"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def current_env
|
196
|
+
{ :display => ENV['DISPLAY'], :xauthority => ENV['XAUTHORITY'] }
|
197
|
+
end
|
198
|
+
|
199
|
+
def load_env(env)
|
200
|
+
if env && env.respond_to?(:[])
|
201
|
+
ENV['DISPLAY'], ENV['XAUTHORITY'] = env[:display], env[:xauthority]
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
|
207
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Kopflos
|
4
|
+
describe Xvfb do
|
5
|
+
it "should be a class" do
|
6
|
+
Xvfb.should be_a(Class)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should determine font path on initialization" do
|
10
|
+
Xvfb.should_receive(:determine_font_path)
|
11
|
+
Xvfb.new :wait => 1
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
describe 'new' do
|
16
|
+
before :each do
|
17
|
+
process = mock("process running in background", :pid => 0, :status => 'sleep')
|
18
|
+
Xvfb.stub!(:determine_font_path).and_return('/usr/share/fonts/X11/misc')
|
19
|
+
@xvfb = Xvfb.new
|
20
|
+
Open4.stub!(:background).and_return( process )
|
21
|
+
@xvfb.stub!(:kill_server).and_return(true)
|
22
|
+
end
|
23
|
+
|
24
|
+
after :each do
|
25
|
+
@xvfb.stop
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should set font path" do
|
29
|
+
@xvfb.font_path.should == '/usr/share/fonts/X11/misc'
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should have default resolution" do
|
33
|
+
@xvfb.resolution.should == '1024x768x24'
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should have default screen" do
|
37
|
+
@xvfb.screen.should == '1'
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should have a default wait time of 5s" do
|
41
|
+
@xvfb.wait.should == 5
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should authorize itself on startup" do
|
45
|
+
@xvfb.should_receive(:authorize).and_return(true)
|
46
|
+
@xvfb.start
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should reflect its status" do
|
50
|
+
@xvfb.start
|
51
|
+
@xvfb.should be_running
|
52
|
+
@xvfb.stop
|
53
|
+
@xvfb.should_not be_running
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should set DISPLAY and XAUTHORITY env vars" do
|
57
|
+
display, xauthority = ENV['DISPLAY'], ENV['XAUTHORITY']
|
58
|
+
@xvfb.authorize
|
59
|
+
|
60
|
+
ENV['DISPLAY'].should_not be_nil
|
61
|
+
ENV['DISPLAY'].should_not be_empty
|
62
|
+
ENV['DISPLAY'].should_not == display
|
63
|
+
ENV['XAUTHORITY'].should_not be_nil
|
64
|
+
ENV['XAUTHORITY'].should_not be_empty
|
65
|
+
ENV['XAUTHORITY'].should_not == xauthority
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "in forked process" do
|
69
|
+
before :each do
|
70
|
+
@xvfb.stub!(:authorize).and_return(true)
|
71
|
+
@xvfb.stub!(:execute).and_return(true) # not really, should NOT return, it calles Kernel#exec
|
72
|
+
end
|
73
|
+
it "should start window manager" do
|
74
|
+
@xvfb.should_receive(:start_window_manager).and_return(true)
|
75
|
+
@xvfb.start
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|