cloud_test 1.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "cloud_test"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,48 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "cloud_test/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "cloud_test"
7
+ spec.version = CloudTest::VERSION
8
+ spec.authors = ["Philipp Häusele"]
9
+ spec.email = ["philipp.haeusele@makandra.de"]
10
+
11
+ spec.summary = %q{Enables automated Cloud-Testing with various providers. }
12
+ spec.description = %q{Enables cross-browser-testing with by the integration of the following providers Browserstack, Crossbrowsertesting, Saucelabs and lambdatest. Based on cucumber and capybara}
13
+ spec.homepage = "https://github.com/makandra/cloud_test"
14
+ spec.license = "MIT"
15
+
16
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
18
+ if spec.respond_to?(:metadata)
19
+ # spec.metadata["allowed_push_host"] = "RubyGems.org"
20
+
21
+ spec.metadata["homepage_uri"] = "https://github.com/makandra/cloud_test"
22
+ spec.metadata["source_code_uri"] = "https://github.com/makandra/cloud_test"
23
+ spec.metadata["changelog_uri"] = "https://github.com/makandra/cloud_test/CHANGELOG.md"
24
+ else
25
+ raise "RubyGems 2.0 or newer is required to protect against " \
26
+ "public gem pushes."
27
+ end
28
+
29
+ # Specify which files should be added to the gem when it is released.
30
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
31
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
32
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
33
+ end
34
+ spec.bindir = "exe"
35
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
36
+ spec.require_paths = ["lib"]
37
+
38
+ spec.add_development_dependency "bundler", "~> 2.0"
39
+ spec.add_development_dependency "rake", "~> 10.0"
40
+ spec.add_development_dependency "minitest", "~> 5.0"
41
+
42
+ spec.add_development_dependency "aruba"
43
+
44
+ spec.add_dependency "thor"
45
+ spec.add_dependency 'capybara'
46
+ spec.add_dependency "cucumber"
47
+ spec.add_development_dependency "selenium-webdriver"
48
+ end
@@ -0,0 +1,19 @@
1
+ server: "hub-cloud.browserstack.com"
2
+ user: "philipp127"
3
+ key: "vJwJsjXGMKeQw6vDus5x"
4
+
5
+ common_caps:
6
+ "browserstack.local": true
7
+ "browserstack.debug": true # Visual log
8
+ "acceptSslCerts": true # allow self signed certificates
9
+ "name": "Bstack-[Capybara] Local Test"
10
+
11
+ browser_caps:
12
+ IE:
13
+ "browser": "IE"
14
+ "browser_version": "11.0"
15
+ "os": "Windows"
16
+ "os_version": "7"
17
+
18
+ #
19
+ #curl -u "philipp127:vJwJsjXGMKeQw6vDus5x" -X PUT -H "Content-Type: application/json" -d "{\"status\":\"<new-status>\", \"reason\":\"<reason text>\"}" https://api.browserstack.com/automate/sessions/<session-id>.json
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/cloud_test/cli'
4
+ # 'cloud_test/cli'
5
+ CloudTest::CLI.start
@@ -0,0 +1,34 @@
1
+ require "cloud_test/version"
2
+ require "cloud_test/core"
3
+
4
+
5
+ module CloudTest
6
+ class Error < StandardError; end
7
+ def self.enabled?
8
+ ENV.has_key?('CLOUD_TEST')
9
+ end
10
+
11
+ def self.upload_status_to_provider(success:, session_id:, reason: "Unknown")
12
+ begin
13
+ Core.upload_status success: success, session_id: session_id, reason: reason
14
+ rescue StandardError => e
15
+ puts e.message
16
+ end
17
+ end
18
+ if enabled?
19
+ config = Core.load_config
20
+ CloudTest::Core.get_provider_class(config).init config
21
+ end
22
+
23
+ at_exit do # is this better in here or in the cloud_test.rb file with the Before tag?
24
+ if enabled?
25
+ if $!.nil? || $!.is_a?(SystemExit) && $!.success? # if test was successful
26
+ Core.list_dashboard_link
27
+ else
28
+ code = $!.is_a?(SystemExit) ? $!.status : 1 #if test was not successful, keep exit code
29
+ Core.list_dashboard_link
30
+ exit code
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,77 @@
1
+ require_relative 'core'
2
+
3
+ module CloudTest
4
+ class Browserstack < Core
5
+ # do anything browserstack specific
6
+ SERVER = "hub-cloud.browserstack.com/wd/hub"
7
+ ENV_USER ='BROWSERSTACK_USERNAME'
8
+ ENV_PWD = 'BROWSERSTACK_ACCESS_KEY'
9
+ DASHBOARD_LINK = "https://automate.browserstack.com/dashboard"
10
+ REST_STATUS_SERVER = "https://api.browserstack.com/automate/sessions/"
11
+
12
+ def self.init(config=nil)
13
+ @config = config || Core.load_config(ENV_USER, ENV_PWD)
14
+ @caps = Core.get_default_caps
15
+
16
+ @caps["browserstack.local"] = true
17
+ @caps["browserstack.debug"] = true # Visual log
18
+ @caps['os_version'] = '10'
19
+ @caps['os'] = 'WINDOWS'
20
+ @caps['browser'] = 'CHROME'
21
+
22
+ @caps = Core.merge_caps(@caps, @config, 'browserstack')
23
+ Capybara.app_host = "http://127.0.0.1:38946"
24
+ Capybara.server_port = 38946
25
+ if !config.nil?
26
+ start()
27
+ end
28
+ end
29
+
30
+ def self.start
31
+ puts '> Running features on browserstack.com'
32
+ begin
33
+ require 'browserstack/local'
34
+ rescue LoadError
35
+ puts 'Please add gem "browserstack-local" to your gemfile!'
36
+ raise LoadError
37
+ end
38
+ # Code to start browserstack local (tunnel) before start of test
39
+ @bs_local = BrowserStack::Local.new
40
+ bs_local_args = {"key" => "#{@config['key']}", "logfile" => "log/browserstack-local-logs.log"}
41
+ @bs_local.start(bs_local_args)
42
+ register_driver(@caps, @config['user'], @config['key'], SERVER)
43
+ # Code to stop browserstack local after end of test
44
+ at_exit do
45
+ @bs_local.stop unless @bs_local.nil?
46
+ end
47
+ end
48
+
49
+ def self.list_caps # defaults
50
+ Core.list_caps
51
+ puts "Browserstack specific defaults:"
52
+ puts "\tbrowserstack.local: true"
53
+ puts "\tbrowserstack.debug: true "
54
+ puts 'you can generate capabilities here https://www.browserstack.com/automate/capabilities?tag=selenium-2-3'
55
+ end
56
+
57
+ def self.get_all_caps # dry run
58
+ @caps.kind_of?(Hash) || init()
59
+ puts "Capabilities: "
60
+ Core.list_these_caps(@caps)
61
+ end
62
+
63
+ def self.get_status_msg(failed, reason)
64
+ {
65
+ "status" => failed ? "passed" : "failed",
66
+ "reason" => reason
67
+ }
68
+ end
69
+
70
+ def self.check_session_id(session_id)
71
+ unless session_id =~ Regexp.new('^[0-9a-z]{40}$')
72
+ puts session_id << "length: " << session_id.length
73
+ raise "session_id is invalid!"
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,150 @@
1
+ require 'thor'
2
+ require 'cloud_test/generators/config'
3
+ require 'cloud_test/generators/cloud_test_env'
4
+ require_relative 'browserstack'
5
+ require_relative 'lambdatest'
6
+ require_relative 'cross_browser_testing'
7
+ require_relative 'saucelabs'
8
+
9
+
10
+ module CloudTest
11
+ class CLI < Thor
12
+ desc "list-caps PROVIDER", "Shows the currently applied capabilities of that provider"
13
+ def list_caps(provider)
14
+ hash = {"provider" => provider}
15
+ Core.get_provider_class(hash).get_all_caps
16
+ end
17
+
18
+ desc "list-default-caps PROVIDER", "Shows the default capabilities for that provider"
19
+ def list_default_caps(provider)
20
+ hash = {"provider" => provider}
21
+ Core.get_provider_class(hash).list_caps
22
+ end
23
+
24
+ desc "generate config", "Puts a sample config yml file into /config directory"
25
+ def generate_config(config)
26
+ CloudTest::Generators::Config.start([config])
27
+ end
28
+
29
+ desc "generate", "Puts a sample config yml file into /config directory, and additionally put a cloud_test.rb in features/support"
30
+ option :provider, :aliases => '-p', :default => 'browserstack'
31
+ def generate()
32
+ case options[:provider].to_s.downcase
33
+ when 'lambdatest', 'lt', 'l'
34
+ provider = 'lambdatest'
35
+ when 'crossbrowsertesting', 'cbs', 'ct', 'cbt', 'c'
36
+ provider = 'crossbrowsertesting'
37
+ when 'saucelabs', 'sauce', 'sc', 'sl', 's'
38
+ provider = 'saucelabs'
39
+ else
40
+ provider = 'browserstack'
41
+ end
42
+ CloudTest::Generators::Config.start([provider])
43
+ CloudTest::Generators::Support.start()
44
+ end
45
+
46
+ desc "cucumber", "Runs bundle exec cucumber sequentially for all defined browsers. Uses the cucumber tag. With -q hide output."
47
+ option :q
48
+ def start()
49
+ require 'open3'
50
+ config = Core.load_config
51
+ config['browsers'].keys.each { |browser_config_name|
52
+ Open3.popen2e({'CLOUD_TEST' =>browser_config_name.to_s}, "bundle" ,"exec", "cucumber", "-t","#{config['cucumber_tag'].to_s}") do |stdin, stdout_err, wait_thr|
53
+ unless options[:q]
54
+ while line = stdout_err.gets
55
+ puts line
56
+ end
57
+ end
58
+ exit_status = wait_thr.value
59
+ if exit_status == 0
60
+ puts "Test on browser: #{browser_config_name} was successful!"
61
+ else
62
+ puts "Test on browser: #{browser_config_name} was not successful!"
63
+ puts stdout_err
64
+ raise "did not work"
65
+ end
66
+ end
67
+ }
68
+ end
69
+
70
+ desc "each COMMANDS", "Runs the COMMAND sequentially for all defined browsers. With -q hide output."
71
+ option :q
72
+ def each(*commands)
73
+ require 'open3'
74
+ config = Core.load_config
75
+ config['browsers'].keys.each { |browser_config_name|
76
+ Open3.popen2e({'CLOUD_TEST' =>browser_config_name.to_s}, commands.join(" ")) do |stdin, stdout_err, wait_thr|
77
+ unless options[:q]
78
+ while line = stdout_err.gets
79
+ puts line
80
+ end
81
+ end
82
+ exit_status = wait_thr.value
83
+ if exit_status == 0
84
+ puts "Test on browser: #{browser_config_name} was successful!"
85
+ else
86
+ puts "Error on browser: #{browser_config_name}!"
87
+ puts stdout_err.read
88
+ puts "Error on browser: #{browser_config_name}!"
89
+ end
90
+ end
91
+ }
92
+ end
93
+
94
+ desc "test-connection", "Test whether the provider and credentials work, by connectiong to the api"
95
+ def test_connection()
96
+ require 'net/http'
97
+ require 'uri'
98
+ config = CloudTest::Core.load_config
99
+ request, uri, request, server = Hash.new
100
+ if config.has_key?('provider') && config.has_key?('user') && config.has_key?('key')
101
+ case config.delete 'provider'.to_s.downcase
102
+ when 'browserstack', 'bs', 'b'
103
+ server = "https://www.browserstack.com/local/v1/list?auth_token=#{config['key']}&last=1"
104
+ when 'lambdatest', 'lt', 'l'
105
+ server = "https://api.lambdatest.com/automation/api/v1/tunnels"
106
+ uri = URI.parse(server)
107
+ request = Net::HTTP::Get.new(uri)
108
+ request.basic_auth(config['user'], config['key'])
109
+ when 'crossbrowsertesting', 'cbs', 'ct', 'cbt', 'c'
110
+ server = "https://crossbrowsertesting.com/api/v3/tunnels"
111
+ uri = URI.parse(server)
112
+ request = Net::HTTP::Get.new(uri)
113
+ request.basic_auth(config['user'].sub("@", "%40"), config['key'])
114
+ when 'saucelabs', 'sauce', 'sc', 'sl', 's'
115
+ server = "https://saucelabs.com/rest/v1/#{config['user']}/tunnels"
116
+ uri = URI.parse(server)
117
+ request = Net::HTTP::Get.new(uri)
118
+ request.basic_auth(config['user'], config['key'])
119
+ else
120
+ puts "Unknown provider!"
121
+ return
122
+ end
123
+ uri ||= URI.parse(server)
124
+ request ||= Net::HTTP::Get.new(uri)
125
+ request["Accept"] = "application/json"
126
+ req_options = {
127
+ use_ssl: uri.scheme == "https",
128
+ }
129
+ response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
130
+ http.request(request)
131
+ end
132
+ if response.code == '200'
133
+ puts "Connection successful!"
134
+ else
135
+ puts "Connection was not successful! :("
136
+ puts response.code
137
+ end
138
+ else
139
+ puts "You have not all necessary keys in your config file. `provider`, `user`, `key` are necessary"
140
+ end
141
+ end
142
+ desc 'version', 'Display version'
143
+ map %w[-v --version] => :version
144
+
145
+ def version
146
+ say "cloud_test version: #{CloudTest::VERSION}"
147
+ end
148
+ end
149
+ end
150
+
@@ -0,0 +1,180 @@
1
+ require 'selenium/webdriver'
2
+ require 'capybara'
3
+ require 'yaml'
4
+
5
+ module CloudTest
6
+ class Core
7
+ CONFIG_NAME = 'cloud_test'
8
+
9
+ def self.get_default_caps
10
+ @caps = Hash.new
11
+ @caps['project'] = File.split(Dir.getwd)[-1] # folder name
12
+ @caps['build'] = `git rev-parse HEAD || echo buildname` # HEAD commit hash
13
+ @caps['name'] = `git log -1 --pretty=%B || echo testname` # HEAD commit message
14
+ return @caps
15
+ end
16
+
17
+ def self.check_if_input_is_valid?(str)
18
+ #for all relevant config user input only allow an optional '@' for the cucumber_tag, then some letters, followed by
19
+ # optional digits
20
+ # this should enhance security
21
+ Regexp.new('^@?[A-z]+\d*$') =~ str
22
+ end
23
+
24
+ # the optional parameter could be deleted, or used if someone does not want to put there credentials in the config
25
+ def self.load_config(env_user='CLOUD_TEST_USER', env_pw='CLOUD_TEST_PW')
26
+ config = Hash.new
27
+
28
+ @caps = self.get_default_caps
29
+ path = `pwd`.to_s.gsub(/\s+/, "") + "/config/#{CONFIG_NAME}.yml" # complete path to the config file
30
+ begin
31
+ config = YAML.load_file(File.absolute_path path)
32
+ if ENV.has_key?(env_user) && ENV.has_key?(env_pw)
33
+ config['key'] = ENV[env_pw]
34
+ config['user'] = ENV[env_user]
35
+ end
36
+ rescue SystemCallError
37
+ puts 'Error: no config file found at: ' + path
38
+ puts 'Tip: You should run your tests from your main project directory'
39
+ puts 'Error: I need a config yml file, named ENV["CONFIG_NAME"] or "cloud_test.yml" which has at least a "user" and and "key" pair, thank you!'
40
+ else
41
+ if config.has_key?('user') && config.has_key?('key') && config.has_key?('provider') # check wether all the necessary keys exist
42
+ list_to_check_input = [config['browsers'].keys, (config['cucumber_tag'] if config.has_key?('cucumber_tag')), config['provider']].flatten
43
+ list_to_check_input.each do |str|
44
+ if !check_if_input_is_valid?(str)
45
+ raise "Invalid value: #{str}. Only characters followed by digits are allowed!"
46
+ end
47
+ end
48
+ return config
49
+ else
50
+ puts 'Error: I have a config yml file, but no user, key or provider value :('
51
+ puts "Keys: " + config.keys.to_s
52
+ end
53
+ end
54
+ end
55
+
56
+ def self.register_driver(capsArray, user, key, server)
57
+ # some debugging options
58
+ url = "https://#{user.sub("@", "%40")}:#{key}@#{server}"
59
+ if capsArray.has_key?('cloud_test_debug') and capsArray['cloud_test_debug']
60
+ puts "Capybara.app_host = #{Capybara.app_host}"
61
+ puts "Hub url: #{url}"
62
+ list_these_caps capsArray
63
+ end
64
+ Capybara.register_driver :cloud_test do |app|
65
+ Capybara::Selenium::Driver.new(app,
66
+ :browser => :remote,
67
+ :url => url,
68
+ :desired_capabilities => capsArray
69
+ )
70
+ end
71
+ end
72
+
73
+ def self.list_caps # print defaults
74
+ # this output could be reformatted
75
+ puts 'These are the defaults:' + """
76
+ PROJECT : # name of the folder
77
+ BUILD : # HEAD commit hash
78
+ NAME : # HEAD commit message
79
+ OS : '10'
80
+ PLATFORM : 'WINDOWS'
81
+ BROWSER : 'CHROME'"""
82
+ puts 'Please add additional capabilities in the cloud_test.yml file'
83
+ end
84
+
85
+ def self.list_these_caps(caps)
86
+ if caps.kind_of?(Enumerable)
87
+ caps.each do |key, value|
88
+ puts "|#{key.to_s.ljust(25)}|#{value.to_s.ljust(44)}|\n" # make a nice table like layout
89
+ end
90
+ else
91
+ puts "Error: No caps"
92
+ end
93
+ end
94
+
95
+ def self.copy_keys(caps, config, keys=config.keys) # a small helper method, to copy keys
96
+ keys.each do |key|
97
+ caps[key] = config[key]
98
+ end
99
+ end
100
+
101
+ def self.merge_caps(caps, config, provider=nil, browser=ENV['CLOUD_TEST']) # config overwrites in case of conflict
102
+ if !config.kind_of?(Hash)
103
+ return caps
104
+ end
105
+ keys = config.keys - ['common_caps', 'browsers'] # handle those seperatly
106
+ copy_keys caps, config, keys
107
+ if provider && config.has_key?(provider) && config[provider].class.included_modules.include?(Enumerable)
108
+ copy_keys caps, config[provider]
109
+ end
110
+ if config.has_key?('common_caps')
111
+ caps = caps.merge(config['common_caps'])
112
+ end
113
+ if config.has_key?('browsers')
114
+ if config['browsers'].kind_of?(Hash)
115
+ if !browser.nil? && config['browsers'][browser].nil?
116
+ puts "There is no browser with the key:#{browser} in your config file!"
117
+ raise "No matching browser key found!"
118
+ end
119
+ caps = caps.merge(config['browsers'][browser || config['browsers'].keys[0]])
120
+ else
121
+ caps = caps.merge(config['browsers'])
122
+ end
123
+ end
124
+ return caps
125
+ end
126
+
127
+ def self.get_provider_class(config=load_config)
128
+ case config.delete('provider').to_s.downcase
129
+ when 'browserstack', 'bs', 'b'
130
+ require 'cloud_test/browserstack'
131
+ return Browserstack
132
+ when 'lambdatest', 'lt', 'l'
133
+ require 'cloud_test/lambdatest'
134
+ return Lambdatest
135
+ when 'crossbrowsertesting', 'cbs', 'ct', 'cbt', 'c'
136
+ require 'cloud_test/cross_browser_testing'
137
+ return CrossBrowserTesting
138
+ when 'saucelabs', 'sauce', 'sc', 'sl', 's'
139
+ require 'cloud_test/saucelabs'
140
+ return Saucelabs
141
+ else
142
+ puts "Error: Please add a valid provider to your config file!"
143
+ end
144
+ end
145
+
146
+ def self.list_dashboard_link
147
+ puts "link to the dashboard: #{get_provider_class::DASHBOARD_LINK}"
148
+ end
149
+
150
+ def self.upload_status(success:, session_id:, reason: "Unknown")
151
+ config = load_config
152
+ provider = get_provider_class config
153
+ provider.check_session_id session_id
154
+ puts session_id
155
+ unless provider::REST_STATUS_SERVER.present?
156
+ puts "skipping upload, not implementet for your provider yet."
157
+ return
158
+ end
159
+ require 'net/http'
160
+ require 'uri'
161
+ require 'json'
162
+ uri = URI.parse(provider::REST_STATUS_SERVER + session_id )
163
+ request = Net::HTTP::Put.new(uri)
164
+ request.basic_auth(config['user'], config['key'])
165
+ request.content_type = "application/json"
166
+ request.body = JSON.dump(provider.get_status_msg(success, reason))
167
+ req_options = {
168
+ use_ssl: uri.scheme == "https",
169
+ }
170
+ response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
171
+ http.request(request)
172
+ end
173
+ if response.code != '200'
174
+ puts "Response Code: #{response.code}"
175
+ puts "Status upload error!"
176
+ end
177
+
178
+ end
179
+ end
180
+ end