jsunit-sauce 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2011 Sauce Labs Inc
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.
data/README.md ADDED
@@ -0,0 +1 @@
1
+ JSUnit glue for running your JSUnit tests in Sauce OnDemand
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:test) do |test|
6
+ test.libs << 'lib'
7
+ test.pattern = 'test/test_*.rb'
8
+ test.verbose = true
9
+ end
10
+
11
+ task :default => :test
@@ -0,0 +1,18 @@
1
+ module Sauce
2
+ module JSUnit
3
+ class RunUtils
4
+ def self.run(command, options = {})
5
+ default_options = {
6
+ :raise_on_fail => true
7
+ }
8
+ options = default_options.merge(options)
9
+ puts "Executing: #{command}"
10
+ success = system(command)
11
+ if !success && options[:raise_on_fail]
12
+ raise "Command failed: #{command}"
13
+ end
14
+ success
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,220 @@
1
+ module Sauce
2
+ module JSUnit
3
+ class SeleniumConfig
4
+
5
+ include Utilities
6
+
7
+ attr_reader :configuration
8
+
9
+ def initialize(configuration_name = nil, selenium_yml_path = nil)
10
+ selenium_yml_path = selenium_yml_path || File.join(ENV['RAILS_ROOT'] || RAILS_ROOT, 'config', 'selenium.yml')
11
+ SeleniumConfig.parse_yaml(selenium_yml_path)
12
+ build_configuration(configuration_name)
13
+ end
14
+
15
+ def []=(attribute, value)
16
+ @configuration[attribute.to_s] = value
17
+ end
18
+
19
+ [ :test_framework, :start_server,
20
+ :selenium_server_address, :selenium_server_port,
21
+ :application_address, :application_port,
22
+ :saucelabs_username, :saucelabs_access_key,
23
+ :saucelabs_browser_os, :saucelabs_browser, :saucelabs_browser_version,
24
+ :saucelabs_max_duration_seconds,
25
+ :tunnel_method, :tunnel_to_localhost_port, :tunnel_startup_timeout,
26
+ :tunnel_username, :tunnel_password, :tunnel_keyfile,
27
+ :jsunit_polling_interval_seconds, :kill_mongrel_after_suite ].each do |attr|
28
+ define_method(attr) do
29
+ @configuration[attr.to_s]
30
+ end
31
+ end
32
+
33
+ def selenium_browser_key
34
+ if selenium_server_address == 'saucelabs.com'
35
+ # Create the JSON string that Saucelabs needs:
36
+ { 'username' => saucelabs_username,
37
+ 'access-key' => saucelabs_access_key,
38
+ 'os' => saucelabs_browser_os,
39
+ 'browser' => saucelabs_browser,
40
+ 'browser-version' => saucelabs_browser_version,
41
+ 'max-duration' => saucelabs_max_duration_seconds.to_i,
42
+ 'job-name' => ENV['SAUCELABS_JOB_NAME'] || Socket.gethostname
43
+ }.to_json
44
+ else
45
+ @configuration['selenium_browser_key']
46
+ end
47
+ end
48
+
49
+ def application_address
50
+ if start_tunnel? &&
51
+ [:sauceconnecttunnel, :saucetunnel].include?(@configuration['tunnel_method'].to_sym)
52
+ # We are using Sauce Labs and Sauce Connect Tunnel or Sauce Tunnel.
53
+ # We need to use a masquerade hostname on the EC2/Sauce end of the tunnel that will be unique within the scope of
54
+ # this account (e.g. pivotallabs). Therefore we mint a fairly unique hostname here.
55
+ hostname = Socket.gethostname.split(".").first
56
+ "#{hostname}-#{Process.pid}.com"
57
+ else
58
+ @configuration['application_address']
59
+ end
60
+
61
+ end
62
+
63
+ # Takes a Webrat::Configuration object and configures it by calling methods on it
64
+ def configure_webrat(webrat_configuration_object)
65
+ # NOTE: application_port_for_selenium requires version > 0.7.3 of webrat
66
+ # Prior versions only have application_address, and don't have a concept of
67
+ # starting a rails server at one port, and hitting it at selenium via another
68
+ {
69
+ 'selenium_server_address' => :selenium_server_address,
70
+ 'selenium_server_port' => :selenium_server_port,
71
+ 'selenium_browser_key' => :selenium_browser_key,
72
+ 'application_address' => :application_address,
73
+ 'application_port_for_selenium' => start_tunnel? ? :tunnel_to_localhost_port : :application_port,
74
+ 'application_port' => :application_port
75
+ }.each do |webrat_configuration_method, our_accessor|
76
+ webrat_configuration_object.send("#{webrat_configuration_method}=", self.send(our_accessor).to_s)
77
+ end
78
+ end
79
+
80
+ # Takes a Polonium::Configuration object and configures it by calling methods on it
81
+ def configure_polonium(polonium_configuration_object)
82
+ {
83
+ 'selenium_server_host' => :selenium_server_address,
84
+ 'selenium_server_port' => :selenium_server_port,
85
+ 'browser' => :selenium_browser_key,
86
+ 'external_app_server_host' => :application_address,
87
+ 'external_app_server_port' => :application_port
88
+ }.each do |polonium_configuration_method, our_accessor|
89
+ polonium_configuration_object.send("#{polonium_configuration_method}=", self.send(our_accessor).to_s)
90
+ end
91
+ end
92
+
93
+ def create_driver(selenium_args = {})
94
+ args = selenium_client_driver_args.merge(selenium_args)
95
+ say "Connecting to Selenium RC server at #{args[:host]}:#{args[:port]} (testing app at #{args[:url]})" if ENV['SAUCELABS_ADAPTER_DEBUG']
96
+ say "args = #{display_safely(args)}" if ENV['SAUCELABS_ADAPTER_DEBUG']
97
+ driver = ::Selenium::Client::Driver.new(args)
98
+ debug "done"
99
+ driver
100
+ end
101
+
102
+ def start_tunnel?
103
+ !tunnel_method.nil? && tunnel_method.to_sym != :othertunnel
104
+ end
105
+
106
+ def kill_mongrel_after_suite?
107
+ return true if kill_mongrel_after_suite.nil?
108
+ kill_mongrel_after_suite.to_s == 'true'
109
+ end
110
+
111
+ def self.parse_yaml(selenium_yml_path)
112
+ raise "[saucelabs-adapter] could not open #{selenium_yml_path}" unless File.exist?(selenium_yml_path)
113
+ file_contents = File.open(selenium_yml_path).read
114
+ erb_parsed_file_contents = ERB.new(%{#{file_contents}}).result
115
+ configs = YAML.load(erb_parsed_file_contents)
116
+ @@selenium_configs ||= configs
117
+ end
118
+
119
+ private
120
+
121
+ def display_safely(selenium_args)
122
+ safe = selenium_args.dup
123
+ begin
124
+ safe[:browser] = JSON.parse( safe[:browser])
125
+ safe[:browser]['access-key'] = safe[:browser]['access-key'][0..4] + '...'
126
+ safe[:browser] = safe[:browser].to_json
127
+ rescue
128
+ # args are not always json, e.g. when running locally
129
+ # for now, just ignore any exceptions when trying to parse args with json
130
+ end
131
+ safe.inspect
132
+ end
133
+
134
+ def build_configuration(configuration_name)
135
+ @configuration = @@selenium_configs[configuration_name]
136
+ raise "[saucelabs-adapter] stanza '#{configuration_name}' not found in #{@selenium_yml}" unless @configuration
137
+ # If the Saucelabs-Adapter picked a port out of a range during this session, use it.
138
+ if ENV['SAUCELABS_ADAPTER_APPLICATION_PORT']
139
+ @configuration['application_port'] = ENV['SAUCELABS_ADAPTER_APPLICATION_PORT'].to_i
140
+ debug("Using application port #{application_port} from environment variable SAUCELABS_ADAPTER_APPLICATION_PORT", 2)
141
+ end
142
+ check_configuration(configuration_name)
143
+ end
144
+
145
+ def check_configuration(configuration_name)
146
+ errors = []
147
+ errors << require_attributes([:selenium_server_address, :selenium_server_port, :application_port])
148
+ if selenium_server_address == 'saucelabs.com'
149
+ errors << require_attributes([ :saucelabs_username, :saucelabs_access_key,
150
+ :saucelabs_browser_os, :saucelabs_browser, :saucelabs_browser_version,
151
+ :saucelabs_max_duration_seconds ],
152
+ :when => "when selenium_server_address is saucelabs.com")
153
+ if tunnel_method
154
+ case tunnel_method.to_sym
155
+ when nil, ""
156
+ when :saucetunnel
157
+ errors << require_attributes([:tunnel_to_localhost_port ], :when => "if tunnel_method is :saucetunnel")
158
+ when :sauceconnecttunnel
159
+ errors << require_attributes([:tunnel_to_localhost_port ], :when => "if tunnel_method is :sauceconnecttunnel")
160
+ when :othertunnel
161
+ errors << require_attributes([:application_address], :when => "when tunnel_method is :othertunnel")
162
+ errors << require_attributes([:tunnel_to_localhost_port ], :when => "if tunnel_method is :othertunnel")
163
+ when :sshtunnel
164
+ errors << require_attributes([:application_address], :when => "when tunnel_method is :sshtunnel")
165
+ errors << require_attributes([:tunnel_password, :tunnel_keyfile],
166
+ :when => "when tunnel_method is :sshtunnel",
167
+ :any_or_all => :any)
168
+ if application_address && application_port.is_a?(String) && application_port =~ /(\d+)-(\d+)/
169
+ # We have been given a port range. Find an unused one.
170
+ port = find_unused_port(application_address, ($1.to_i)..($2.to_i))
171
+ @configuration['application_port'] = port
172
+ @configuration['tunnel_to_localhost_port'] = port if test_framework.to_sym == :webrat
173
+ # Pass this calculated value on to any other instances of SeleniumConfig created
174
+ ENV['SAUCELABS_ADAPTER_APPLICATION_PORT'] = port.to_s
175
+ end
176
+ if tunnel_keyfile && !File.exist?(File.expand_path(tunnel_keyfile))
177
+ errors << "tunnel_keyfile '#{tunnel_keyfile}' does not exist"
178
+ end
179
+ else
180
+ errors << "Unknown tunnel_method: #{tunnel_method}"
181
+ end
182
+ end
183
+ else
184
+ errors << require_attributes([:selenium_browser_key, :application_address ],
185
+ :when => "unless server is saucelabs.com")
186
+ end
187
+
188
+ errors.flatten!.compact!
189
+ if !errors.empty?
190
+ raise "[saucelabs-adapter] Aborting; stanza #{configuration_name} has the following errors:\n\t" + errors.join("\n\t")
191
+ end
192
+ end
193
+
194
+ def require_attributes(names, options = {})
195
+ default_options = {
196
+ :when => "",
197
+ :any_or_all => :all
198
+ }
199
+ options = default_options.merge(options)
200
+
201
+ errors = []
202
+ names.each do |attribute|
203
+ errors << "#{attribute} is required #{options[:when]}" if send(attribute).nil?
204
+ end
205
+ errors = [] if options[:any_or_all] == :any && errors.size < names.size
206
+ errors
207
+ end
208
+
209
+ def selenium_client_driver_args
210
+ {
211
+ :host => selenium_server_address,
212
+ :port => selenium_server_port.to_s,
213
+ :browser => selenium_browser_key,
214
+ :url => "http://#{application_address}:#{application_port}",
215
+ :timeout_in_seconds => 600
216
+ }
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,69 @@
1
+ module Sauce
2
+ module JSUnit
3
+ module Utilities
4
+
5
+ def diagnostics_prefix
6
+ @diagnostics_prefix ||= '[saucelabs-adapter]'
7
+ end
8
+
9
+ def say(what)
10
+ STDOUT.puts "#{diagnostics_prefix} #{what}"
11
+ end
12
+
13
+ def debug(what, print_if_level_ge = 0)
14
+ if ENV['SAUCELABS_ADAPTER_DEBUG']
15
+ actual_level = ENV['SAUCELABS_ADAPTER_DEBUG'].to_i
16
+ STDOUT.puts "#{diagnostics_prefix} #{what}" if print_if_level_ge >= actual_level
17
+ end
18
+ end
19
+
20
+ def raise_with_message(message)
21
+ raise "#{diagnostics_prefix} #{message}"
22
+ end
23
+
24
+ def find_unused_port(hostname, range = (3000..5000))
25
+ debug 'searching for unused port', 2
26
+ range.each do |port|
27
+ debug "trying #{hostname}:#{port}", 2
28
+ begin
29
+ socket = TCPSocket.new(hostname, port)
30
+ rescue Errno::ECONNREFUSED
31
+ debug "it's good, returning #{port}", 2
32
+ return port
33
+ ensure
34
+ socket.close if socket
35
+ end
36
+ end
37
+ end
38
+
39
+ # parameters required when invoked by test_unit
40
+ def start_mongrel(suite_name = {})
41
+ pid_file = File.join(RAILS_ROOT, "tmp", "pids", "mongrel_selenium.pid")
42
+ port = suite_name[:port] rescue @selenium_config.application_port
43
+ say "Starting mongrel at #{pid_file}, port #{port}"
44
+ system "mongrel_rails start -d --chdir='#{RAILS_ROOT}' --port=#{port} --environment=test --pid #{pid_file} %"
45
+ end
46
+
47
+ def kill_mongrel_if_needed(suite_name = {})
48
+ mongrel_pid_file = File.join(RAILS_ROOT, "tmp", "pids", "mongrel_selenium.pid")
49
+ if File.exists?(mongrel_pid_file)
50
+ pid = File.read(mongrel_pid_file).to_i
51
+ say "Killing mongrel at #{pid}"
52
+ Process.kill("KILL", pid)
53
+ end
54
+ if File.exists?(mongrel_pid_file)
55
+ FileUtils.rm(mongrel_pid_file)
56
+ end
57
+ end
58
+
59
+ def setup_tunnel(suite_name = {})
60
+ @tunnel = SaucelabsAdapter::Tunnel.factory(@selenium_config)
61
+ @tunnel.start_tunnel
62
+ end
63
+
64
+ def teardown_tunnel(suite_name = {})
65
+ @tunnel.shutdown
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,134 @@
1
+ require 'sauce/jsunit/utilities'
2
+ module Sauce
3
+ module JSUnit
4
+ include Utilities
5
+
6
+ def requires
7
+ require 'sauce'
8
+ require 'sauce/jsunit/run_utils'
9
+ require 'sauce/jsunit/selenium_config'
10
+ require "selenium/client"
11
+ require 'lsof'
12
+ end
13
+
14
+ def setup_jsunit_selenium(options = {})
15
+ @diagnostics_prefix = '[SauceJSUnit]'
16
+ requires
17
+ @selenium_config = SeleniumConfig.new(ENV['SELENIUM_ENV'])
18
+ start_app_server(options)
19
+ @tunnel = Sauce::Connect.new(:port => 8080, :domain => "jsunit.test")
20
+ @tunnel.wait_until_ready
21
+ @selenium_driver = Sauce::Selenium.new(:job_name => "JSUnit", :browser_url=>"http://jsunit.test/")
22
+ debug "calling @selenium_driver.start"
23
+ @selenium_driver.start_new_browser_session :trustAllSSLCertificates => false
24
+ debug "@selenium_driver.start done"
25
+ end
26
+
27
+ def teardown_jsunit_selenium
28
+ @selenium_driver.stop
29
+ @tunnel.disconnect
30
+ stop_app_server
31
+ end
32
+
33
+ def run_jsunit_test(jsunit_params, options = {})
34
+ if $:.detect{ |x| x =~ /Selenium/}
35
+ raise_with_message 'Selenium gem should not be in path! (deprecated in favor of selenium-client, which we require)'
36
+ end
37
+
38
+ default_jsunit_params = {
39
+ :testPage => "/jsunit/javascripts/test-pages/suite.html",
40
+ :autorun => "true",
41
+ :setupPageTimeout => "60",
42
+ :pageLoadTimeout => "60",
43
+ :suppressCacheBuster => (@selenium_config.selenium_server_address == 'saucelabs.com').to_s
44
+ }
45
+ jsunit_params = default_jsunit_params.merge(jsunit_params)
46
+
47
+ test_url = "/jsunit/javascripts/jsunit/jsunit/testRunner.html?" + jsunit_params.map { |k,v| "#{k}=#{v}" }.join("&")
48
+ if @selenium_config.jsunit_polling_interval_seconds
49
+ options = {:polling_interval => @selenium_config.jsunit_polling_interval_seconds}.merge(options)
50
+ end
51
+ run_suite(@selenium_driver, test_url, options)
52
+ end
53
+
54
+ private
55
+
56
+ def pid_file
57
+ prepare_pid_file("#{RAILS_ROOT}/tmp/pids", "mongrel_selenium.pid")
58
+ end
59
+
60
+ def prepare_pid_file(file_path, pid_file_name)
61
+ FileUtils.mkdir_p File.expand_path(file_path)
62
+ File.expand_path("#{file_path}/#{pid_file_name}")
63
+ end
64
+
65
+ def local_app_server_port
66
+ @selenium_config.tunnel_to_localhost_port || @selenium_config.application_port
67
+ end
68
+
69
+ def start_app_server(options = {})
70
+ stop_app_server
71
+ say "starting application server:"
72
+ app_server_logfile_path = options[:app_server_logfile_path] || "#{RAILS_ROOT}/log/jsunit_jetty_app_server.log"
73
+ RunUtils.run "ant -f #{RAILS_ROOT}/public/javascripts/jsunit/jsunit/build.xml start_server " +
74
+ "-Dport=#{local_app_server_port} " +
75
+ "-DcustomJsUnitJarPath=#{RAILS_ROOT}/public/javascripts/jsunit/jsunit_jar/jsunit.jar " +
76
+ "-DresourceBase=#{RAILS_ROOT}/public >> #{app_server_logfile_path} 2>&1 &"
77
+ end
78
+
79
+ def stop_app_server
80
+ raise_with_message "oops don't know port app server is running on" unless local_app_server_port
81
+ while Lsof.running?(local_app_server_port)
82
+ say "Killing app server at #{local_app_server_port}..."
83
+ Lsof.kill(local_app_server_port)
84
+ sleep 1
85
+ end
86
+ end
87
+
88
+ def run_suite(selenium_driver, suite_path, options = {})
89
+ default_options = {
90
+ :timeout_in_seconds => 1200,
91
+ :polling_interval => 5
92
+ }
93
+ options = default_options.merge(options)
94
+
95
+ selenium_driver.open(suite_path)
96
+
97
+ # It would be nice if this worked, but it doesn't (it returns nil even though 'Done' is not in the element).
98
+ # selenium.wait_for_condition(
99
+ # "new RegExp('Done').test(window.mainFrame.mainStatus.document.getElementById('content').innerHTML)")
100
+
101
+ tests_completed = false
102
+ begin_time = Time.now
103
+ status = ""
104
+ say "Starting to poll JsUnit (every #{options[:polling_interval]}s)..." if options[:verbose]
105
+ while (Time.now - begin_time) < options[:jsunit_suite_timeout_seconds] && !tests_completed
106
+ sleep options[:polling_interval]
107
+ debug "polling now...", 2
108
+ status = selenium_driver.js_eval("window.mainFrame.mainStatus.document.getElementById('content').innerHTML")
109
+ status.gsub!(/^<[bB]>Status:<\/[bB]> /, '')
110
+ # Long form: window.frames['mainFrame'].frames['mainCounts'].frames['mainCountsRuns'].document.getElementById('content').innerHTML
111
+ runs = selenium_driver.js_eval("window.mainFrame.mainCounts.mainCountsRuns.document.getElementById('content').innerHTML").strip
112
+ fails = selenium_driver.js_eval("window.mainFrame.mainCounts.mainCountsFailures.document.getElementById('content').innerHTML").strip
113
+ errors = selenium_driver.js_eval("window.mainFrame.mainCounts.mainCountsErrors.document.getElementById('content').innerHTML").strip
114
+ run_count = runs.match(/\d+$/)[0].to_i
115
+ fail_count = fails.match(/\d+$/)[0].to_i
116
+ error_count = errors.match(/\d+$/)[0].to_i
117
+ say "runs/fails/errors: #{run_count}/#{fail_count}/#{error_count} status: #{status}" if options[:verbose]
118
+ if status =~ /^Done /
119
+ tests_completed = true
120
+ end
121
+ end
122
+ raise_with_message "Tests failed to complete after #{options[:jsunit_suite_timeout_seconds]}, status was '#{status}'" unless tests_completed
123
+
124
+ say "********** JSUnit tests complete, Runs: #{run_count}, Fails: #{fail_count}, Errors: #{error_count} **********"
125
+
126
+ if (fail_count + error_count > 0)
127
+ error_messages = selenium_driver.js_eval("window.mainFrame.mainErrors.document.getElementsByName('problemsList')[0].innerHTML")
128
+ say "Error messages: #{error_messages}"
129
+ end
130
+
131
+ (fail_count + error_count) == 0
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,5 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jsunit-sauce
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Eric Allen
14
+ - Chad Wooley
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2011-02-04 00:00:00 -08:00
20
+ default_executable:
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: sauce
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ hash: 95
31
+ segments:
32
+ - 0
33
+ - 16
34
+ - 0
35
+ version: 0.16.0
36
+ type: :runtime
37
+ version_requirements: *id001
38
+ - !ruby/object:Gem::Dependency
39
+ name: childprocess
40
+ prerelease: false
41
+ requirement: &id002 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ hash: 23
47
+ segments:
48
+ - 0
49
+ - 1
50
+ - 6
51
+ version: 0.1.6
52
+ type: :runtime
53
+ version_requirements: *id002
54
+ description: Adapter to run JsUnit test suites using browsers in Sauce OnDemand
55
+ email: eric@hackerengineer.net
56
+ executables: []
57
+
58
+ extensions: []
59
+
60
+ extra_rdoc_files:
61
+ - LICENSE
62
+ - README.md
63
+ files:
64
+ - LICENSE
65
+ - README.md
66
+ - Rakefile
67
+ - /Users/epall/Dropbox/code/jsunit-sauce/lib/sauce/jsunit/run_utils.rb
68
+ - /Users/epall/Dropbox/code/jsunit-sauce/lib/sauce/jsunit/selenium_config.rb
69
+ - /Users/epall/Dropbox/code/jsunit-sauce/lib/sauce/jsunit/utilities.rb
70
+ - /Users/epall/Dropbox/code/jsunit-sauce/lib/sauce/jsunit.rb
71
+ - /Users/epall/Dropbox/code/jsunit-sauce/test/helper.rb
72
+ has_rdoc: true
73
+ homepage: http://github.com/saucelabs/jsunit-sauce
74
+ licenses: []
75
+
76
+ post_install_message:
77
+ rdoc_options: []
78
+
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ hash: 3
87
+ segments:
88
+ - 0
89
+ version: "0"
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ hash: 3
96
+ segments:
97
+ - 0
98
+ version: "0"
99
+ requirements: []
100
+
101
+ rubyforge_project:
102
+ rubygems_version: 1.4.2
103
+ signing_key:
104
+ specification_version: 3
105
+ summary: JsUnit + Sauce OnDemand
106
+ test_files: []
107
+