cloud_test 1.0.7

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,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