saucelabs-adapter 0.7.6

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.
@@ -0,0 +1,66 @@
1
+ local: &local
2
+ selenium_server_address: "127.0.0.1"
3
+ selenium_server_port: "4444"
4
+ selenium_browser_key: "*chrome /Applications/Firefox.app/Contents/MacOS/firefox-bin"
5
+ application_address: "127.0.0.1"
6
+ application_port: "4000"
7
+
8
+ local_jsunit:
9
+ <<: *local
10
+ application_port: "8080"
11
+
12
+ # Possible Sauce Labs configurations as of 2009/11/19
13
+ # From: http://saucelabs.com/products/docs/sauce-ondemand/browsers
14
+ #
15
+ # saucelabs_browser_os saucelabs_browser saucelabs_browser_version (pick one)
16
+ #
17
+ # "Windows 2003" "iexplore" "6.", "7.", "8."
18
+ # "firefox" "2.", "3.0", "3.5"
19
+ # "safari" "3.", "4."
20
+ # "opera" "9."
21
+ # "googlechrome" ""
22
+ # "Linux" "firefox" "3."
23
+ saucelabs: &saucelabs
24
+ # URL of Selenium RC server:
25
+ selenium_server_address: "saucelabs.com"
26
+ selenium_server_port: "4444"
27
+ # Saucelabs credentials / Browser to drive
28
+ saucelabs_username: "YOUR-SAUCELABS-USERNAME"
29
+ saucelabs_access_key: "YOUR-SAUCELABS-ACCESS-KEY"
30
+ saucelabs_browser_os: "Linux"
31
+ saucelabs_browser: "firefox"
32
+ saucelabs_browser_version: "3."
33
+ saucelabs_max_duration_seconds: 1800
34
+ # Selenium RC browser connects to and tests the app at this URL:
35
+ application_address: "this will be ovewritten if tunnel_method == :saucetunnel"
36
+ application_port: 80
37
+ # App host is actually a tunnel that tunnels from <application_address>:<application_port> to localhost:<tunnel_to_localhost_port>
38
+ tunnel_method: :saucetunnel
39
+ tunnel_to_localhost_port: 4000 # Warning: application_port and tunnel_to_localhost_port must be identical if you are using Webrat
40
+ tunnel_startup_timeout: 240
41
+
42
+ saucelabs_jsunit: &saucelabs_jsunit
43
+ <<: *saucelabs
44
+ # We are using the Jetty server for Saucelabs JsUnit selenium testing.
45
+ localhost_app_server_port: "8080"
46
+
47
+ saucelabs_jsunit_firefox:
48
+ <<: *saucelabs_jsunit
49
+
50
+ saucelabs_jsunit_ie:
51
+ <<: *saucelabs_jsunit
52
+ saucelabs_browser_os: "Windows 2003"
53
+ saucelabs_browser: "iexplore"
54
+ saucelabs_browser_version: "7."
55
+
56
+ saucelabs_jsunit_safari:
57
+ <<: *saucelabs_jsunit
58
+ saucelabs_browser_os: "Windows 2003"
59
+ saucelabs_browser: "safari"
60
+ saucelabs_browser_version: "4."
61
+
62
+ saucelabs_jsunit_chrome:
63
+ <<: *saucelabs_jsunit
64
+ saucelabs_browser_os: "Windows 2003"
65
+ saucelabs_browser: "googlechrome"
66
+ saucelabs_browser_version: ""
@@ -0,0 +1 @@
1
+ require 'saucelabs_adapter'
@@ -0,0 +1,6 @@
1
+ require 'saucelabs_adapter/selenium_config'
2
+ require 'saucelabs_adapter/sauce_tunnel'
3
+ require 'saucelabs_adapter/test_unit_adapter'
4
+ require 'saucelabs_adapter/jsunit_selenium_support'
5
+ module SaucelabsAdapter
6
+ end
@@ -0,0 +1,121 @@
1
+ module SaucelabsAdapter
2
+ module JsunitSeleniumSupport
3
+
4
+ def requires
5
+ require 'saucelabs_adapter/run_utils'
6
+ require "selenium/client"
7
+ require 'lsof'
8
+ end
9
+
10
+ def setup_jsunit_selenium(options = {})
11
+ requires
12
+ @selenium_config = SeleniumConfig.new(ENV['SELENIUM_ENV'])
13
+ start_app_server(options)
14
+ @selenium_driver = @selenium_config.create_driver(options)
15
+ puts "[JsunitSeleniumSupport] calling @selenium_driver.start" if options[:debug]
16
+ @selenium_driver.start
17
+ puts "[JsunitSeleniumSupport] @selenium_driver.start done" if options[:debug]
18
+ end
19
+
20
+ def teardown_jsunit_selenium
21
+ @selenium_driver.stop
22
+ stop_app_server
23
+ end
24
+
25
+ def run_jsunit_test(jsunit_params, options = {})
26
+ if $:.detect{ |x| x =~ /Selenium/}
27
+ raise 'Selenium gem should not be in path! (deprecated in favor of selenium-client, which we require)'
28
+ end
29
+
30
+ default_jsunit_params = {
31
+ :testPage => "/jsunit/javascripts/test-pages/suite.html",
32
+ :autorun => "true",
33
+ :setupPageTimeout => "60",
34
+ :pageLoadTimeout => "60",
35
+ :suppressCacheBuster => (@selenium_config.selenium_server_address == 'saucelabs.com').to_s
36
+ }
37
+ jsunit_params.reverse_merge!(default_jsunit_params)
38
+
39
+ test_url = "/jsunit/javascripts/jsunit/jsunit/testRunner.html?" + jsunit_params.map { |k,v| "#{k}=#{v}" }.join("&")
40
+ run_suite(@selenium_driver, test_url, options)
41
+ end
42
+
43
+ private
44
+
45
+ def pid_file
46
+ prepare_pid_file("#{RAILS_ROOT}/tmp/pids", "mongrel_selenium.pid")
47
+ end
48
+
49
+ def prepare_pid_file(file_path, pid_file_name)
50
+ FileUtils.mkdir_p File.expand_path(file_path)
51
+ File.expand_path("#{file_path}/#{pid_file_name}")
52
+ end
53
+
54
+ def local_app_server_port
55
+ @selenium_config.tunnel_to_localhost_port || @selenium_config.application_port
56
+ end
57
+
58
+ def start_app_server(options = {})
59
+ stop_app_server
60
+ puts "[JsunitSeleniumSupport] starting application server:"
61
+ app_server_logfile_path = options[:app_server_logfile_path] || "#{RAILS_ROOT}/log/jsunit_jetty_app_server.log"
62
+ RunUtils.run "ant -f #{RAILS_ROOT}/public/javascripts/jsunit/jsunit/build.xml start_server " +
63
+ "-Dport=#{local_app_server_port} " +
64
+ "-DcustomJsUnitJarPath=#{RAILS_ROOT}/public/javascripts/jsunit/jsunit_jar/jsunit.jar " +
65
+ "-DresourceBase=#{RAILS_ROOT}/public >> #{app_server_logfile_path} 2>&1 &"
66
+ end
67
+
68
+ def stop_app_server
69
+ raise "oops don't know port app server is running on" unless local_app_server_port
70
+ while Lsof.running?(local_app_server_port)
71
+ puts "Killing app server at #{local_app_server_port}..."
72
+ Lsof.kill(local_app_server_port)
73
+ sleep 1
74
+ end
75
+ end
76
+
77
+ def run_suite(selenium_driver, suite_path, options = {})
78
+ default_options = {
79
+ :timeout_in_seconds => 1200
80
+ }
81
+ options.reverse_merge!(default_options)
82
+
83
+ selenium_driver.open(suite_path)
84
+
85
+ # It would be nice if this worked, but it doesn't (it returns nil even though 'Done' is not in the element).
86
+ # selenium.wait_for_condition(
87
+ # "new RegExp('Done').test(window.mainFrame.mainStatus.document.getElementById('content').innerHTML)")
88
+
89
+ tests_completed = false
90
+ begin_time = Time.now
91
+ status = ""
92
+ puts "[JsunitSeleniumSupport] Starting to poll JsUnit..." if options[:verbose]
93
+ while (Time.now - begin_time) < options[:jsunit_suite_timeout_seconds] && !tests_completed
94
+ sleep 5
95
+ status = selenium_driver.js_eval("window.mainFrame.mainStatus.document.getElementById('content').innerHTML")
96
+ status.gsub!(/^<[bB]>Status:<\/[bB]> /, '')
97
+ # Long form: window.frames['mainFrame'].frames['mainCounts'].frames['mainCountsRuns'].document.getElementById('content').innerHTML
98
+ runs = selenium_driver.js_eval("window.mainFrame.mainCounts.mainCountsRuns.document.getElementById('content').innerHTML").strip
99
+ fails = selenium_driver.js_eval("window.mainFrame.mainCounts.mainCountsFailures.document.getElementById('content').innerHTML").strip
100
+ errors = selenium_driver.js_eval("window.mainFrame.mainCounts.mainCountsErrors.document.getElementById('content').innerHTML").strip
101
+ run_count = runs.match(/\d+$/)[0].to_i
102
+ fail_count = fails.match(/\d+$/)[0].to_i
103
+ error_count = errors.match(/\d+$/)[0].to_i
104
+ puts "[JsunitSeleniumSupport] runs/fails/errors: #{run_count}/#{fail_count}/#{error_count} status: #{status}" if options[:verbose]
105
+ if status =~ /^Done /
106
+ tests_completed = true
107
+ end
108
+ end
109
+ raise "[JsunitSeleniumSupport] Tests failed to complete after #{options[:jsunit_suite_timeout_seconds]}, status was '#{status}'" unless tests_completed
110
+
111
+ puts "[JsunitSeleniumSupport] ********** JSUnit tests complete, Runs: #{run_count}, Fails: #{fail_count}, Errors: #{error_count} **********"
112
+
113
+ if (fail_count + error_count > 0)
114
+ error_messages = selenium_driver.js_eval("window.mainFrame.mainErrors.document.getElementsByName('problemsList')[0].innerHTML")
115
+ puts "[JsunitSeleniumSupport] Error messages: #{error_messages}"
116
+ end
117
+
118
+ (fail_count + error_count) == 0
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,14 @@
1
+ class RunUtils
2
+ def self.run(command, options = {})
3
+ default_options = {
4
+ :raise_on_fail => true
5
+ }
6
+ options.reverse_merge!(default_options)
7
+ puts "Executing: #{command}"
8
+ success = system(command)
9
+ if !success && options[:raise_on_fail]
10
+ raise "Command failed: #{command}"
11
+ end
12
+ success
13
+ end
14
+ end
@@ -0,0 +1,122 @@
1
+ require 'net/ssh'
2
+ require 'net/ssh/gateway'
3
+ require 'saucerest-ruby/saucerest'
4
+ require 'saucerest-ruby/gateway'
5
+
6
+ module SaucelabsAdapter
7
+ class SauceTunnel
8
+ DEFAULT_TUNNEL_STARTUP_TIMEOUT = 240
9
+
10
+ def initialize(se_config)
11
+ raise "SauceTunnel.new requires a SeleniumConfig argument" unless se_config.is_a?(SeleniumConfig)
12
+ @se_config = se_config
13
+ connect_to_rest_api
14
+ start_tunnel
15
+ end
16
+
17
+ def start_tunnel
18
+ say "Setting up tunnel from Saucelabs (#{@se_config.application_address}:#{@se_config.application_port}) to localhost:#{@se_config.tunnel_to_localhost_port} (timeout #{tunnel_startup_timeout}s)..."
19
+ boot_tunnel_machine
20
+ setup_ssh_reverse_tunnel
21
+ # WARNING: JsUnit depends upon the format of this output line:
22
+ say "Tunnel ID #{@tunnel_id} for #{@se_config.application_address} is up."
23
+ end
24
+
25
+ def tunnel_startup_timeout
26
+ (@se_config.tunnel_startup_timeout || DEFAULT_TUNNEL_STARTUP_TIMEOUT).to_i
27
+ end
28
+
29
+ def shutdown
30
+ say "Shutting down tunnel to Saucelabs..."
31
+ teardown_ssh_reverse_tunnel
32
+ shutdown_tunnel_machine
33
+ say "done."
34
+ end
35
+
36
+ private
37
+
38
+ def connect_to_rest_api
39
+ sauce_api_url = "https://#{@se_config.saucelabs_username}:#{@se_config.saucelabs_access_key}@saucelabs.com/rest/#{@se_config.saucelabs_username}/"
40
+ debug "Connecting to Sauce API at #{sauce_api_url}"
41
+ @sauce_api_endpoint = SauceREST::Client.new sauce_api_url
42
+ end
43
+
44
+ def boot_tunnel_machine
45
+ debug "Booting tunnel host:"
46
+ response = @sauce_api_endpoint.create(:tunnel, 'DomainNames' => [@se_config.application_address])
47
+ if response.has_key? 'error'
48
+ raise "Error booting tunnel machine: " + response['error']
49
+ end
50
+ @tunnel_id = response['id']
51
+ debug "Tunnel id: %s" % @tunnel_id
52
+
53
+ Timeout::timeout(tunnel_startup_timeout) do
54
+ last_status = tunnel_status = nil
55
+ begin
56
+ sleep 5
57
+ @tunnel_info = @sauce_api_endpoint.get :tunnel, @tunnel_id
58
+ tunnel_status = @tunnel_info['Status']
59
+ debug " tunnel host is #{tunnel_status}" if tunnel_status != last_status
60
+ last_status = tunnel_status
61
+ case tunnel_status
62
+ when 'new', 'booting'
63
+ # Alrighty. Keep going.
64
+ when 'running'
65
+ # We're done.
66
+ when 'terminated'
67
+ raise "There was a problem booting the tunnel machine: it terminated (%s)" % @tunnel_info['Error']
68
+ else
69
+ raise "Unknown tunnel machine status: #{tunnel_status} (#{@tunnel_info.inspect})"
70
+ end
71
+ end while tunnel_status != 'running'
72
+ end
73
+ rescue Timeout::Error
74
+ error_message = "Tunnel did not come up in #{tunnel_startup_timeout} seconds."
75
+ STDERR.puts "[saucelabs-adapter] " + error_message
76
+ shutdown_tunnel_machine
77
+ raise error_message
78
+ end
79
+
80
+ def shutdown_tunnel_machine
81
+ return unless @sauce_api_endpoint && @tunnel_id
82
+ debug "Shutting down tunnel machine:"
83
+ Timeout::timeout(120) do
84
+ @sauce_api_endpoint.delete :tunnel, @tunnel_id
85
+ status = nil
86
+ begin
87
+ sleep 5
88
+ status = @sauce_api_endpoint.get(:tunnel, @tunnel_id)['Status']
89
+ debug status
90
+ end while status != 'terminated'
91
+ end
92
+ rescue Timeout::Error
93
+ # Do not raise here, or else you give false negatives from test runs
94
+ STDERR.puts "*" * 80
95
+ STDERR.puts "Sauce Tunnel failed to shut down! Go visit http://saucelabs.com/tunnels and shut down the tunnel for #{@se_config.application_address}"
96
+ STDERR.puts "*" * 80
97
+ end
98
+
99
+ def setup_ssh_reverse_tunnel
100
+ debug "Starting ssh reverse tunnel"
101
+ @gateway = Net::SSH::Gateway.new(@tunnel_info['Host'], @se_config.saucelabs_username, {:password => @se_config.saucelabs_access_key})
102
+ @port = @gateway.open_remote(@se_config.tunnel_to_localhost_port.to_i, "127.0.0.1", @se_config.application_port.to_i, "0.0.0.0")
103
+ end
104
+
105
+ def teardown_ssh_reverse_tunnel
106
+ if @gateway
107
+ debug "Shutting down ssh reverse tunnel"
108
+ @gateway.close(@port) if @port
109
+ @gateway.shutdown! if @gateway
110
+ debug "done."
111
+ end
112
+ end
113
+
114
+ def say(what)
115
+ STDOUT.puts "[saucelabs-adapter] " + what
116
+ end
117
+
118
+ def debug(what)
119
+ STDOUT.puts "[saucelabs-adapter] " + what if ENV['SAUCELABS_ADAPTER_DEBUG']
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,152 @@
1
+ module SaucelabsAdapter
2
+ class SeleniumConfig
3
+ attr_reader :configuration
4
+
5
+ def initialize(configuration_name = nil, selenium_yml_path = nil)
6
+ selenium_yml_path = selenium_yml_path || File.join(RAILS_ROOT, 'config', 'selenium.yml')
7
+ SeleniumConfig.parse_yaml(selenium_yml_path)
8
+ build_configuration(configuration_name)
9
+ end
10
+
11
+ def []=(attribute, value)
12
+ @configuration[attribute.to_s] = value
13
+ end
14
+
15
+ [ :selenium_server_address, :selenium_server_port,
16
+ :application_address, :application_port,
17
+ :saucelabs_username, :saucelabs_access_key,
18
+ :saucelabs_browser_os, :saucelabs_browser, :saucelabs_browser_version,
19
+ :saucelabs_max_duration_seconds,
20
+ :tunnel_method, :tunnel_to_localhost_port, :tunnel_startup_timeout ].each do |attr|
21
+ define_method(attr) do
22
+ @configuration[attr.to_s]
23
+ end
24
+ end
25
+
26
+ def selenium_browser_key
27
+ if selenium_server_address == 'saucelabs.com'
28
+ # Create the JSON string that Saucelabs needs:
29
+ { 'username' => saucelabs_username,
30
+ 'access-key' => saucelabs_access_key,
31
+ 'os' => saucelabs_browser_os,
32
+ 'browser' => saucelabs_browser,
33
+ 'browser-version' => saucelabs_browser_version,
34
+ 'max-duration' => saucelabs_max_duration_seconds.to_i,
35
+ 'job-name' => ENV['SAUCELABS_JOB_NAME'] || Socket.gethostname
36
+ }.to_json
37
+ else
38
+ @configuration['selenium_browser_key']
39
+ end
40
+ end
41
+
42
+ def application_address
43
+ if start_sauce_tunnel?
44
+ # We are using Sauce Labs and Sauce Tunnel.
45
+ # We need to use a masquerade hostname on the EC2 end of the tunnel that will be unique within the scope of
46
+ # this account (e.g. pivotallabs). Therefore we mint a fairly unique hostname here.
47
+ hostname = Socket.gethostname.split(".").first
48
+ "#{hostname}-#{Process.pid}.com"
49
+ else
50
+ @configuration['application_address']
51
+ end
52
+
53
+ end
54
+
55
+ # Takes a Webrat::Configuration object and configures it by calling methods on it
56
+ def configure_webrat(webrat_configuration_object)
57
+ {
58
+ 'selenium_server_address' => :selenium_server_address,
59
+ 'selenium_server_port' => :selenium_server_port,
60
+ 'selenium_browser_key' => :selenium_browser_key,
61
+ 'application_address' => :application_address,
62
+ 'application_port' => :application_port
63
+ }.each do |webrat_configuration_method, our_accessor|
64
+ webrat_configuration_object.send("#{webrat_configuration_method}=", self.send(our_accessor).to_s)
65
+ end
66
+ end
67
+
68
+ # Takes a Polonium::Configuration object and configures it by calling methods on it
69
+ def configure_polonium(polonium_configuration_object)
70
+ {
71
+ 'selenium_server_host' => :selenium_server_address,
72
+ 'selenium_server_port' => :selenium_server_port,
73
+ 'browser' => :selenium_browser_key,
74
+ 'external_app_server_host' => :application_address,
75
+ 'external_app_server_port' => :application_port
76
+ }.each do |polonium_configuration_method, our_accessor|
77
+ polonium_configuration_object.send("#{polonium_configuration_method}=", self.send(our_accessor).to_s)
78
+ end
79
+ end
80
+
81
+ def create_driver(selenium_args = {}, options = {})
82
+ args = selenium_client_driver_args.merge(selenium_args)
83
+ puts "[saucelabs-adapter] Connecting to Selenium RC server at #{args[:host]}:#{args[:port]} (testing app at #{args[:url]})" if options[:debug]
84
+ puts "[saucelabs-adapter] args = #{args.inspect}" if options[:debug]
85
+ driver = ::Selenium::Client::Driver.new(args)
86
+ puts "[saucelabs-adapter] done" if options[:debug]
87
+ driver
88
+ end
89
+
90
+ def start_sauce_tunnel?
91
+ tunnel_method.to_sym == :saucetunnel
92
+ end
93
+
94
+ def self.parse_yaml(selenium_yml_path)
95
+ raise "[saucelabs-adapter] could not open #{selenium_yml_path}" unless File.exist?(selenium_yml_path)
96
+ @@selenium_configs ||= YAML.load_file(selenium_yml_path)
97
+ end
98
+
99
+ private
100
+
101
+ def build_configuration(configuration_name)
102
+ @configuration = @@selenium_configs[configuration_name]
103
+ raise "[saucelabs-adapter] stanza '#{configuration_name}' not found in #{@selenium_yml}" unless @configuration
104
+ check_configuration(configuration_name)
105
+ end
106
+
107
+ def check_configuration(configuration_name)
108
+ errors = []
109
+ errors << require_attributes([:selenium_server_address, :selenium_server_port, :application_port])
110
+ if selenium_server_address == 'saucelabs.com'
111
+ errors << require_attributes([ :saucelabs_username, :saucelabs_access_key,
112
+ :saucelabs_browser_os, :saucelabs_browser, :saucelabs_browser_version,
113
+ :saucelabs_max_duration_seconds ],
114
+ "when selenium_server_address is saucelabs.com")
115
+ case tunnel_method.to_sym
116
+ when nil, ""
117
+ when :saucetunnel, :othertunnel
118
+ errors << require_attributes([:tunnel_to_localhost_port ],
119
+ "if tunnel_method is set")
120
+ else
121
+ errors << "Unknown tunnel_method: #{tunnel_method}"
122
+ end
123
+ else
124
+ errors << require_attributes([:selenium_browser_key, :application_address ],
125
+ "unless server is saucelab.com")
126
+ end
127
+
128
+ errors.flatten!.compact!
129
+ if !errors.empty?
130
+ raise "[saucelabs-adapter] Aborting; stanza #{configuration_name} has the following errors:\n\t" + errors.join("\n\t")
131
+ end
132
+ end
133
+
134
+ def require_attributes(names, under_what_circumstances = "")
135
+ errors = []
136
+ names.each do |attribute|
137
+ errors << "#{attribute} is required #{under_what_circumstances}" if send(attribute).nil?
138
+ end
139
+ errors
140
+ end
141
+
142
+ def selenium_client_driver_args
143
+ {
144
+ :host => selenium_server_address,
145
+ :port => selenium_server_port.to_s,
146
+ :browser => selenium_browser_key,
147
+ :url => "http://#{application_address}:#{application_port}",
148
+ :timeout_in_seconds => 600
149
+ }
150
+ end
151
+ end
152
+ end