fastlane-plugin-saucectl 0.1.0.pre → 0.1.3.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,115 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'uri'
4
- require 'net/http'
5
- require 'open-uri'
6
- require 'json'
7
- require 'base64'
8
- require 'timeout'
9
- require 'fastlane_core/ui/ui'
10
- require 'fileutils'
11
-
12
- module Fastlane
13
- module Saucectl
14
- #
15
- # This class provides the functions required to interact with the saucectl api
16
- # for more information see: https://docs.saucelabs.com/dev/api/storage/
17
- #
18
- class Api
19
- UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
20
-
21
- def initialize(config)
22
- @config = config
23
- @encoded_auth_string = Base64.strict_encode64("#{@config[:sauce_username]}:#{@config[:sauce_access_key]}")
24
- @messages = YAML.load_file("#{__dir__}/../strings/messages.yml")
25
- end
26
-
27
- def available_devices
28
- path = 'v1/rdc/devices/available'
29
- https, url = build_http_request_for(path)
30
- request = Net::HTTP::Get.new(url)
31
- request['Authorization'] = "Basic #{@encoded_auth_string}"
32
- response = https.request(request)
33
- UI.user_error!("❌ Request failed: #{response.code} #{response.message}") unless response.kind_of?(Net::HTTPOK)
34
-
35
- JSON.parse(response.body)
36
- end
37
-
38
- def fetch_ios_devices
39
- devices = []
40
- get_devices = available_devices
41
- get_devices.each do |device|
42
- devices << device if device =~ /iPhone_.*/ || device =~ /iPad_.*/
43
- end
44
- devices
45
- end
46
-
47
- def fetch_android_devices
48
- devices = []
49
- get_devices = available_devices
50
- get_devices.each do |device|
51
- devices << device unless device =~ /iPhone_.*/ || device =~ /iPad_.*/
52
- end
53
- devices
54
- end
55
-
56
- def retrieve_all_apps
57
- UI.message("retrieving all apps for \"#{@config[:query]}\".")
58
- path = "v1/storage/files?q=#{@config[:query]}&kind=#{@config[:platform]}"
59
- https, url = build_http_request_for(path)
60
- request = Net::HTTP::Get.new(url)
61
- request['Authorization'] = "Basic #{@encoded_auth_string}"
62
- response = https.request(request)
63
-
64
- UI.user_error!("❌ Request failed: #{response.code} #{response.message}") unless response.kind_of?(Net::HTTPOK)
65
-
66
- response
67
- end
68
-
69
- def upload
70
- UI.message("⏳ Uploading \"#{@config[:app]}\" upload to Sauce Labs.")
71
- path = 'v1/storage/upload'
72
- https, url = build_http_request_for(path)
73
- request = Net::HTTP::Post.new(url)
74
- request['Authorization'] = "Basic #{@encoded_auth_string}"
75
- form_data = [['payload', File.open(@config[:file])], ['name', @config[:app]]]
76
- request.set_form(form_data, 'multipart/form-data')
77
- response = https.request(request)
78
- UI.success("✅ Successfully uploaded app to sauce labs: \n #{response.body}") if response.code.eql?('201')
79
- UI.user_error!("❌ Request failed: #{response.code} #{response.message}") unless response.code.eql?('201')
80
-
81
- response
82
- end
83
-
84
- def delete_app(path)
85
- https, url = build_http_request_for(path)
86
- request = Net::HTTP::Delete.new(url.path)
87
- request['Authorization'] = "Basic #{@encoded_auth_string}"
88
- response = https.request(request)
89
- UI.success("✅ Successfully deleted app from sauce labs storage: \n #{response.body}") if response.kind_of?(Net::HTTPOK)
90
- UI.user_error!("❌ Request failed: #{response.code} #{response.message}") unless response.kind_of?(Net::HTTPOK)
91
-
92
- response
93
- end
94
-
95
- def base_url_for_region
96
- case @config[:region]
97
- when 'eu' then base_url('eu-central-1')
98
- when 'us' then base_url('us-west-1')
99
- else UI.user_error!("#{@config[:region]} is an invalid region ❌. Available: #{@messages['supported_regions']}")
100
- end
101
- end
102
-
103
- def build_http_request_for(path)
104
- url = URI("#{base_url_for_region}/#{path}")
105
- https = Net::HTTP.new(url.host, url.port)
106
- https.use_ssl = true
107
- [https, url]
108
- end
109
-
110
- def base_url(region)
111
- "https://api.#{region}.saucelabs.com"
112
- end
113
- end
114
- end
115
- end
@@ -1,97 +0,0 @@
1
- require 'fileutils'
2
- require 'yaml'
3
- require 'uri'
4
- require 'net/http'
5
- require 'json'
6
- require 'base64'
7
- require 'open3'
8
- require_relative 'suites'
9
-
10
- module Fastlane
11
- module Saucectl
12
- #
13
- # This class creates saucectl config.yml file based on given specifications
14
- #
15
- class ConfigGenerator
16
- UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
17
-
18
- def initialize(config)
19
- @config = config
20
- end
21
-
22
- def base_config
23
- {
24
- 'apiVersion' => 'v1alpha',
25
- 'kind' => @config[:kind],
26
- 'retries' => @config[:retries],
27
- 'sauce' => {
28
- 'region' => set_region.to_s,
29
- 'concurrency' => @config[:max_concurrency_size],
30
- 'metadata' => {
31
- 'name' => "#{ENV['JOB_NAME']}-#{ENV['BUILD_NUMBER']}",
32
- 'build' => "Release #{ENV['CI_COMMIT_SHORT_SHA']}"
33
- }
34
- },
35
- (@config[:kind]).to_s => set_apps,
36
- 'artifacts' => {
37
- 'download' => {
38
- 'when' => 'always',
39
- 'match' => ['junit.xml'],
40
- 'directory' => './artifacts/'
41
- }
42
- },
43
- 'reporters' => {
44
- 'junit' => {
45
- 'enabled' => true
46
- }
47
- }
48
- }
49
- end
50
-
51
- def set_region
52
- case @config[:region]
53
- when 'eu'
54
- 'eu-central-1'
55
- else
56
- 'us-west-1'
57
- end
58
- end
59
-
60
- def set_apps
61
- {
62
- 'app' => @config[:app],
63
- 'testApp' => @config[:test_app]
64
- }
65
- end
66
-
67
- def create
68
- UI.message("Creating saucectl config .....🚕💨")
69
- file_name = 'config.yml'
70
- UI.user_error!("❌ Sauce Labs platform does not support virtual device execution for ios apps") if @config[:platform].eql?('ios') && @config[:emulators]
71
-
72
- config = base_config.merge(create_suite)
73
- out_file = File.new(file_name, 'w')
74
- out_file.puts(config.to_yaml)
75
- out_file.close
76
- creat_sauce_dir
77
- FileUtils.move(file_name, './.sauce')
78
- UI.message("Successfully created saucectl config ✅") if Dir.exist?('.sauce')
79
- UI.user_error!("Failed to create saucectl config ❌") unless Dir.exist?('.sauce')
80
- end
81
-
82
- def create_suite
83
- suite = Fastlane::Saucectl::Suites.new(@config)
84
- { 'suites' => if @config[:emulators]
85
- suite.create_virtual_device_suites
86
- else
87
- suite.create_real_device_suites
88
- end }
89
- end
90
-
91
- def creat_sauce_dir
92
- dirname = '.sauce'
93
- FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
94
- end
95
- end
96
- end
97
- end
@@ -1,93 +0,0 @@
1
- require "find"
2
- require "open3"
3
- require "json"
4
- require_relative "file_utils"
5
-
6
- module Fastlane
7
- module Saucectl
8
- # This class is responsible for creating test execution plans for ios applications and will distribute tests
9
- # that will be be executed via the cloud provider.
10
- #
11
- class Espresso
12
- include FileUtils
13
- UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
14
-
15
- TEST_FUNCTION_REGEX = /([a-z]+[A-Z][a-zA-Z]+)[(][)]/.freeze
16
-
17
- def initialize(config)
18
- @config = config
19
- end
20
-
21
- def test_data
22
- test_details = []
23
- search_retrieve_test_classes(@config[:path_to_tests]).each do |f|
24
- next unless File.basename(f) =~ CLASS_NAME_REGEX
25
-
26
- test_details << { package: File.readlines(f).first.chomp.gsub("package ", "").gsub(";", ""),
27
- class: File.basename(f).gsub(FILE_TYPE_REGEX, ""),
28
- tests: tests_from(f) }
29
- end
30
-
31
- strip_empty(test_details)
32
- end
33
-
34
- def test_distribution
35
- test_distribution_check
36
- tests_arr = []
37
- case @config[:test_distribution]
38
- when "package"
39
- test_data.each { |type| tests_arr << type[:package] }
40
- when 'class', 'shard'
41
- test_data.each { |type| tests_arr << "#{type[:package]}.#{type[:class]}" }
42
- else
43
- test_data.each do |type|
44
- type[:tests].each { |test| tests_arr << "#{type[:package]}.#{type[:class]}##{test}" }
45
- end
46
- end
47
- tests_arr.uniq
48
- end
49
-
50
- def test_distribution_check
51
- return @config[:test_distribution] if @config[:test_distribution].kind_of?(Array)
52
-
53
- distribution_types = %w[class testCase package shard]
54
- unless distribution_types.include?(@config[:test_distribution]) || @config[:test_distribution].nil?
55
- UI.user_error!("#{@config[:test_distribution]} is not a valid method of test distribution")
56
- end
57
- end
58
-
59
- def strip_empty(test_details)
60
- tests = []
61
- test_details.each { |test| tests << test unless test[:tests].size.zero? }
62
- tests
63
- end
64
-
65
- def tests_from(path)
66
- stdout, = find(path, "@Test")
67
- test_cases = []
68
- stdout.split.each do |line|
69
- test_cases << line.match(TEST_FUNCTION_REGEX).to_s.gsub(/[()]/, "") if line =~ TEST_FUNCTION_REGEX
70
- end
71
- strip_skipped(path, test_cases)
72
- end
73
-
74
- def fetch_disabled_tests(path)
75
- stdout, = find(path, "@Ignore")
76
- test_cases = []
77
- stdout.split.each do |line|
78
- test_cases << line.match(TEST_FUNCTION_REGEX).to_s.gsub(/[()]/, "") if line =~ TEST_FUNCTION_REGEX
79
- end
80
- test_cases
81
- end
82
-
83
- def strip_skipped(path, tests)
84
- enabled_ui_tests = []
85
- skipped_tests = fetch_disabled_tests(path)
86
- tests.each do |test|
87
- enabled_ui_tests << test unless skipped_tests.include?(test)
88
- end
89
- enabled_ui_tests
90
- end
91
- end
92
- end
93
- end
@@ -1,27 +0,0 @@
1
- require "open3"
2
-
3
- # utility module for helper functions
4
- module FileUtils
5
- CLASS_NAME_REGEX = /(Spec|Specs|Test|Tests)/.freeze
6
- FILE_TYPE_REGEX = /(.swift|.kt|.java)/.freeze
7
-
8
- def read_file(name)
9
- raise "File not found: #{name}" unless File.exist?(name)
10
-
11
- File.read(name).split
12
- end
13
-
14
- def search_retrieve_test_classes(path)
15
- Find.find(path).select do |f|
16
- File.file?(f) if File.basename(f) =~ CLASS_NAME_REGEX
17
- end
18
- end
19
-
20
- def find(class_name, regex)
21
- syscall("find '#{class_name}' -type f -exec grep -h -C2 '#{regex}' {} +")
22
- end
23
-
24
- def syscall(*cmd)
25
- Open3.capture3(*cmd)
26
- end
27
- end
@@ -1,51 +0,0 @@
1
- require 'fastlane_core/ui/ui'
2
- require 'fastlane'
3
- require 'open-uri'
4
- require_relative 'file_utils'
5
-
6
- module Fastlane
7
- module Saucectl
8
- #
9
- # This class provides the functions required to install the saucectl binary
10
- #
11
- class Installer
12
- include FileUtils
13
- UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
14
-
15
- def install
16
- timeout_in_seconds = 30
17
- Timeout.timeout(timeout_in_seconds) do
18
- download_saucectl_installer
19
- execute_saucectl_installer
20
- UI.success("✅ Successfully installed saucectl runner binary 🚀")
21
- rescue OpenURI::HTTPError => e
22
- response = e.io
23
- UI.user_error!("❌ Failed to install saucectl binary: status #{response.status[0]}")
24
- end
25
- end
26
-
27
- def download_saucectl_installer
28
- URI.open('sauce', 'wb') do |file|
29
- file << URI.open('https://saucelabs.github.io/saucectl/install').read
30
- end
31
- end
32
-
33
- def execute_saucectl_installer
34
- status = system('sh sauce')
35
- status == 1 ? UI.user_error!("❌ failed to install saucectl: #{stderr}") : status
36
- executable = 'saucectl'
37
- FileUtils.mv("bin/#{executable}", executable) unless File.exist?(executable)
38
- end
39
-
40
- def system(*cmd)
41
- Open3.popen2e(*cmd) do |stdin, stdout_stderr, wait_thread|
42
- Thread.new do
43
- stdout_stderr.each { |out| UI.message(out) }
44
- end
45
- stdin.close
46
- wait_thread.value
47
- end
48
- end
49
- end
50
- end
51
- end
@@ -1,36 +0,0 @@
1
- require 'fastlane'
2
- require 'open3'
3
- require 'fastlane_core/ui/ui'
4
- require_relative "file_utils"
5
-
6
- module Fastlane
7
- module Saucectl
8
- #
9
- # This class provides the ability to execute tests via configured specifications and capture the output of the sauce executable.
10
- #
11
- class Runner
12
- include FileUtils
13
- EXECUTABLE = 'saucectl'
14
- UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
15
-
16
- def execute
17
- unless File.exist?(EXECUTABLE)
18
- UI.user_error!("❌ sauce labs executable file does not exist! Expected sauce executable file to be located at:'#{Dir.pwd}/#{EXECUTABLE}'")
19
- end
20
-
21
- system("chmod +x #{EXECUTABLE}")
22
- system("./#{EXECUTABLE} run")
23
- end
24
-
25
- def system(*cmd)
26
- Open3.popen2e(*cmd) do |stdin, stdout_stderr, wait_thread|
27
- Thread.new do
28
- stdout_stderr.each { |out| UI.message(out) }
29
- end
30
- stdin.close
31
- wait_thread.value
32
- end
33
- end
34
- end
35
- end
36
- end
@@ -1,46 +0,0 @@
1
- require 'uri'
2
- require 'net/http'
3
- require 'json'
4
- require_relative 'api'
5
-
6
- module Fastlane
7
- module Saucectl
8
- # This class provides the ability to store, delete, and retrieve data from the Sauce Labs Storage API
9
- class Storage
10
- def initialize(config)
11
- @config = config
12
- end
13
-
14
- # Get App Storage Files
15
- # @return the set of files that have been uploaded to Sauce Storage by the requester.
16
- def retrieve_all_apps
17
- api = Fastlane::Saucectl::Api.new(@config)
18
- api.retrieve_all_apps
19
- end
20
-
21
- # Delete app by the Sauce Labs identifier of the stored file. You can look up file IDs using the Get App Storage Files endpoint.
22
- # https://docs.saucelabs.com/dev/api/storage/#get-app-storage-files
23
- # @return json response containing the file id and the number of files deleted.
24
- def delete_app_with_file_id
25
- api = Fastlane::Saucectl::Api.new(@config)
26
- api.delete_app("v1/storage/files/#{@config[:app_id]}")
27
- end
28
-
29
- # Deletes the specified group of files from Sauce Storage.
30
- # The Sauce Labs identifier of the group of files. You can look up file IDs using the Get App Storage Groups endpoint.
31
- # https://docs.saucelabs.com/dev/api/storage/#get-app-storage-groups
32
- # @return json response containing the group ID and the number of files deleted.
33
- def delete_all_apps_for_group_id
34
- path = "v1/storage/files/#{@config[:group_id]}"
35
- api = Fastlane::Saucectl::Api.new(@config)
36
- api.delete_app(path)
37
- end
38
-
39
- # Uploads an application file to Sauce Storage for the purpose of mobile application testing
40
- # @return a unique file ID assigned to the app.
41
- def upload_app
42
- Fastlane::Saucectl::Api.new(@config).upload
43
- end
44
- end
45
- end
46
- end
@@ -1,201 +0,0 @@
1
- require 'base64'
2
- require 'fastlane'
3
- require 'fastlane_core/ui/ui'
4
- require_relative 'espresso'
5
- require_relative 'xctest'
6
-
7
- module Fastlane
8
- module Saucectl
9
- #
10
- # This class will create test suites based on user specified configuration properties
11
- #
12
- class Suites
13
- include FileUtils
14
-
15
- UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
16
-
17
- def initialize(config)
18
- @config = config
19
- end
20
-
21
- def create_test_plan
22
- check_kind
23
- if @config[:platform].casecmp('ios').zero?
24
- is_ios_reqs_satisfied?
25
- Fastlane::Saucectl::XCTest.new(@config)
26
- else
27
- Fastlane::Saucectl::Espresso.new(@config)
28
- end
29
- end
30
-
31
- def check_kind
32
- if @config[:platform].eql?('android')
33
- UI.user_error!("❌ #{@config[:kind]} is not a supported test framework for android. Use espresso") unless @config[:kind].eql?('espresso')
34
- else
35
- UI.user_error!("❌ #{@config[:kind]} is not a supported test framework for iOS. Use xcuitest") unless @config[:kind].eql?('xcuitest')
36
- end
37
- end
38
-
39
- def is_ios_reqs_satisfied?
40
- if @config[:test_target].nil? && @config[:test_plan].nil?
41
- UI.user_error!("❌ For ios you must specify test_target or test_plan")
42
- end
43
- end
44
-
45
- def test_distribution_array
46
- @config[:test_class] || create_test_plan.test_distribution
47
- end
48
-
49
- def suite_name(test_type)
50
- if ENV['JOB_NAME'].nil? && ENV['BUILD_NUMBER'].nil?
51
- "#{@config[:kind]}-#{test_type.split('.')[-1]}"
52
- else
53
- "#{ENV['JOB_NAME']}-#{ENV['BUILD_NUMBER']}-#{test_type.split('.')[-1]}"
54
- end
55
- end
56
-
57
- def create_virtual_device_suites
58
- if @config[:test_distribution] == 'shard'
59
- shard_virtual_device_suites
60
- elsif @config[:test_class]
61
- custom_test_classes
62
- else
63
- test_suites = []
64
- @config[:emulators].each do |emulator|
65
- test_distribution_array.each do |test_type|
66
- test_suites << {
67
- 'name' => suite_name(test_type).downcase,
68
- 'testOptions' => default_test_options(test_type)
69
- }.merge(virtual_device_options(emulator))
70
- end
71
- end
72
- test_suites
73
- end
74
- end
75
-
76
- def shard_virtual_device_suites
77
- UI.user_error!("❌ Cannot split #{@config[:test_distribution]}'s across virtual devices with a single emulator. \nPlease specify a minimum of two devices!") if @config[:emulators].size.eql?(1)
78
- test_suites = []
79
- arr = test_distribution_array
80
- shards = arr.each_slice((arr.size / @config[:emulators].size.to_f).round).to_a
81
- shards.each_with_index do |suite, i|
82
- test_suites << {
83
- 'name' => suite_name("shard #{i + 1}").downcase,
84
- 'testOptions' => default_test_options(suite)
85
- }.merge(virtual_device_options(@config[:emulators][i]))
86
- end
87
- test_suites
88
- end
89
-
90
- def shard_real_device_suites
91
- test_suites = []
92
- arr = test_distribution_array
93
- shards = arr.each_slice((arr.size / @config[:devices].size.to_f).round).to_a
94
- shards.each_with_index do |suite, i|
95
- test_suites << {
96
- 'name' => suite_name("shard #{i + 1}").downcase,
97
- 'testOptions' => default_test_options(suite)
98
- }.merge(real_device_options(@config[:devices][i]))
99
- end
100
- test_suites
101
- end
102
-
103
- def test_plan_suites
104
- test_suites = []
105
- @config[:devices].each do |device|
106
- test_suites << {
107
- 'name' => suite_name(@config[:test_plan].to_s).downcase,
108
- 'testOptions' => default_test_options(test_distribution_array)
109
- }.merge(real_device_options(device))
110
- end
111
- test_suites
112
- end
113
-
114
- def custom_test_classes
115
- test_suites = []
116
- devices = @config[:devices].nil? ? @config[:emulators] : @config[:devices]
117
- devices.each do |device|
118
- device_options = @config[:devices].nil? ? virtual_device_options(device) : real_device_options(device)
119
- test_classes = @config[:test_class].reject(&:empty?).join(',')
120
- test_suites << {
121
- 'name' => suite_name(device[:name]).downcase,
122
- 'testOptions' => default_test_options(test_classes.split(','))
123
- }.merge(device_options)
124
- end
125
- test_suites
126
- end
127
-
128
- def create_real_device_suites
129
- if !@config[:test_plan].nil? && @config[:test_distribution].eql?('class')
130
- test_plan_suites
131
- elsif @config[:test_distribution] == 'shard'
132
- shard_real_device_suites
133
- elsif @config[:test_class].kind_of?(Array)
134
- custom_test_classes
135
- else
136
- test_suites = []
137
- @config[:devices].each do |device|
138
- test_distribution_array.each do |test_type|
139
- test_suites << {
140
- 'name' => suite_name(test_type).downcase,
141
- 'testOptions' => default_test_options(test_type)
142
- }.merge(real_device_options(device))
143
- end
144
- end
145
- test_suites
146
- end
147
- end
148
-
149
- def virtual_device_options(device)
150
- platform_versions = device[:platform_versions].reject(&:empty?).join(',')
151
- { 'emulators' => [{ 'name' => device[:name],
152
- 'orientation' => device[:orientation],
153
- 'platformVersions' => platform_versions.split(',') }] }
154
- end
155
-
156
- def real_device_options(device)
157
- { 'devices' => [rdc_options(device)] }
158
- end
159
-
160
- def rdc_options(device)
161
- device_type_key = device.key?(:id) ? 'id' : 'name'
162
- name = device.key?(:id) ? device[:id] : device[:name]
163
-
164
- base_device_hash = {
165
- device_type_key => name,
166
- 'orientation' => device[:orientation]
167
- }.merge('options' => device_options(device))
168
-
169
- unless device[:platform_version].nil?
170
- base_device_hash = base_device_hash.merge({ 'platformVersion' => device[:platform_version] })
171
- end
172
-
173
- base_device_hash
174
- end
175
-
176
- def device_options(device)
177
- {
178
- 'carrierConnectivity' => device[:carrier_connectivity],
179
- 'deviceType' => device[:device_type].upcase!,
180
- 'private' => device[:private]
181
- }
182
- end
183
-
184
- def default_test_options(test_type)
185
- test_option_type = @config[:test_distribution].eql?('package') ? 'package' : 'class'
186
- if @config[:platform] == 'android'
187
- { test_option_type => test_type }.merge(android_test_options)
188
- else
189
- { 'class' => test_type }
190
- end
191
- end
192
-
193
- def android_test_options
194
- {
195
- 'clearPackageData' => @config[:clear_data],
196
- 'useTestOrchestrator' => @config[:use_test_orchestrator]
197
- }
198
- end
199
- end
200
- end
201
- end