saucelabs-adapter 0.7.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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